由于想使用 protobuf 实现一个自己的 c++ RPC framework,可能需要编写 plugin 来完成一些自定义代码的生成,所以了解了一下 c++ plugin 的开发方法。网上这方面的资料几乎为0,只好去翻官方的 reference 了。
第一步:实现插件的 main 函数
对应官网 plugin 中的内容。所需头文件 google/protobuf/compiler/plugin.h
要使用 c++ 实现 protoc 插件,首先要实现一个 CodeGenerator (第二步中说),然后创建一个 main 函数:
1 2 3 4
| int main(int argc, char* argv[]) { MyCodeGenerator generator; return google::protobuf::compiler::PluginMain(argc, argv, &generator); }
|
protoc 插件是要编译成一个可执行文件的,这个 main 就是插件的入口。当 protoc 执行插件时,会调用 PluginMain(),将对应 .proto 文件的描述信息传递给我们自己的 CodeGenerator, 再由后者进行代码的生成。假设编译出的插件可执行文件名为 NAME,使用下面的命令让 protoc 执行插件:
1
| protoc --plugin=protoc-gen-NAME=path/to/my/plugin --NAME_out=OUT_DIR
|
第二步:自定义 CodeGenerator
自定义 MyGenerator 类,继承 CppGenerator。CppGenerator 自身也是 CodeGenerator 的子类,是专门用于生成 c++ 代码的。然后重写 CppGenerator 的 Generate() 方法。
1 2 3 4
|
virtual bool Generate(const FileDescriptor* file, const std::string& parameter, GeneratorContext* generator_context, std::string* error) const
|
参数:
file: proto 文件的描述符,包含了 proto 文件的所有信息。protobuf 中各种类都有对应的 Descriptor。
FileDescriptor 部分方法如下:
1
| const Descriptor* FindMessageTypeByName(ConstStringParam name) const
|
根据名称获取 message 的描述符
1
| int message_type_count() const
|
顶层 message 的个数(嵌套的不算)
1
| const Descriptor* message_type(int index) const
|
根据索引获取 message 的描述符,可以和 message_type_count() 配合遍历所有顶层 message
1
| const ServiceDescriptor* FindServiceByName(ConstStringParam name) const
|
根据名称获取 service 的描述符
1
| int service_count() const
|
service 的个数
1
| const ServiceDescriptor* service(int index) const
|
根据索引获取 service的描述符,可以和 service_count() 配合遍历所有 service.
还有好多方法,见官网
parameter:传递给 plugin 的命令行参数,可以通过 ParseGeneratorParameter() 函数解析。
1 2 3 4
|
void ParseGeneratorParameter(absl::string_view, std::vector<std::pair<std::string,std::string>>*);
|
解析到的参数以键值对形式保存在 vector 里。执行 protoc 时这样传参(假设插件名称为 plg):
1
| protoc test.proto --plg_out=param1=v1,param2=v2:. --plugin=protoc-gen-plg=/path/to/protoc-gen/plg
|
表示生成文件到当前目录,传递 param1,param2 两个参数。vector 中内容将会是
{{"param1", "v1"}, {"param2", "v2"}}
。
generator_context:代表输出目录。GeneratorContext 在旧版本中叫 OutputDirectory,代表 generator 生成文件的目录。部分方法如下:
1
| virtual io::ZeroCopyOutputStream* Open(const std::string& filename) = 0;
|
以截断方式打开输出文件
1
| virtual io::ZeroCopyOutputStream* OpenForAppend(const std::string& filename);
|
以追加方式打开输出文件
1 2
| virtual io::ZeroCopyOutputStream* OpenForInsert( const std::string& filename, const std::string& insertion_point);
|
在指定的插入点处插入,插入点的概念见此处,目前还没有仔细研究过这种方法。
简单 DEMO
test.proto
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| syntax = "proto3";
package tutorial;
message Person { optional string name = 1; optional int32 id = 2; optional string email = 3;
enum PhoneType { PHONE_TYPE_UNSPECIFIED = 0; PHONE_TYPE_MOBILE = 1; PHONE_TYPE_HOME = 2; PHONE_TYPE_WORK = 3; }
message PhoneNumber { optional string number = 1; optional PhoneType type = 2; }
repeated PhoneNumber phones = 4; }
message AddressBook { repeated Person people = 1; }
message ResultCode { int32 errcode = 1; bytes errmsg = 2; }
message LoginResponse { ResultCode result = 1; bool success = 2; }
option cc_generic_services = true;
service PersonLoginService { rpc Login(Person) returns(LoginResponse); rpc GetPhone(Person) returns(Person.PhoneNumber); }
|
编写一个简单的 plugin,将输入参数,所有顶层 message 的名称,service 以及 method 的信息输出到生成文件 plg_out.txt 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| #include "google/protobuf/compiler/cpp/generator.h" #include "google/protobuf/compiler/code_generator.h" #include "google/protobuf/compiler/plugin.h" #include "google/protobuf/descriptor.h" #include "google/protobuf/io/zero_copy_stream.h" #include "google/protobuf/io/printer.h"
using namespace google::protobuf; using namespace google::protobuf::io; using namespace google::protobuf::compiler;
class MyGenerator : public google::protobuf::compiler::cpp::CppGenerator { bool Generate(const FileDescriptor* file, const std::string& parameter, GeneratorContext* generator_context, std::string* error) const override { auto stream = generator_context->Open("plg_out.txt"); Printer printer(stream);
printer.PrintRaw("input params: \n"); std::vector<std::pair<std::string, std::string>> params; ParseGeneratorParameter(parameter, ¶ms); for(const auto& p: params) { printer.PrintRaw(p.first + "=" + p.second + "\n"); } printer.PrintRaw("\n");
printer.PrintRaw("messages: \n"); for(int i = 0; i < file->message_type_count(); i++) { auto message = file->message_type(i); printer.PrintRaw(message->name() + "\n"); } printer.PrintRaw("\n");
auto service = file->service(0); printer.PrintRaw("service: \n"); printer.PrintRaw(service->DebugString()); printer.PrintRaw("methods in this service: \n"); for(int i = 0; i < service->method_count(); i++) { auto method = service->method(i); printer.PrintRaw(method->name() + "\n"); auto input = method->input_type(); auto output = method->output_type(); printer.PrintRaw("method input: " + input->name() + "\n"); printer.PrintRaw("method output: " + output->name() + "\n"); }
return true; } };
int main(int argc, char* argv[]) { MyGenerator generator; return PluginMain(argc, argv, &generator); }
|
plg_out 内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| input params: param1=v1 param2=v2
messages: Person AddressBook ResultCode LoginResponse
service: service PersonLoginService { rpc Login(.tutorial.Person) returns (.tutorial.LoginResponse); rpc GetPhone(.tutorial.Person) returns (.tutorial.Person.PhoneNumber); } methods in this service: Login method input: Person method output: LoginResponse GetPhone method input: Person method output: PhoneNumber
|
注:插件链接的时候要链接 libprotoc,而普通的使用 protobuf 的程序不需要。而且 libprotoc 要最先链接,CMakeList 如下:
1 2 3 4 5 6 7 8
| find_package(PkgConfig) pkg_search_module(Protobuf REQUIRED protobuf) include_directories(${Protobuf_INCLUDE_DIRS})
link_directories(${Protobuf_LIBRARY_DIRS}) link_libraries(protoc ${Protobuf_LIBRARIES})
add_executable(protoc-gen-plg plugin_test.cpp)
|