首页 > 其他分享 >protoc插件(一):如何快速编写一个protoc插件

protoc插件(一):如何快速编写一个protoc插件

时间:2022-12-01 01:11:08浏览次数:53  
标签:插件 protoc generator proto -- file 编写

利用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;
}

于是我们只需要知道两点:

  1. protoc进程将proto文件的信息封装为CodeGeneratorRequest传递给插件子进程,插件子进程将根据CodeGeneratorRequest中的信息,将要生成的代码数据封装为CodeGeneratorResponse对象传递给protoc进程
  2. 插件进程从标准输入读取出CodeGeneratorRequest数据,将CodeGeneratorResponse数据写到标准输出

CodeGeneratorRequestCodeGeneratorRequest两者也是使用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插件的开发可以简单分为三步:

  1. 从标准输入读取解析出CodeGeneratorRequest数据
  2. 利用读取的数据来生成对应的代码
  3. 将生成的结果封装为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

相关文章