- 跨平台
- 图形学
- 前端
【免费下载链接】engine
The Flutter engine
Protocol Buffers(protobuf)是 Flutter Engine 依赖链中重要的序列化基础设施,而本仓库的 build/secondary/third_party/protobuf 目录保存的正是让 protobuf 能够被 GN 构建系统完整驱动的"胶水层":它定义了proto_library模板、protoc 调用封装脚本以及protobuf_lite/protobuf_full/protoc等 GN 目标的构建规则。本文将以该 README 为骨架,结合仓库中的 proto_library.gni、BUILD.gn、protoc_wrapper.py 等实现,讲清楚这套构建支持的设计动机、目录约定、模板参数以及 protoc 生成链路的底层原理。读完本文,你将能在自己的 GN 工程中正确配置 protobuf 依赖、书写proto_library目标,并理解生成代码与静态库之间的完整装配流程。
一、为什么 protobuf 的 GN 支持要"自成一体"
与普通的第三方库不同,protobuf 的 GN 构建支持被设计成一个可被多个大型仓库共享的独立组件。README 明确说明,它独立成仓的原因在于需要同时被 Fuchsia 与 Cobalt 两个项目复用:这两个项目都使用 GN 作为构建系统,也都在各自的依赖树中携带 protobuf,因此将"GN 如何构建 protobuf"这一套规则抽离出来,可以避免在每个宿主仓库中重复维护一份几乎相同的构建脚本。
在 Flutter Engine 中,这套支持被镜像到了build/secondary/third_party/protobuf/目录下,目录内包含的 proto_library.gni(声明proto_library模板)、BUILD.gn(protobuf 库与 protoc 编译器的构建目标)、protoc_wrapper.py(protoc 调用与输出后处理)、gen.py(自动生成 BUILD.gn 的脚本)以及 BUILD.input.gn(BUILD.gn 的模板源),共同构成了完整的 protobuf 构建支持体系。
二、目录布局约定与 secondary_source 机制
README 给出了这套支持正常工作的三个前提条件,这是任何工程接入时的第一道门槛:
- 支持仓位于
//build/secondary/third_party/protobuf:即当前仓库中的build/secondary/third_party/protobuf目录; - protobuf 本体位于
//third_party/protobuf:即 protobuf 的源码(含src/google/protobuf/下的实现)被检出在third_party/protobuf; - 根目录
//.gn中声明secondary_source = "//build/secondary/":这是 GN 提供的secondary_source机制——当 GN 在某个目录找不到.gni文件(如proto_library.gni)时,它会退回secondary_source指定的根目录继续查找。这样,任何模块都可以通过import("//third_party/protobuf/proto_library.gni")的方式引用模板,而实际文件却存放在build/secondary/下。
需要注意的是,README 描述的是该组件在 Fuchsia/Cobalt 等宿主仓库中的标准布局。而在当前 Flutter Engine 镜像中,BUILD.gn 内部实际引用的 protobuf 源码路径写的是//flutter/third_party/protobuf/src(见其protobuf_config与using_proto配置),与 BUILD.input.gn 中相对路径src的写法存在差异。从源码结构看,这应当是生成脚本在移植到不同宿主仓库时经过了额外的路径改写所致。因此在阅读或移植时,务必以自己仓库中 protobuf 的实际检出位置为准,保持secondary_source声明、import路径与源码 include 路径三者一致。
三、proto_library模板:一个.proto文件的完整构建入口
proto_library是这套支持的核心模板,定义于 proto_library.gni。它接收一个或多个.proto文件,自动完成"调用 protoc 生成代码 → 将生成代码编译为静态库 → 对外暴露 group 目标"的全过程。其最简单的用法只需一个sources参数:
import("//third_party/protobuf/proto_library.gni") proto_library("mylib") { sources = [ "foo.proto", ] }模板内部结构可以拆成三个层次:
${target_name}_protoc_outputs(generated_file):把待生成的输出文件清单写入${target_gen_dir}/${target_name}.protoc_output_info,供后续生成动作使用;${target_name}_gen(action):以 protoc_wrapper.py 为脚本,携带--protoc、--proto-in-dir、各输出目录参数实际调用 protoc;${target_name}_static_lib(static_library):收集 action 产出的.h/.cc文件,编译为同名静态库;若处于 component build 且设置了component_build_force_source_set = true,则降级为source_set;${target_name}(group):将 action 与静态库聚合,作为对外的统一目标,供上层deps引用。
3.1 核心参数一览
根据模板头部注释与实现,proto_library支持以下参数(带默认值的均为可选):
| 参数 | 默认值 | 作用 |
|---|---|---|
sources | 必填 | 待编译的.proto文件列表,缺失会触发断言 |
proto_in_dir | 自动推断 | .proto文件所在目录(相对当前 BUILD.gn),目录结构由此处起算;存在嵌套目录时必须显式指定,否则断言报错 |
proto_out_dir | 自动推断 | 输出文件在root_gen_dir下的后缀路径;Python stub 则输出到root_out_dir/pyproto下 |
generate_cc | true | 生成 C++ stub(*.pb.h/*.pb.cc) |
generate_python | true | 生成 Python stub(*_pb2.py) |
generate_descriptor_set | false | 在默认位置${target_out_dir}/${target_name}.desc.pb生成 descriptor set 文件 |
generate_descriptor | "" | 在 generated files 目录下指定位置生成 descriptor set;与上一项互斥 |
generate_go | false | 生成 Go stub(.pb.go) |
generate_go_grpc | false | 生成 Go gRPC stub(_grpc.pb.go),与generate_go互斥 |
cc_generator_options | 无 | 传给 C++ 生成器的额外选项,如dllexport_decl=FOO_EXPORT:(注意结尾冒号),用于给生成的 C++ 头文件注入导出宏 |
cc_include | 无 | 需要额外#include的头文件,如"foo/bar.h",配合导出宏使用 |
generator_plugin_label/generator_plugin_script | 无 | 自定义生成插件:前者给 GN label(默认 host toolchain),后者给脚本路径,二者互斥 |
generator_plugin_suffix[es] | 无 | 插件产出的文件后缀(扩展名前),支持单个或列表 |
generator_plugin_options | 无 | 传给插件的额外参数 |
deps | 无 | 追加的依赖,会在 protoc 运行前就绪 |
use_protobuf_full | 无 | 为true时生成的 C++ 代码依赖protobuf_full而非protobuf_lite |
import_protobuf_full | 无 | 允许.protoimport protobuf_full 中的.proto文件,但不引入其 C++ 依赖 |
import_dirs | 无 | 额外的 protoc import 目录列表,可重复;默认只有proto_in_dir |
defines/extra_configs | 无 | 编译生成代码的 source set 时追加的宏定义与配置 |
3.2 目录推导与输出文件命名
当省略proto_in_dir时,模板取第一个sources文件的目录作为基准,并逐一断言所有源文件都在同一目录,否则要求显式声明proto_in_dir。proto_out_dir默认取当前 BUILD.gn 的目录(rebase_path(".", "//")),若proto_in_dir非根则继续拼接。最终各语言输出路径的规则为:
- C++:
$root_gen_dir/+proto_out_dir+/foo.pb.{h,cc} - Python:
$root_out_dir/pyproto/+proto_out_dir+/foo_pb2.py - Go:
$root_gen_dir/go-proto-gen/src/+proto_out_dir+/foo.pb.go(gRPC 时为_grpc.pb.go)
另外,模板还通过 depfile 机制保证增量构建的正确性:生成 action 的depfile指向${target_gen_dir}/${target_name}.d,并将protoc_output_info文件作为--depfile-outputs传给 wrapper,使任何.proto或 import 文件的变更都能精确触发重新生成。
四、protoc 封装:protoc_wrapper.py 的职责
protoc_wrapper.py 是 protoc 与 GN 之间的"翻译层"。它并不只是简单转发命令行,而是承担了四项关键职责:
1. 统一生成器选项格式。FormatGeneratorOptions确保传给--cpp_out/--plugin_out的选项以冒号结尾(自动补:),符合 protoc 的生成器选项语法。
2. 拦截非法命名。VerifyProtoNames会检查所有.proto文件名,禁止出现连字符-(历史上会导致生成代码命名问题,参见仓库注释中引用的 crbug 记录)。
3. 生成并维护 depfile。ExtractImports递归解析.proto文件中的import/import public语句,结合--import-dir找到被引用文件,最终写出输出文件: 依赖文件格式的 depfile,供 Ninja 做增量判断。
4. 注入额外 include。当配置了--include时,WriteIncludes会在生成的.pb.h中找到// @@protoc_insertion_point(includes)标记行(protoc 生成代码预留的插入点),在其后写入#include "..."。这是cc_include/cc_generator_options(如导出宏)得以生效的机制,因此生成的头文件中若找不到该标记会直接报错。
此外,wrapper 还支持插件模式:通过--plugin protoc-gen-plugin=<路径>把自定义生成器注册给 protoc,并支持--plugin-depfile系列参数单独维护插件的依赖信息。
五、protobuf 库与 protoc 编译器的构建目标
BUILD.gn 定义了运行期与编译期所需的所有目标:
protobuf_lite(static_library):轻量运行时,编译message_lite.cc、wire_format_lite.cc、coded_stream.cc等约 30 个源文件,并对外暴露protobuf_config(含GOOGLE_PROTOBUF_NO_RTTI、HAVE_PTHREAD宏与 include 目录)与protobuf_warnings(一组针对 protobuf 源码的 clang 警告抑制参数,兼容至 v3.21.12 版本线)。默认的proto_library目标链接的就是它,体积更小;protobuf_full(static_library):完整运行时,追加descriptor.cc、reflection_ops.cc、text_format.cc、JSON 工具等重型实现,deps = [":protobuf_lite"]。凡.proto未声明option optimize_for = LITE_RUNTIME的 C++ 代码(包括 protoc 自身)都需要它;protoc_lib(static_library):protoc 编译器本体(command_line_interface.cc、各语言生成器实现),不含main(),拆成库是为了支持需要链接 libprotoc 的插件;protoc(executable):protoc_lib加上compiler/main.cc得到的可执行文件。注意它被包裹在if (current_toolchain == host_toolchain)条件中——protoc 永远以 host 工具链构建,因为代码生成动作必须运行在编译机上。
在 proto_library.gni 的生成 action 中,protoc_label同样被显式指定为"//third_party/protobuf:protoc($host_toolchain)",并且在 protoc_wrapper.py 的设计上刻意"绝不使用系统 protoc"(wrapper 注释与 BUILD.gn 的--protoc "./..."参数均印证了这一点),从而保证跨平台、跨工具链环境下生成代码行为的一致性与可复现性。
六、BUILD.gn 的自动生成:gen.py 与 BUILD.input.gn
值得注意的一个维护细节是,BUILD.gn 顶部明确写着 "THIS FILE IS GENERATED FROM BUILD.input.gn BY gen.py"。也就是说,protobuf 库的源文件清单并不靠人工维护:开发者只需编辑 BUILD.input.gn,把需要自动枚举的位置替换为PROTOBUF_LITE_PUBLIC、PROTOBUF_FULL_PUBLIC、PROTOC_LIB_SOURCES占位符,然后运行 gen.py。
gen.py 的逻辑很直接:借助git ls-files在 protobuf 源码仓库中枚举符合条件的文件(如src/google/protobuf/*.h,并排除compiler、testing、util等目录),再经sed转成带引号的 GN 列表,替换模板占位符,最后调用gn format格式化后写入 BUILD.gn。这保证了升级 protobuf 版本后,库的编译清单能自动与上游源码对齐,避免手写清单漏文件或引用已删除文件。
七、在 Flutter Engine 中接入这套支持的关键要点
综合上述实现,在类似 Flutter Engine 这样的 GN 工程中使用 protobuf 构建支持,需要注意以下几点:
- 三处路径必须对齐:
//.gn的secondary_source、proto_library.gni的实际存放目录、protobuf 源码的检出位置,任何一处错位都会导致 import 或 include 失败; - 默认用
protobuf_lite控制体积:只有确实需要完整反射、TextFormat 等能力的.proto(如 protoc 自身)才设置use_protobuf_full;仅需 import 上游.proto定义时用import_protobuf_full避免拖入完整 C++ 依赖; - 代码生成动作绑定 host toolchain:protoc 与自定义插件(
generator_plugin_label自动追加($host_toolchain))都以 host 工具链构建,输出目录通过rebase_path统一换算,跨平台(含 Windows 的.exe后缀)均有处理; - 利用 depfile 保证增量:wrapper 会递归收集
.proto的 import 依赖并生成 depfile,因此只要改动被 import 的.proto,相关目标就会被准确重跑; - 命名与后处理约束:
.proto文件名禁止连字符;若需为生成头文件注入额外 include(如导出宏),依赖的正是 wrapper 对// @@protoc_insertion_point(includes)插入点的改写逻辑。
对于希望深挖的读者,建议以 proto_library.gni 的模板主体为起点,对照 protoc_wrapper.py 的命令行参数逐一还原 protoc 的真实调用形态,再回到 BUILD.gn 查看protobuf_lite与protobuf_full的源码装配差异,即可完整掌握从.proto定义到链接进最终二进制的全链路。
- 跨平台
- 图形学
- 前端
【免费下载链接】engine
The Flutter engine
相关推荐
Apiato实战案例:如何构建一个完整的电商API系统
Apiato实战案例:如何构建一个完整的电商API系统 Apiato是一个基于Laravel构建的PHP框架,专门用于创建可扩展、可测试的API中心化应用程序。
后端软件架构Protocol Buffers(protobuf)构建与安装指南:Bazel/Bzlmod 集成、protoc 编译与多语言运行时部署实战
Protocol Buffers(protobuf)构建与安装指南:Bazel/Bzlmod 集成、protoc 编译与多语言运行时部署实战 Protocol
数据库文档数据库后端终极指南:深入理解protoc-gen-doc插件架构与Google Protocol Buffers文档生成原理
终极指南:深入理解protoc gen doc插件架构与Google Protocol Buffers文档生成原理 protoc gen doc是一款强大的Do
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考