lyft/protoc-gen-star

GitHub: lyft/protoc-gen-star

protoc-gen-star 是一个 Go 语言的 protoc 插件开发库,通过构建完整的 proto AST 和模块化架构来简化高效的自定义代码生成器开发。

Stars: 670 | Forks: 75

# protoc-gen-star (PG*) [![Build Status](https://travis-ci.org/lyft/protoc-gen-star.svg?branch=master)](https://travis-ci.org/lyft/protoc-gen-star) [![GoDoc](https://godoc.org/github.com/lyft/protoc-gen-star?status.svg)](https://godoc.org/github.com/lyft/protoc-gen-star) **!!! 此项目正在开发中 | API 应视为不稳定状态 !!!** _PG* 是一个 protoc 插件库,用于高效的基于 proto 的代码生成_ ``` package main import "github.com/lyft/protoc-gen-star/v2" func main() { pgs.Init(pgs.DebugEnv("DEBUG")). RegisterModule(&myPGSModule{}). RegisterPostProcessor(&myPostProcessor{}). Render() } ``` ## 功能 ### 文档 虽然本 README 试图描述 `protoc` 插件开发和 PG* 使用的许多细节,但真正的文档来源是代码本身。Go 语言是自文档化的,并提供了易于阅读和查看示例的工具。文档可以在 [GoDoc](https://godoc.org/github.com/lyft/protoc-gen-star) 上查看,或者在本地运行 `make docs`,这将启动一个 `godoc` 服务器并在默认浏览器中打开它们。 ### 路线图 - [x] 基于接口且完全链接的依赖图,支持访问原始 descriptor - [x] 内置的上下文感知调试功能 - [x] 详尽的、接近 100% 的单元测试覆盖率 - [x] 通过可覆盖的 IO 和基于接口的 API 实现端到端测试 - [x] [`Visitor`][visitor] 模式和辅助工具,用于高效遍历依赖图 - [x] [`BuildContext`][context] 以促进复杂的代码生成 - [x] 已解析的、类型化的命令行 [`Parameters`][params] 访问 - [x] 可扩展的 `ModuleBase`,用于快速创建 `Modules` 并促进代码生成 - [x] 可配置的生成文件后处理(例如 gofmt) - [x] 支持处理来自多个 package 的 proto 文件 - [x] 将 proto 文件中的注释(通过 SourceCodeInfo)加载到收集的 AST 中以便于访问 - [x] 特定语言的辅助子包,用于处理常见的、复杂的生成任务 - [ ] 使用 Go 共享库在运行时加载插件/模块 ### 示例 [`protoc-gen-example`][pge] 可以在 `testdata` 目录中找到。它包含两个 `Module` 实现,使用了各种可用的功能。它的 `protoc` 执行包含在 `testdata/generated` [Makefile][make] 目标中。也可以通过运行 `make docs` 生成的文档来访问示例。 ## 工作原理 ### `protoc` 流程 由于这个过程有点令人困惑,本节将介绍 proto 文件如何转换为生成代码的整个流程,这里使用一个假设的 PG* 插件:`protoc-gen-myplugin`。典型的执行如下所示: ``` protoc \ -I . \ --myplugin_out="foo=bar:../generated" \ ./pkg/*.proto ``` `protoc`(即 PB 编译器)使用一组标志(在 `protoc -h` 中有文档说明)进行配置,并接收一组文件作为参数。在这种情况下,`I` 标志可以多次指定,它是用于查找 proto 文件中导入依赖项的路径。默认情况下,官方的 descriptor proto 已经包含在内。 `myplugin_out` 告诉 `protoc` 使用 `protoc-gen-myplugin` 这个 protoc 插件。这些插件会从系统的 `PATH` 环境变量中自动解析,或者可以通过另一个标志显式指定。官方的 protoc 插件(例如 `protoc-gen-python`)已经注册到 `protoc` 中。该标志的值特定于具体的插件,但 `:../generated` 后缀是个例外。这个后缀表示 `protoc` 将放置该 package 生成文件的根目录(相对于当前工作目录)。然而,这个生成输出目录 _不会_ 传递给 `protoc-gen-myplugin`,因此它需要在标志的左侧进行复制。PG* 通过 `output_path` 参数支持这一点。 `protoc` 解析传入的 proto 文件,确保它们语法正确,并加载所有导入的依赖项。它将这些文件和依赖项转换为 descriptor(它们本身就是 PB message),并创建一个 `CodeGeneratorRequest`(又一个 PB)。`protoc` 序列化此请求,然后执行每个配置好的 protoc 插件,并通过 `stdin` 发送 payload。 `protoc-gen-myplugin` 启动,接收并反序列化请求 payload。基于 PG* 的 protoc 插件分为两个阶段。首先,PG* 反序列化从 `protoc` 接收到的 `CodeGeneratorRequest`,并为每个文件及其包含的所有实体创建一个完全连接的抽象语法树(AST)。同时还会解析为此插件指定的任何参数,以供后续使用。 当此步骤完成时,PG* 将执行任何已注册的 `Modules`,并将构建的 AST 交给它们。`Modules` 可以被编写为用于生成产物(例如文件),或者只是对提供的图执行某种形式的验证,而不产生任何其他副作用。在针对 PB 进行操作方面,`Modules` 提供了极大的灵活性。 一旦所有 `Modules` 运行完毕,PG* 会将任何自定义产物写入文件系统,或者将特定于生成器的产物序列化到 `CodeGeneratorResponse` 中,并将数据发送到其 `stdout`。`protoc` 接收此 payload,将其反序列化,并在所有插件返回后将请求的文件持久化到磁盘。整个流程如下所示: ``` foo.proto → protoc → CodeGeneratorRequest → protoc-gen-myplugin → CodeGeneratorResponse → protoc → foo.pb.go ``` PG* 库隐藏了实现 protoc 插件所需的几乎所有这些复杂性! ### Modules PG* `Modules` 会接收到一个完整的 AST,其中包含了作为生成目标的文件以及所有的依赖项。然后,`Module` 可以将文件添加到 protoc 的 `CodeGeneratorResponse` 中,或者直接将文件作为 `Artifacts` 写入磁盘。 PG* 提供了一个 `ModuleBase` 结构体来简化模块的开发。开箱即用时,它满足了 `Module` 的接口,只需要创建 `Name` 和 `Execute` 方法。`ModuleBase` 最适合作为包装 `Module` 实现的匿名嵌入字段。一个最小的模块如下所示: ``` // ReportModule creates a report of all the target messages generated by the // protoc run, writing the file into the /tmp directory. type reportModule struct { *pgs.ModuleBase } // New configures the module with an instance of ModuleBase func New() pgs.Module { return &reportModule{&pgs.ModuleBase{}} } // Name is the identifier used to identify the module. This value is // automatically attached to the BuildContext associated with the ModuleBase. func (m *reportModule) Name() string { return "reporter" } // Execute is passed the target files as well as its dependencies in the pkgs // map. The implementation should return a slice of Artifacts that represent // the files to be generated. In this case, "/tmp/report.txt" will be created // outside of the normal protoc flow. func (m *reportModule) Execute(targets map[string]pgs.File, pkgs map[string]pgs.Package) []pgs.Artifact { buf := &bytes.Buffer{} for _, f := range targets { m.Push(f.Name().String()).Debug("reporting") fmt.Fprintf(buf, "--- %v ---", f.Name()) for i, msg := range f.AllMessages() { fmt.Fprintf(buf, "%03d. %v\n", i, msg.Name()) } m.Pop() } m.OverwriteCustomFile( "/tmp/report.txt", buf.String(), 0644, ) return m.Artifacts() } ``` `ModuleBase` 暴露了一个 PG* [`BuildContext`][context] 实例,并且已经加上了模块名称的前缀。调用 `Push` 和 `Pop` 可以向错误和调试消息中添加更多信息。在上面代码中,在记录“reporting”调试消息之前,目标 package 中的每个文件都被推送到上下文中。 该 base 还提供了辅助方法,用于添加或覆盖 protoc 生成的文件和自定义文件。上面的 execute 方法在 `/tmp/report.txt` 创建了一个自定义文件,并指定它应该覆盖同名的现有文件。如果它调用的是 `AddCustomFile` 并且文件已经存在,则不会生成任何文件(尽管会输出一条调试消息)。对于添加 generator 文件、追加内容和注入,也存在类似的方法。同样,诸如 `AddCustomTemplateFile` 之类的方法允许渲染 `Templates` 来代替。 在所有模块执行完毕后,返回的 `Artifacts` 要么被放入 protoc 的 `CodeGenerationResponse` payload 中,要么被写出到文件系统。出于测试目的,文件系统已被抽象化,因此可以通过 `FileSystem` `InitOption` 向 PG* generator 提供自定义的文件系统(例如内存文件系统)。 #### 后处理 由 `Modules` 生成的 `Artifacts` 有时需要在写入磁盘或在发送给 protoc 的响应之前进行一些修改。这包括对 Go 源码运行 `gofmt`,或者为所有生成的源文件添加版权头。为了简化 PG* 中的这项任务,可以使用 `PostProcessor`。一个简单的 `PostProcessor` 实现可能如下所示: ``` // New returns a PostProcessor that adds a copyright comment to the top // of all generated files. func New(owner string) pgs.PostProcessor { return copyrightPostProcessor{owner} } type copyrightPostProcessor struct { owner string } // Match returns true only for Custom and Generated files (including templates). func (cpp copyrightPostProcessor) Match(a pgs.Artifact) bool { switch a := a.(type) { case pgs.GeneratorFile, pgs.GeneratorTemplateFile, pgs.CustomFile, pgs.CustomTemplateFile: return true default: return false } } // Process attaches the copyright header to the top of the input bytes func (cpp copyrightPostProcessor) Process(in []byte) (out []byte, err error) { cmt := fmt.Sprintf("// Copyright © %d %s. All rights reserved\n", time.Now().Year(), cpp.owner) return append([]byte(cmt), in...), nil } ``` `copyrightPostProcessor` 结构体通过实现 `Match` 和 `Process` 方法满足了 `PostProcessor` 接口。在 PG* 接收到所有 `Artifacts` 之后,每个产物都会依次被传递给每个已注册处理器的 `Match` 方法。在上面的例子中,如果文件属于目标 Artifact 类型,我们就返回 `true`。如果返回 `true`,则会立即调用 `Process` 方法并传入文件的渲染内容。此方法会修改输入,将修改后的值输出,或者在出现问题时返回错误。如上所述,通知被添加到了输入的最前面。 PostProcessor 在 PG* 中的注册方式与 `Modules` 类似: ``` g := pgs.Init(pgs.IncludeGo()) g.RegisterModule(some.NewModule()) g.RegisterPostProcessor(copyright.New("PG* Authors")) ``` ## Protocol Buffer AST 虽然 `protoc` 确保生成 proto 文件所需的所有依赖项都作为 descriptor 加载,但是识别它们之间关系的任务是由 protoc 插件来完成的。为了解决这个问题,PG* 为加载到插件中的所有 `Entities` 构建了一个抽象语法树(AST)。这个 AST 会提供给每个 `Module` 以促进代码生成。 ### 层次结构 由 PG* `gatherer` 生成的层次结构是完全链接的,从顶层的 `Package` 一直向下到 `Message` 的每个单独的 `Field`。AST 可以用以下有向图表示:

