前言
google开源的Protocol Buffers(后面简称protobuf)作为一种结构化数据序列化和反序列化的工具,在很多场景中都有应用,而extend的拓展机制又增加了很多可能性。下面通过一个示例来简单说明一下如何使用protobuf拓展和编写插件。
问题
游戏服务器开发的时候,会把数据放入protobuf结构中进行序列化,然后放入key-value db中,从db加载出来后再反序列化成一个protobuf对象,有时候我们可能不能直接使用protobuf对象,而是需要转换成一个C struct(如需要放入共享内存的时候)。这时候我们就需要一种很方便把protobuf定义的message可以映射成一个C结构,同时又能从一个C结构还原成protobuf的message的方式。每次都逐个字段的去写,即繁琐又容易出错,一般这种能让工具生成是最好的。
protobuf拓展
我们需要指定哪些message需要生成c struct,哪些不需要,因此我们可以定义message的拓展。 新建一个文件,假设叫做 pod_options.proto
import "google/protobuf/descriptor.proto";extend google.protobuf.MessageOptions{ // 1: need to generate pod data code // 2: need to generate pod data code without constructor optional int32 gen_pod = 30000;}
接下来,可以定义我们自己的test.proto了。
import "pod_options.proto";message NeedPod{ // 设置了1,需要生成 c struct option (gen_pod) = 1; optional uint32 id = 1;}message DontNeedPod{ optional uint32 id = 1;}
解析proto文件
有了拓展字段,接下来怎么获取呢,难道要修改protoc的源码,输出需要的c struct么? 其实,并不需要修改源码,起码有几种方法可以达到目的。
使用Importer
仔细阅读protoc源码,有一个 google::protobuf::compiler::Importer 类
// Simple interface for parsing .proto files. This wraps the process// of opening the file, parsing it with a Parser, recursively parsing all its// imports, and then cross-linking the results to produce a FileDescriptor. This is really just a thin wrapper around SourceTreeDescriptorDatabase.// You may find that SourceTreeDescriptorDatabase is more flexible. TODO(kenton): I feel like this class is not well-named.class PROTOBUF_EXPORT Importer {
public:
Importer(SourceTree* source_tree, MultiFileErrorCollector* error_collector);
// Import the given file and build a FileDescriptor representing it. If // the file is already in the DescriptorPool, the existing FileDescriptor // will be returned. The FileDescriptor is property of the DescriptorPool, // and will remain valid until it is destroyed. If any errors occur, they // will be reported using the error collector and Import() will return NULL. // // A particular Importer object will only report errors for a particular // file once. All future attempts to import the same file will return NULL // without reporting any errors. The idea is that you might want to import // a lot of files without seeing the same errors over and over again. If // you want to see errors for the same files repeatedly, you can use a // separate Importer object to import each one (but use the same // DescriptorPool so that they can be cross-linked). const FileDescriptor* Import(const std::string& filename);
//………………}
早期的protobuf版本就是用这个类来产生FileDescriptor,而有了FileDescriptor,就有了整个proto文件的所有信息。Importer的用法可以参照早期版本的protoc源码,或者在单元测试用例中也仍然有用到Importer,大概代码如下:
MultiFileErrorCollector error_collector;
DiskSourceTree source_tree;
// MapPath的用法可以查看源码的注释,这里就不细说了source_tree.MapPath("", "./");
Importer importer(&source_tree, &error_collector);
const FileDescriptor* parsed_descriptor = importer.Import("test.proto");
使用py2文件
还有一种方法可以以曲线救国的方式来实现我们的目的。我们知道protoc可以支持产出不同语言的目标文件,而针对python的目标文件就是py2.py文件,查看 py2 文件,可以看到,里面已经包含了FileDescriptor类了,因此,只要用python写一个脚本加载这个py2文件,就可以获得一个FileDescriptor类
protobuf插件
相比于前面的两种方法,protobuf官方支持自定义插件,只要根据protobuf要求的方式编写插件,大概的格式如下:
#include #include #include #include #include using namespace google::protobuf::compiler;
class PodCodeGenerator : public CodeGenerator
{
public:
virtual ~PodCodeGenerator(){};
virtual bool Generate(const ::google::protobuf::FileDescriptor* file_, const string& parameter, GeneratorContext* context, string* error) const
{
// 根据FileDescriptor,产生输出文件的信息 return true;
}
};
int main(int argc, char* argv[])
{
PodCodeGenerator generator;
return google::protobuf::compiler::PluginMain(argc, argv, &generator);
}
可以看到,Generate函数的参数里面已经有protobuf解析好的FileDescriptor了,接下来就是根据自己需要的格式生成了。至于调用插件的方式也是很简单,假设插件的二进制文件叫test_plugin,则如下:protoc --proto_path=your path --NAME_out=output path --plugin=protoc-gen-NAME=test_plugin test.proto
其中的NAME可以是自定义的字符串,另外还可以加上 -NAME_opt=your param 选项带上参数给插件。使用插件的方式可以很好的在单次解析中完成pb和自定义格式的输出。
P.S. 插件的示例源码