protobuf Kotlin 代码生成器(protoc-gen-kotlin):Kotlin DSL 生成原理与 --kotlin_out 实战指南
在 Protocol Buffers 的多语言生态中,Kotlin 语言绑定由两部分组成:JVM 上的消息类实现,以及构建在其上的 Kotlin DSL。本文以 src/google/protobuf/compiler/kotlin/ 目录下的 Kotlin 代码生成器文档 为核心,结合插件源码与运行时库实现,完整讲解 protoc-gen-kotlin 的调用方式、参数选项、生成流程、产出文件结构,以及 Kotlin DSL 在 JVM 与其他平台上的适用边界。读完本文,你可以直接在自己的项目中正确使用 --kotlin_out 生成代码,并理解生成器在 protoc 内部的注册机制与实现链路。
一、Kotlin DSL 定位:构建在 Java/Kotlin 实现层之上的便捷层
根据 README 的表述,这个代码生成器实现的正是 Kotlin DSL:
The Kotlin DSL sits on top of another proto implementation (written in Java or Kotlin) and adds convenient support for building proto messages using DSL syntax.
也就是说,Kotlin 绑定并不是一个独立的 wire format 实现,而是叠加在 Java/Kotlin 消息实现层之上的一层 DSL 语法支持,让构建 proto 消息时可以使用更符合 Kotlin 惯用法的链式、类型安全写法。这一点在仓库中可以找到明确的实现对应:
- 运行时库位于 java/kotlin 目录,核心源文件包括 DslList.kt、DslMap.kt、DslProxy.kt、ByteStrings.kt 等;
- ProtoDslMarker.kt 定义了
@DslMarker标注的ProtoDslMarker注解,其注释明确写道 "Indicates an API that is part of a DSL to generate protocol buffer messages",且带有@OnlyForUseByGeneratedProtoCode标记——这说明 DSL API 专供生成代码使用,不应由手写代码直接依赖; - lite 变体则放在 java/kotlin-lite,通过
lite.awk从完整库裁剪出轻量版本。
因此理解 Kotlin 绑定的第一原则是:消息类的字段访问、序列化逻辑仍由底层实现提供,Kotlin DSL 只负责让"构造消息"这件事写得更像 Kotlin。
二、如何调用:--kotlin_out 与 --java_out 的组合
README 给出了两条关键调用规则:
- 通过向
protoc传递--kotlin_out来调用该代码生成器; - 当在 JVM 上使用 Kotlin 时,还必须同时传递
--java_out,用于生成承载消息类本身实现的 Java 代码; - 当在 JVM 之外的平台(如 Kotlin Native)上使用 Kotlin 时,目前尚不支持生成消息类,因此现阶段无法使用 Kotlin DSL。
第 2 点可以从源码中得到印证。protoc 主程序中同时注册了两个生成器,见 main.cc:
kotlin::KotlinGenerator kt_generator;
cli.RegisterGenerator("--kotlin_out", "--kotlin_opt", &kt_generator, "...");
注册时第二个参数 --kotlin_opt 即为生成器选项的传递通道(--kotlin_opt=option1,option2:...),这决定了下一节所有参数都是如何被解析的。
三、生成器支持的完整参数列表(源码级补充)
README 没有展开参数细节,但 generator.cc 中 KotlinGenerator::Generate 的参数解析逻辑给出了完整清单。生成器接收 --kotlin_opt 传入的逗号分隔选项,逐项处理:
| 参数 | 效果 | 源码行为 |
|---|---|---|
output_list_file |
输出一个确定位置的文本清单,逐行列出本次生成的全部 .kt 文件 |
写入 file_options.output_list_file,生成时在 generator.cc 中用 io::Printer 逐个打印文件路径 |
immutable |
生成不可变代码 | 无论传入与否,Kotlin 生成器都强制生成不可变代码(见下) |
mutable |
直接报错:"Mutable not supported by Kotlin generator" | 显式拒绝,生成失败 |
shared |
生成 shared 代码 | 与 immutable 类似,视为总是设置 |
lite |
生成 lite 运行时对应的代码 | 设置 file_options.enforce_lite = true |
annotate_code |
在生成文件末尾附加 // google.protobuf.GeneratedCodeInfo: <base64> 注释行,记录生成位置映射 |
使用 AnnotationProtoCollector 收集,Base64 后以 printer.Emit 输出(generator.cc) |
annotation_list_file |
指定注解清单输出文件 | 写入 file_options.annotation_list_file |
experimental_strip_nonfunctional_codegen |
剥离非功能性生成代码(实验性) | 设置 strip_nonfunctional_codegen = true |
no_jvm_dsl |
关闭 JVM DSL,改为对具体类型使用 DSL | 同时设置 jvm_dsl = false 与 dsl_use_concrete_types = true(generator.cc) |
| 其他任何参数 | 报错 "Unknown generator option: xxx" | 未识别选项直接导致生成失败 |
其中有两点值得特别注意:
- Kotlin 生成器只支持不可变实现。解析完用户参数后,代码无条件执行:
// We only support generation of immutable code so we do it.
file_options.generate_immutable_code = true;
file_options.generate_shared_code = true;
这意味着即使传入 mutable 之外的任何组合,产物也都是 immutable/shared 语义;而显式传 mutable 会被直接拒绝。
- 参数解析复用 Java 生成器的
google::protobuf::compiler::java::Options结构(using google::protobuf::compiler::java::Options;,generator.cc),Kotlin 生成器在选项模型上是搭在 Java 生成体系之上的,这也解释了为什么 JVM 上必须同时跑--java_out。
上述选项的解析行为有对应的自动化测试 annotation_test.cc,该测试将 KotlinGenerator 通过 cli.RegisterGenerator("--kotlin_out", ...) 注册后用 --kotlin_out=annotate_code,...:目录 这样的完整命令行驱动,验证 annotate_code 选项的真实效果。
四、生成流程与产出文件结构
Kotlin 生成器的实现分为三层:入口插件、文件级生成、消息级生成。
4.1 插件入口:protoc-gen-kotlin
plugin_main.cc 仅十几行,是标准 protoc 插件骨架:
int main(int argc, char* argv[]) {
google::protobuf::compiler::kotlin::KotlinGenerator generator;
return google::protobuf::compiler::PluginMain(argc, argv, &generator);
}
.bazel 构建规则 将其构建为可执行目标 protoc-gen-kotlin(cc_binary),依赖 :kotlin 库与 //src/google/protobuf/compiler:plugin。除了独立插件形式,KotlinGenerator 也作为库被 protoc 主程序静态内建(即 --kotlin_out 可直接用,无需外部插件在 PATH 中)。
4.2 文件级生成:一个文件对应"1 + N" 个 .kt 文件
KotlinGenerator::Generate 的主流程(generator.cc):
- 创建
FileGenerator,计算 Java 包名对应的目录package_dir与 Kotlin 文件名; - 主文件名格式为
<包目录>/<FileImmutableClassName>Kt.proto.kt(GetKotlinClassname()的返回值由 file.cc 拼接,取 Java 不可变文件类名加Kt后缀); - 调用
file_generator->Generate(&printer)输出文件头; - 调用
file_generator->GenerateSiblings(...)为每个顶层消息单独生成一个伴生文件<包目录>/<MessageName>Kt.kt(file.cc),文件内容调用消息生成器的GenerateMembers与GenerateTopLevelMembers; - 若设置了
output_list_file,把上述全部文件路径写入清单。
每个生成文件都以统一的文件头开始(file.cc):
// Generated by the protocol buffer compiler. DO NOT EDIT!
// NO CHECKED-IN PROTOBUF GENCODE
// source: <文件名>
@file:com.google.protobuf.Generated
// Generated files should ignore deprecation warnings
@file:Suppress("DEPRECATION")
package <java_package>;
注意包名会经过 java::EscapeKotlinKeywords 转义处理,避免 Java java_package 撞 Kotlin 保留字。
4.3 消息级生成:oneof 追踪与 lite/JVM DSL 标记
message.h 中的 MessageGenerator 持有若干关键状态:lite_、jvm_dsl_、dsl_use_concrete_types_ 布尔标记(分别对应 lite 与 no_jvm_dsl 选项的传播),用 absl::btree_map 按编号索引 oneof 描述符,并复用 Java 生成体系的 java::Context、java::FieldGeneratorMap<FieldGenerator> 做字段级代码生成。也就是说,Kotlin 生成器在字段层面是直接站在 Java 生成器基础设施之上组装 Kotlin 语法的——这正是 README 所说 "sits on top of another proto implementation" 在源码层面的具体体现。BUILD.bazel 中 kotlin_internal 库对 //src/google/protobuf/compiler/java、//src/google/protobuf/compiler/java:context 等目标的大面积依赖也印证了这一点。
五、能力边界:Editions 支持与平台限制
关于生成器能力边界,generator.h 给出了权威声明:
uint64_t KotlinGenerator::GetSupportedFeatures() const {
return CodeGenerator::Feature::FEATURE_PROTO3_OPTIONAL |
CodeGenerator::Feature::FEATURE_SUPPORTS_EDITIONS;
}
Edition GetMinimumEdition() const override { return Edition::EDITION_PROTO2; }
Edition GetMaximumEdition() const override { return Edition::EDITION_2026; }
- 支持
proto3_optional特性(即 proto3 显式optional字段); - 声明支持 Editions,且支持的 edition 区间覆盖 proto2 到 edition 2026;
GetFeatureExtensions返回GetExtensionReflection(pb::java),说明其 edition feature 默认值沿用了 Java 生成器的扩展定义。
平台限制方面,README 明确指出:Kotlin 绑定当前面向 JVM 场景设计;Kotlin Native 等其他平台"目前没有生成消息类的支持,因此现阶段无法使用 Kotlin DSL"。这一点在 file.h 的 TODO 注释中也有侧面体现——仓库正在计划向 Kotlin Native 与 Rust 生成器所用的 "Context" 模型演进,从源码结构看,未来面向非 JVM 平台的 Kotlin 生成是路线图上的方向,但在当前仓库状态下不可用。
六、运行时配套与测试证据
生成代码依赖的运行时 API 全部由 java/kotlin 提供,测试目录 java/kotlin/src/test 提供了可直接参考的验证样本:
- Proto2Test.kt 与 Proto3Test.kt:验证 DSL 在 proto2/proto3 语义下的行为;
- DslListTest.kt、DslMapTest.kt:验证 repeated/map 字段的 DSL 封装;
- ExtendableMessageExtensionsTest.kt:验证可扩展消息(extensions)的 DSL 支持,配套测试 proto 见 example_extensible_message.proto;
- evil_names_proto2.proto / evil_names_proto3.proto:专门覆盖"恶意"命名场景,验证生成器在特殊标识符下的健壮性。
构建层面,java/kotlin/BUILD.bazel 与 java/kotlin-lite/BUILD.bazel 均通过 //build_defs:kotlin_opts.bzl 加载的 protobuf_versioned_kt_jvm_library 规则组织 Kotlin JVM 库,Kotlin 运行时是 protobuf Java 发布体系的一部分。
七、快速上手:典型调用示例
综合以上信息,JVM 项目中一次典型的 Kotlin 代码生成调用为:
# 1) 生成 Java 消息类(Kotlin DSL 的承载层)
protoc --java_out=java-gen my.proto
# 2) 生成 Kotlin DSL 文件(可与 --java_out 放在同一次调用中)
protoc --kotlin_out=kotlin-gen my.proto
# 组合成一条命令更常见:
protoc --java_out=java-gen --kotlin_out=kotlin-gen my.proto
若需要构建系统集成,可追加选项:
protoc --kotlin_opt=output_list_file:generated/kotlin_files.txt \
--kotlin_out=kotlin-gen my.proto
生成的 kotlin_files.txt 中逐行列出 <包目录>/<File>OrdsKt.proto.kt 及每个顶层消息的 <Message>Kt.kt 伴生文件,方便 Bazel/Gradle 等构建系统做精确的文件依赖声明。
适用前提与限制小结(以当前仓库为准):
--kotlin_out与--java_out需配合使用(JVM 场景);- 只能生成不可变代码,
mutable选项会直接报错; - 不支持在 Kotlin Native 等非 JVM 平台生成消息类;
- 支持 proto2、proto3(含
proto3 optional)及 editions(proto2 至 edition 2026); - 所有未知
--kotlin_opt选项都会导致生成失败,参数名需严格对照上文表格。
八、小结
src/google/protobuf/compiler/kotlin/ 下的 Kotlin 生成器是 protobuf 语言绑定体系中"分层复用"的典型样例:它以 protoc 插件与内建生成器双重形态接入 protoc(--kotlin_out / --kotlin_opt),选项模型与字段生成复用 Java 生成器基础设施,产出"1 个文件级 + N 个消息级"的 .kt 文件,为 JVM 上的 Kotlin 项目提供 DSL 风格的消息构造能力。对使用者而言,牢记"JVM 上必须搭配 --java_out、只用 immutable 语义、非 JVM 暂不可用"这三条边界,即可正确落地 Kotlin DSL;对想了解实现的人,阅读路径建议从 plugin_main.cc → generator.cc → file.cc → message.h 依次展开,再对照 java/kotlin 运行时库与测试用例,即可完整掌握这条生成链路。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00