温故知新,机器人进化论之瑞士军刀Protobuf,自动将嵌套层级的Proto自动编译成CSharp代码
背景
在机器人和工业自动化领域,我们经常遇到这样的架构:底层的运动控制(Control/Planning)使用C++运行在Linux/ROS环境下,而上层的调度或监控系统使用C# (.NET) 运行在Windows环境下。
它们之间的通讯语言,最常用的就是Google的Protocol Buffers(Protobuf)。
什么是Protobuf

简单来说,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的项目,把这些文件包括在其中


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

3、给当前项目安装Nuget依赖
Google.ProtobufGrpc.Tools(编译工具)Grpc.Net.Client(可选,如果涉及 gRPC 通讯)


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

添加如下配置。ProtoRoot是解决问题的关键,它相当于C++编译器中的-I参数,告诉编译器:“所有的import路径都是相对于这个目录开始算的”。
<ItemGroup>
<Protobuf Include="proto\**\*.proto"
ProtoRoot="proto"
GrpcServices="Client" />
</ItemGroup>

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

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

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} 字节");
}
}
}

契约驱动开发
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)-> 还原成对象。

浙公网安备 33010602011771号