温故知新,机器人进化论之瑞士军刀Protobuf,自动将嵌套层级的Proto自动编译成CSharp代码

背景

在机器人和工业自动化领域,我们经常遇到这样的架构:底层的运动控制(Control/Planning)使用C++运行在Linux/ROS环境下,而上层的调度或监控系统使用C# (.NET) 运行在Windows环境下。

它们之间的通讯语言,最常用的就是Google的Protocol Buffers(Protobuf)。

什么是Protobuf

image

简单来说,Protobuf是代码世界的“通用翻译官”

跨语言:它让C++ 的结构体能被C#读懂,就像JSON 一样,但比JSON更强大。

高性能:它传输的是二进制(0101...),比文本格式的JSON体积小得多,解析速度快10倍以上。

强契约:通过.proto文件定义数据结构,一旦定义好,类型就是安全的。

场景重现

假设我们的 C++ 工程目录结构如下,不仅有子文件夹,还有跨文件夹的引用(Import):

MyProject/
├── proto/                  <-- 【关键】这是 Proto 的根目录
│   ├── common/             <-- 公共基础库
│   │   └── geometry.proto  <-- 定义了 Point3D 等基础类型
│   └── robot/              <-- 具体的业务模块
│       └── status.proto    <-- 这里引用了 common/geometry.proto
└── MyCSharpApp/            <-- 我们的 C# 项目
    └── MyCSharpApp.csproj

基础文件:common/geometry.proto

syntax = "proto3";
package common;
option csharp_namespace = "SmartLogistics.Common";

message Point3D {
  double x = 1;
  double y = 2;
  double z = 3;
}

业务文件:robot/status.proto

它引用了common目录下的文件

syntax = "proto3";
package robot;
option csharp_namespace = "SmartLogistics.Robot";

// 注意这里的路径:它是基于 proto/ 根目录写的
import "common/geometry.proto"; 
import "google/protobuf/timestamp.proto";

message VehicleStatus {
  string device_id = 1;
  // 直接使用了引用的类型
  common.Point3D current_location = 2; 
  google.protobuf.Timestamp last_updated = 3;
}

解决方案

1、创建一个.Net的项目,把这些文件包括在其中

image

image

2、将proto目录整体放入项目中来

image

3、给当前项目安装Nuget依赖

  • Google.Protobuf
  • Grpc.Tools (编译工具)
  • Grpc.Net.Client (可选,如果涉及 gRPC 通讯)

image

image

4、双击当前项目进入项目文件编辑.csproj

image

添加如下配置。ProtoRoot是解决问题的关键,它相当于C++编译器中的-I参数,告诉编译器:“所有的import路径都是相对于这个目录开始算的”。

<ItemGroup>
  <Protobuf Include="proto\**\*.proto"
            ProtoRoot="proto"
            GrpcServices="Client" />
</ItemGroup>

image

5、直接重新生成即可

配置完成后,点击“生成”,编译器会在后台自动生成C#类。我们完全不需要手写序列化代码,直接使用即可

image

这时候这些proto对应的.cs文件就在/obj目录下,如果你顺利看到你要的cs文件,说明前面的配置对了,如果没看到说明前面的配置有问题,尤其是注意刚才那个ItemGroup节点对应的路径哈。

image

6、使用

using System;
using Google.Protobuf.WellKnownTypes; // 用于 Timestamp
using SmartLogistics.Common;          // 自动生成的命名空间
using SmartLogistics.Robot;

namespace MyCSharpApp
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // 创建一个复杂的嵌套对象
            var status = new VehicleStatus
            {
                DeviceId = "AGV-2026-X1",
                
                // 直接使用引用的 Point3D 类型
                CurrentLocation = new Point3D 
                { 
                    X = 10.5, 
                    Y = 20.1, 
                    Z = 0 
                },
                
                // 使用 Google 标准时间戳
                LastUpdated = Timestamp.FromDateTime(DateTime.UtcNow)
            };

            Console.WriteLine($"设备 {status.DeviceId} 位置: ({status.CurrentLocation.X}, {status.CurrentLocation.Y})");
            
            // 序列化为二进制(模拟发送给 C++ 端)
            byte[] bytes = status.ToByteArray();
            Console.WriteLine($"序列化后大小: {bytes.Length} 字节");
        }
    }
}

image

契约驱动开发

1、定义契约 (.proto 文件)

这是“宪法”。无论是写C++还是C#,都要遵守这个文件的定义

message RobotStatus {
  int32 battery = 1; // 1号字段是电量
}

其中这个=1很关键,这是Protobuf最精髓的设计。

在JSON里,我们存数据是这样的:"battery": 80。你存了"battery"这个单词,占了7个字节。 在Protobuf里,数据传输时不传变量名,只传编号(Tag),比JSON节省了极其可观的空间。

2、代码生成(Compilation)

使用protoc编译器,比如C#项目中就是Grpc.Tools自动完成,而在C++项目中就是protoc + libprotobuf

  • 它帮C++生成RobotStatus.pb.h
  • 它帮C#生成RobotStatus.cs

开发者不需要自己写序列化/反序列化的代码。

3、序列化与反序列化(Runtime)

  • C++ 端:robot_status.SerializeToString(&output) -> 得到二进制流。
  • 网络传输:二进制流飞过网线...
  • C# 端:RobotStatus.Parser.ParseFrom(bytes) -> 还原成对象。
posted @ 2026-01-19 17:04  TaylorShi  阅读(12)  评论(0)    收藏  举报