lyft/protoc-gen-star
GitHub: lyft/protoc-gen-star
protoc-gen-star 是一个 Go 语言的 protoc 插件开发库,通过构建完整的 proto AST 和模块化架构来简化高效的自定义代码生成器开发。
Stars: 670 | Forks: 75
# protoc-gen-star (PG*) [](https://travis-ci.org/lyft/protoc-gen-star) [](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 可以用以下有向图表示:

标签:EVTX分析, Go语言, Protobuf, protoc插件, SOC Prime, 代码生成, 开发工具, 日志审计, 渗透测试工具, 程序破解, 自动化payload嵌入