利用proto的IDL文件,我们可以用来描述服务和接口的定义。并利用protoc编译器来快速生成需要的代码。
proto原生支持一部分语言的生成器
// Proto2 C++
cpp::CppGenerator cpp_generator;
cli.RegisterGenerator("--cpp_out", "--cpp_opt", &cpp_generator,
"Generate C++ header and source.");
#ifdef GOOGLE_PROTOBUF_RUNTIME_INCLUDE_BASE
cpp_generator.set_opensource_runtime(true);
cpp_generator.set_runtime_include_base(GOOGLE_PROTOBUF_RUNTIME_INCLUDE_BASE);
#endif
// Proto2 Java
java::JavaGenerator java_generator;
cli.RegisterGenerator("--java_out", "--java_opt", &java_generator,
"Generate Java source file.");
#ifdef GOOGLE_PROTOBUF_RUNTIME_INCLUDE_BASE
java_generator.set_opensource_runtime(true);
#endif
// Proto2 Kotlin
java::KotlinGenerator kt_generator;
cli.RegisterGenerator("--kotlin_out", "--kotlin_opt", &kt_generator,
"Generate Kotlin file.");
// Proto2 Python
python::Generator py_generator;
cli.RegisterGenerator("--python_out", "--python_opt", &py_generator,
"Generate Python source file.");
#ifdef GOOGLE_PROTOBUF_RUNTIME_INCLUDE_BASE
py_generator.set_opensource_runtime(true);
#endif
// Python pyi
python::PyiGenerator pyi_generator;
cli.RegisterGenerator("--pyi_out", &pyi_generator,
"Generate python pyi stub.");
// PHP
php::Generator php_generator;
cli.RegisterGenerator("--php_out", "--php_opt", &php_generator,
"Generate PHP source file.");
// Ruby
ruby::Generator rb_generator;
cli.RegisterGenerator("--ruby_out", "--ruby_opt", &rb_generator,
"Generate Ruby source file.");
// CSharp
csharp::Generator csharp_generator;
cli.RegisterGenerator("--csharp_out", "--csharp_opt", &csharp_generator,
"Generate C# source file.");
// Objective-C
objectivec::ObjectiveCGenerator objc_generator;
cli.RegisterGenerator("--objc_out", "--objc_opt", &objc_generator,
"Generate Objective-C header and source.");
而对于其它语言,或者想要自定义生成的代码的,可以通过插件的方式来工作。例如go语言的生成器protoc-gen-go。
protoc插件的工作方式
protoc插件是一个独立的二进制程序,protoc进程通过fork生成子进程,并exec加载插件程序运行。父子进程间通过管道通信,并将管道的输入和输出重定向到标准输入和标准输出。
核心代码如下:
bool CommandLineInterface::GeneratePluginOutput(
const std::vector<const FileDescriptor*>& parsed_files,
const std::string& plugin_name, const std::string& parameter,
GeneratorContext* generator_context, std::string* error) {
// 请求与响应
CodeGeneratorRequest request;
CodeGeneratorResponse response;
std::string processed_parameter = parameter;
// ...
// 启动插件子进程
Subprocess subprocess;
if (plugins_.count(plugin_name) > 0) {
subprocess.Start(plugins_[plugin_name], Subprocess::EXACT_NAME);
} else {
subprocess.Start(plugin_name, Subprocess::SEARCH_PATH);
}
//写入请求并获得响应
std::string communicate_error;
if (!subprocess.Communicate(request, &response, &communicate_error)) {
*error = absl::Substitute("$0: $1", plugin_name, communicate_error);
return false;
}
// ...
// 写文件
std::unique_ptr<io::ZeroCopyOutputStream> current_output;
for (int i = 0; i < response.file_size(); i++) {
const CodeGeneratorResponse::File& output_file = response.file(i);
// ...
// our own buffer-copying loop.
io::CodedOutputStream writer(current_output.get());
writer.WriteString(output_file.content());
}
// ...
return true;
}
于是我们只需要知道两点:
- protoc进程将proto文件的信息封装为
CodeGeneratorRequest
传递给插件子进程,插件子进程将根据CodeGeneratorRequest
中的信息,将要生成的代码数据封装为CodeGeneratorResponse
对象传递给protoc进程 - 插件进程从标准输入读取出
CodeGeneratorRequest
数据,将CodeGeneratorResponse
数据写到标准输出
CodeGeneratorRequest
和CodeGeneratorRequest
两者也是使用proto定义的
// CodeGeneratorRequest 编码后写入到插件的标准输入
message CodeGeneratorRequest {
// 命令行中显式列出的proto文件,代码生成器只为这些文件生成代码。对应的描述信息在下面的proto_file中
repeated string file_to_generate = 1;
// 传递给生成器的命令行参数
optional string parameter = 2;
// FileDescriptorProto 描述了proto文件的所有信息
repeated FileDescriptorProto proto_file = 15;
// protoc的版本号
optional Version compiler_version = 3;
}
// 插件将编码后的 CodeGeneratorResponse 写入到标准输出
message CodeGeneratorResponse {
// 错误信息
optional string error = 1;
// 代码生成器支持的特性,值来自于Feature
optional uint64 supported_features = 2;
// Sync with code_generator.h.
enum Feature {
FEATURE_NONE = 0;
FEATURE_PROTO3_OPTIONAL = 1;
}
// 描述一个生成的文件的信息
message File {
// 文件名,相对于输入目录
optional string name = 1;
// 插入点要求在指定的位置插入,所以要求name对应的文件必须已经存在
optional string insertion_point = 2;
// 文件的内容
optional string content = 15;
// 描述正在插入的文件内容的信息。如果使用了插入点,则该信息将被适当偏移并插入到生成文件的代码生成元数据中。
repeated File file = 15;
}
于是一个protoc插件的开发可以简单分为三步:
- 从标准输入读取解析出
CodeGeneratorRequest
数据 - 利用读取的数据来生成对应的代码
- 将生成的结果封装为
CodeGeneratorResponse
写入标准输出
插件实现
这里参照proto-gen-go的实现,并且利用google提供的Go版API,可以大大简化插件开发的过程。甚至可以说只需要做一件事,那就是创建一个protogen.Options
对象并调用其Run
方法。
整个框架看起来如下
func main() {
// 用于接收命令行参数
var (
flags flag.FlagSet
plugins = flags.String("plugins", "", "list of plugins to enable (supported values: grpc)")
importPrefix = flags.String("import_prefix", "", "prefix to prepend to import paths")
)
importRewriteFunc := func(importPath protogen.GoImportPath) protogen.GoImportPath {
switch importPath {
case "context", "fmt", "math":
return importPath
}
if *importPrefix != "" {
return protogen.GoImportPath(*importPrefix) + importPath
}
return importPath
}
protogen.Options{
ParamFunc: flags.Set,
ImportRewriteFunc: importRewriteFunc,
}.Run(func(gen *protogen.Plugin) error {
// ...
for _, f := range gen.Files {
// 根据proto文件信息来生成新文件
}
return nil
})
}
Options
有两个字段,这里介绍一下:
- ParamFunc:命令行中的插件参数会以
--go_out=<param1>=<value1>,<param2>=<value2>:<output_directory>
的形式输入并最总被解析为CodeGeneratorRequest
字段,Run方法运行过程中会读取出键值对并调用ParamFunc
函数,这样就可以将命令行参数绑定到flags对应的变量中了。 - ImportRewriteFunc:生成的新文件中的每个包导入的路径可以使用此函数进行重写。
只要在传递给Run方法的函数中编写好生成的逻辑就行了
我们简单编写一个打印message信息的插件
func main() {
protogen.Options{}.Run(func(p *protogen.Plugin) error {
// 遍历proto文件
for _, f := range p.Files {
fname := f.GeneratedFilenamePrefix + ".txt"
t := p.NewGeneratedFile(fname, f.GoImportPath)
for _, msg := range f.Messages {
builder := strings.Builder{}
for _, field := range msg.Fields {
builder.WriteString(field.Desc.TextName() + ": " + strconv.Itoa(field.Desc.Index()) + "\n")
}
t.Write([]byte(builder.String()))
}
// 后续使用t来写入新文件
}
return nil
})
}
go install安装程序,这里我们将插件命令为proto-gen-test,然后存在一个test.proto文件
syntax = "proto3";
package api;
option go_package = "api/v1;v1";
message HelloRequest {
string msg = 1;
}
执行protoc --test_out=. test.proto
,就会调用proto-gen-test程序。
--{name}_out 会调用名为protoc-gen-{name}的插件
最终并生成htest.txt文件,文件内容就只有简单的一行
msg: 0
那么这样一个非常简单的protoc插件就编写完成了。
标签:插件,protoc,generator,proto,--,file,编写 From: https://www.cnblogs.com/smarticen/p/16940135.html