`Package` 描述了在同一个 namespace 中加载的一组 `Files`。正如预期的那样,`File` 代表单个 proto 文件,它包含任意数量的 `Message`、`Enum` 或 `Service` 实体。`Enum` 描述了一种基于整数的枚举类型,其中包含各个单独的 `EnumValue`。`Service` 描述了一组 RPC `Methods`,而这些方法又引用了它们的输入和输出 `Messages`。 一个 `Message` 可以包含其他嵌套的 `Messages` 和 `Enums`,以及它的每一个 `Fields`。对于非标量类型,`Field` 也可以引用其 `Message` 或 `Enum` 类型。作为实现联合类型的一种机制,`Message` 还可以包含引用其部分 `Fields` 的 `OneOf` 实体。 ### Visitor 模式 AST 的结构可能相当复杂且不可预测。同样,`Module` 通常只关心图中实体的一部分。为了将 `Module` 的算法与对 AST 结构的理解和遍历分离开来,PG* 实现了 `Visitor` 模式来解耦这两者。实现此接口非常简单,并且可以极大地简化代码生成。 PG* 提供了两个基础的 `Visitor` 结构体来简化实现开发。首先,`NilVisitor` 返回一个对所有 Entity 类型都进行短路执行的实例。当 AST 中的某些分支对代码生成没有意义时,这非常有用。例如,如果 `Module` 只关心 `Services`,它可以将 `NilVisitor` 用作匿名字段,并且只实现所需的接口方法: ``` // ServiceVisitor logs out each Method's name type serviceVisitor struct { pgs.Visitor pgs.DebuggerCommon } func New(d pgs.DebuggerCommon) pgs.Visitor { return serviceVistor{ Visitor: pgs.NilVisitor(), DebuggerCommon: d, } } // Passthrough Packages, Files, and Services. All other methods can be // ignored since Services can only live in Files and Files can only live in a // Package. func (v serviceVisitor) VisitPackage(pgs.Package) (pgs.Visitor, error) { return v, nil } func (v serviceVisitor) VisitFile(pgs.File) (pgs.Visitor, error) { return v, nil } func (v serviceVisitor) VisitService(pgs.Service) (pgs.Visitor, error) { return v, nil } // VisitMethod logs out ServiceName#MethodName for m. func (v serviceVisitor) VisitMethod(m pgs.Method) (pgs.Vistitor, error) { v.Logf("%v#%v", m.Service().Name(), m.Name()) return nil, nil } ``` 如果需要访问深层嵌套的 `Nodes`,则可以改用 `PassthroughVisitor`。与 `NilVisitor` 不同,正如其名字所暗示的那样,此实现会穿透所有节点,而不是在遇到第一个未实现的接口方法时进行短路。将此类型设置为匿名字段的设置稍微复杂一些,但避免了显式实现接口的每个方法: ``` type fieldVisitor struct { pgs.Visitor pgs.DebuggerCommon } func New(d pgs.DebuggerCommon) pgs.Visitor { v := &fieldVisitor{DebuggerCommon: d} v.Visitor = pgs.PassThroughVisitor(v) return v } func (v *fieldVisitor) VisitField(f pgs.Field) (pgs.Visitor, error) { v.Logf("%v.%v", f.Message().Name(), f.Name()) return nil, nil } ``` 使用任何 `Visitor` 遍历 AST 都非常简单: ``` v := visitor.New(d) err := pgs.Walk(v, pkg) ``` 所有的 `Entity` 类型和 `Package` 都可以传递给 `Walk`,如果需要,允许在比顶层 `Package` 更低的位置启动 `Visitor`。 ## Build Context 向 PG* `Generator` 注册的 `Modules` 会使用一个 `BuildContext` 实例进行初始化,该实例封装了上下文路径、调试和参数信息。 ### 输出路径 `BuildContext` 的 `OutputPath` 方法返回 PG* 插件所针对的输出目录。这个路径最初也是 `.`,但它指的是执行 `protoc` 的目录。可以通过在标志中提供 `output_path` 来覆盖此默认行为。 `OutputPath` 可用于为 `Artifacts` 创建文件名,使用 `JoinPath(name ...string)`,这本质上是 `filepath.Join(ctx.OutputPath(), name...)` 的别名。手动跟踪相对于 `OutputPath` 的目录可能会很繁琐,特别是当名称是动态的时候。相反,`BuildContext` 可以通过 `PushDir` 和 `PopDir` 来管理这些。 ``` ctx.OutputPath() // foo ctx.JoinPath("fizz", "buzz.go") // foo/fizz/buzz.go ctx = ctx.PushDir("bar/baz") ctx.OutputPath() // foo/bar/baz ctx.JoinPath("quux.go") // foo/bar/baz/quux.go ctx = ctx.PopDir() ctx.OutputPath() // foo ``` `ModuleBase` 包装了这些方法来改变其底层的 `BuildContexts`。应该使用这些方法,而不是直接使用包含的 `BuildContext` 上的方法。 ### 调试 `BuildContext` 暴露了一个 `DebuggerCommon` 接口,该接口提供了用于日志记录、错误检查和断言的工具。`Log` 和格式化的 `Logf` 将消息打印到 `os.Stderr`,通常以 `Module` 名称为前缀。`Debug` 和 `Debugf` 行为相同,但只有在通过 `DebugMode` 或 `DebugEnv` `InitOptions` 启用时才打印。 `Fail` 和 `Failf` 会立即停止 protoc 插件的执行,并使 `protoc` 使用提供的消息导致生成失败。如果传入了错误或者表达式计算结果为 false,`CheckErr` 和 `Assert` 也会分别显示提供的消息并导致失败。 可以通过在 `BuildContext` 上调用 `Push` 和 `Pop` 来提供额外的上下文前缀。这种行为类似于 `PushDir` 和 `PopDir`,但仅影响日志消息。`ModuleBase` 包装了这些方法以改变其底层的 `BuildContexts`。应该优先使用这些方法,而不是直接使用包含的 `BuildContext` 上的方法。 ### 参数 `BuildContext` 还提供了对来自指定 protoc 标志的预处理 `Parameters` 的访问。期望的唯一 PG* 特定键是 "output_path",模块的 `BuildContext` 将用于 `OutputPath`。 PG* 允许通过 `MutateParams` `InitOption` 修改 `Parameters`。通过在此处传入 `ParamMutator` 函数,可以在 PGG 工作流开始之前修改或验证这些 KV 对。 ## 特定语言的子包 虽然是用 Go 实现的,但 PG* 试图在其功能上保持语言无关性。因此,除了预生成的基础 descriptor 类型之外,PG* 不依赖于 protoc-gen-go (PGG) 包。然而,每种语言的 protoc 插件引入的许多细微差别都是可以泛化的。例如,PGG 的 package 命名、导入路径和输出路径是 proto package 名称、`go_package` 文件选项以及传递给 protoc 的参数之间复杂的相互作用。虽然 PG* 的核心 API 不应该被大量特定于语言的方法所重载,但可以提供子包,这些子包可以对 `Parameters` 和 `Entities` 进行操作以得出适当的结果。 PG* 目前实现了 [pgsgo](https://godoc.org/github.com/lyft/protoc-gen-star/v2/lang/go/) 子包,为针对 Go 语言的插件提供这些实用工具。计划在未来推出子包以支持多种语言。 ## PG* 开发与 Make 目标 PG* 寻求提供所有必要的工具,以便快速且符合人体工程学地扩展和构建在 Protocol Buffer IDL 之上。无论是旨在修改官方的 protoc-gen-go 输出,还是创建全新的文件和 package,该库都应该在 PB descriptor 和 protoc 插件工作流的复杂性之上提供一个用户友好的包装。 ### 设置 PG* 可以像任何标准 Go 模块一样进行安装和开发: ``` go get -u github.com/lyft/protoc-gen-star/v2 ``` ### Lint 与静态分析 为了避免风格挑剔,同时也为了执行 Go package 的一些最佳实践,PG* 要求所有代码更改都必须通过 `golint`、`go vet` 和 `go fmt -s`。 ``` make lint ``` ### 测试 PG* 致力于通过单元测试实现接近 100% 的代码覆盖率。大多数单元测试都是并行运行的,以捕获潜在的竞态条件。运行单元测试有三种方式,每种方式都比前一种耗时更长,但可以提供更多关于测试覆盖率的洞察: ``` # 为测试使用的数据运行代码生成 make testdata # 运行单元测试,不带 race detection 或 code coverage 报告 make quick # 运行单元测试,带 race detection 和 code coverage make tests # 运行单元测试,带 race detection 并生成 code coverage 报告,在浏览器中打开 make cover ``` #### protoc-gen-debug PG* 附带了一个专门的 protoc 插件 `protoc-gen-debug`。该插件从 protoc 执行中捕获 CodeGeneratorRequest,并将序列化的 PB 保存到磁盘。这些文件可用作输入,以避免在测试中调用 protoc。 ### 文档 Go 是一种自文档化的语言,并提供了内置的实用工具以在本地查看:`godoc`。以下命令启动一个 godoc 服务器,并打开一个浏览器窗口以查看此 package 的文档。如果您最初看到 404 或页面不可用,只需刷新即可。 ``` make docs ``` ### 演示 PG* 提供了一个“大杂烩”式的示例:[`protoc-gen-example`][pge]。这个建立在 PG* 之上的 protoc 插件会将目标 package 的 AST 作为树打印到 stderr。这提供了一种端到端的方式来验证 PB descriptor 中每个复杂的类型和嵌套: ``` # 创建基于 PG* 的示例 plugin make bin/protoc-gen-example # 针对演示 protos 运行 protoc-gen-example make testdata/generated ``` #### CI PG* 使用 [TravisCI][travis] 来验证所有代码更改。请查看 [配置][travis.yml] 以了解验证中涉及的测试。
标签:EVTX分析, Go语言, Protobuf, protoc插件, SOC Prime, 代码生成, 开发工具, 日志审计, 渗透测试工具, 程序破解, 自动化payload嵌入