RunAnywhere IDL 开发指南:39 个 .proto 文件如何驱动 Swift / Kotlin / TypeScript / Dart / C++ / Python 六语言 SDK

原创2026-09-24 13:46:001,336 阅读
文章标签:AI模型推理服务推理引擎本地部署多模态

RunAnywhere IDL 开发指南:39 个 .proto 文件如何驱动 Swift / Kotlin / TypeScript / Dart / C++ / Python 六语言 SDK

本指南以 idl/ 目录的 schema 治理与代码生成体系为主题,完整讲解如何在 RunAnywhere SDK 单仓库中安全地新增或修改 .proto 文件、理解 rac_options.proto 自定义注解的生成语义,以及掌握 generate_all.sh 驱动下六语言代码生成与漂移门禁(SCHEMA_LOCK)的完整工作流。读完本文,你将具备直接参与该仓库 IDL 演进、排查生成代码缺失与 schema 漂移问题的实战能力。

IDL 在仓库中的定位:单一 schema 根

idl/ 是 RunAnywhere monorepo 的"单一 schema 根":目录下共 39 个 .proto 文件,统一使用 package runanywhere.v1,是仓库中所有跨平台类型的唯一事实来源,覆盖:

  • 模型 / 错误 / 事件 / 选项等基础类型(model_types.proto、errors.proto、sdk_events.proto 等);
  • LLM / VLM / STT / TTS / VAD 各模态的配置结构(如 stt_options.proto、llm_options.proto、vlm_options.proto);
  • SDK 事件目录(sdk_events.proto 全文件即"单一权威事件目录")。

idl/codegen/ 把这些 schema 渲染成 Swift、Kotlin(Square Wire)、TypeScript(ts-proto)、Dart、C++ 与 Python 六套绑定。仓库根目录的 AGENTS.md 规定了全仓库级约定与架构;idl/CLAUDE.md 只补充 idl 内部专属的 proto 编写约定与 codegen 细节。在动手之前,务必先读 docs/reference/generated-code-contract.md——它详细说明了每个生成脚本产出什么、落到哪个目录、固定工具链的引导脚本,以及 idl/VERSION / idl/SCHEMA_LOCK 漂移门禁的机制,本指南不再重复这些内容。

新增或修改 .proto 的标准流程

新建文件:八个 option 样板必须齐全

新建 .proto 文件时,从既有文件(例如 pipeline.proto)复制完整的 option 块样板。以 stt_options.proto 为例,一个合法文件的开头必须包含:

syntax = "proto3";

package runanywhere.v1;

import "errors.proto";
import "model_types.proto";
import "rac_options.proto";

option cc_enable_arenas          = true;
option java_multiple_files       = true;
option java_package              = "ai.runanywhere.proto.v1";
option java_outer_classname      = "SttOptionsProto";
option objc_class_prefix         = "RAV1";
option csharp_namespace          = "Runanywhere.V1";
option swift_prefix              = "RA";
option go_package                = "github.com/runanywhere/runanywhere-sdks/idl/v1;runanywherev1";

八个 option(cc_enable_arenas、java_multiple_files、java_package、java_outer_classname、objc_class_prefix、csharp_namespace、swift_prefix、go_package)被每个生成器依赖,且必须保持一致——缺失任何一个都会让对应语言静默采用默认(错误)的命名,例如 Kotlin 的包名会退回 java_package 之外的值、Swift 的类前缀会丢失 RA。这类错误不会在生成时报错,只会在编译期以"找不到类型"的形式暴露。

编辑已有 schema:先按语义升级 idl/VERSION

修改 message / enum 后,必须按语义更新 idl/VERSION(当前为 1.3.0)。idl/VERSION 是手工维护的、针对 schema 表面的 semver:

版本位 适用场景 示例
patch 仅注释 / 文档变更 修改字段的注释说明
minor 新增字段 / message / enum 值(wire 兼容) 给 message 追加一个字段
major wire 破坏性变更 删除或重编号字段、修改既有字段类型

随后运行 ./idl/codegen/generate_all.sh——它会顺带为你重写 idl/SCHEMA_LOCK。CI 中由 schema_lock.sh --check --require-bump <base-lock> 强制区分 patch / minor / major:schema 摘要变化而 idl/VERSION 未动,即视为未完成版本升级而失败。

字段号纪律:永不重编号、永不复用

对已上线 wire 或已持久化的 message,绝不允许重编号或复用字段号。仓库中有三类具体的硬约束:

  1. model_types.proto 的 ModelInfo 子图会序列化到磁盘上的 .rac-manifest.binpb,因此它的 reserved 块是"承重墙"而非装饰——删除一个字段时必须把它加入 reserved,绝不允许直接销毁该字段号(这正是 proto3 中"删除字段必须留 reserved 占位"这一规范在本仓库中的具体落地)。
  2. sdk_events.proto 对全文件声明了同一条规则:字段号、message 名、枚举值均 wire-stable,只有注释与分组可以改动。
  3. errors.proto 的数值必须与 core/include/rac/core/rac_error.h 完全一致——修改既有值属于 wire 破坏(必须升 major),而新增一个值则是安全的(升 minor)。

在 stt_options.proto 中可以看到这两条纪律的典型形态:已废弃但曾经上线的字段用 reserved 加 reserved "字段名" 双重占位(如 STTConfiguration 中 reserved 8, 9; reserved "vocabulary_list", "max_alternatives";),并在注释中说明"None were ever read by any backend"(从未被任何后端读取)作为废弃依据。

service 块:唯一的例外 tool_calling.proto

39 个文件中只有 tool_calling.proto 带有 service 块(ToolCalling),而且它是一个逻辑契约——只描述解析 / prompt 格式化 / 校验的形态,不是 wire 上的 RPC。整个仓库没有任何 gRPC 传输(已被进程内的 C 回调路径取代,见根目录 AGENTS.md),因此如果在其他文件中新增 service,生成的桩代码不会有任何调用方。

rac_options.proto:九个自定义注解与生成语义

idl/rac_options.proto 声明了 9 个 FieldOptions / EnumValueOptions 扩展,字段号落在 50000–99999 这个 Google 规定的"组织私有扩展"区间(50003、50008–50009 留空):

扩展 字段号 作用于 语义
rac_default 50001 字段 默认值字符串,按字段声明类型解析:"3" 对应 int32、"0.7" 对应 float、"true" 对应 bool、"auto" 对应 string、枚举常量名对应 enum
rac_required 50002 字段 validate() 拒绝该字段的 proto3 零值
rac_min 50004 字段 整数字段的包含性下界
rac_max 50005 字段 整数字段的包含性上界
rac_min_float 50006 字段 浮点字段的包含性下界
rac_max_float 50007 字段 浮点字段的包含性上界
rac_display_name 50010 枚举值 面向 UI 与文档的可读标签
rac_analytics_key 50011 枚举值 稳定分析键,枚举重命名不会破坏历史分析数据
rac_wire_string 50012 枚举值 与手工字符串协议互操作时的线上字符串覆盖,codegen 优先使用它而非 proto 常量名

这些注解不直接由 protoc 消费。codegen/generate_{swift,kotlin,dart,ts}_convenience.py 四个后处理脚本在描述集(descriptor set)上做二次加工,为每种语言产出对应的便捷访问器:

注解 生成的访问器
rac_wire_string(枚举值,正向) wireString
rac_wire_string(枚举值,反向) from(wireString:)(Swift) / fromWireString(Kotlin、Dart、TS)
rac_display_name displayName
rac_analytics_key analyticsKey
rac_default(字段) defaults() 工厂
rac_required / rac_min / rac_max / rac_min_float / rac_max_float validate()

一个来自 stt_options.proto 的完整注解示例——采样率默认 16000、合法区间 8000–48000:

int32 sample_rate = 3 [
    (runanywhere.v1.rac_default) = "16000",
    (runanywhere.v1.rac_min) = 8000,
    (runanywhere.v1.rac_max) = 48000
];

它在 Kotlin(Wire)中生成的形态(完整设计见 idl/codegen/CONVENIENCE_CODEGEN_DESIGN.md):

public fun STTConfiguration.Companion.defaults(): STTConfiguration =
    STTConfiguration(
        language = STTLanguage.STT_LANGUAGE_EN,
        sample_rate = 16000,
        enable_punctuation = true,
        enable_word_timestamps = true,
    )

public fun STTConfiguration.validate() {
    if (sample_rate < 8000 || sample_rate > 48000) {
        throw SDKException.validationFailed(
            "sample_rate must be in 8000...48000 (got $sample_rate)"
        )
    }
}

注意三个关键约束:

  1. 生成的 validate() 不会被自动调用——调用方必须显式调用;同时 core/ 的 commons 层会独立执行自己的边界校验,因此不要以为注解覆盖就是唯一的校验层。
  2. 各语言默认值字面量转换规则不同:Kotlin 的 int64 输出 16000L,Dart 因 dart-protobuf 用 package:fixnum 表示 int64 而输出 Int64(16000),TypeScript 的 ts-proto 用 long 库而输出 Long.fromNumber(16000)。
  3. 这些生成器共享 idl/codegen/_convenience_common.py 中的描述集解析、wire 格式读取与命名工具,避免四个生成器各自实现而再次产生 drift。

buf.yaml:手工运行 lint / breaking 检查

idl/buf.yaml 配置了 buf 的两类检查:

  • buf lint:使用 STANDARD 规则集,但排除 PACKAGE_VERSION_SUFFIX 和 PACKAGE_DIRECTORY_MATCH——本仓库不对包做版本后缀,也不把 runanywhere.v1 包镜像进匹配的目录树;
  • buf breaking:使用 WIRE 类别,排除 EXTENSION_NO_DELETE 和 FIELD_SAME_DEFAULT。

buf 未接入任何 CI 工作流——本仓库的 schema 兼容性强制手段是 SCHEMA_LOCK 加漂移检查,而不是 buf。当你拿不准一次编辑是否 wire 破坏时,手工运行:

buf lint idl
buf breaking idl --against '.git#branch=main,subdir=idl'

codegen/ 流水线:generate_all.sh 之外还有什么

generate_all.sh 是总入口,默认运行六个语言(swift kotlin dart ts cpp python),支持两个实用参数:

  • --skip-dart:本地没有 Dart 3 时跳过 Dart 生成(CI 会用固定工具链重新生成);
  • --only LIST:只跑 swift,kotlin,dart,ts,cpp,python 的子集——因为绑定代码不再提交,CI 编译某个 SDK 前必须先为它生成代码,而一个 Linux Web job 没必要为此安装 swift-protobuf 和 wire-compiler。

脚本内部有几个值得注意的实现细节:固定 LC_ALL=C / TZ=UTC / PYTHONHASHSEED=0 等环境变量保证"开发机与 CI 跑出相同字节";用 mkdir 原子锁(.codegen.lock.d)串行化 CMake configure、Gradle preBuild 与开发者手动运行三种并发来源,等待 900 秒后主动打破死锁而非无限阻塞;SCHEMA_LOCK 只在全量运行(六语言齐全)时刷新,--only 部分运行只打印 "not updated"——保证锁的语义是"所有绑定都与这些 schema 一致"。

除 generate_all.sh 覆盖的内容外,codegen/ 里还有四个专项设施:

便捷访问器生成器与回归测试

四个 generate_{swift,kotlin,dart,ts}_convenience.py 是 rac_options.proto 注解的消费方(见上文)。它们的回归测试套件是 idl/codegen/tests/test_convenience_generators.py:以夹具 proto(idl/codegen/tests/fixtures/test_options.proto)覆盖每个 rac_* 注解 × 标量类型的组合,并把每个生成器的输出与 golden/ 目录下的提交版金样文件做 diff。本地运行:

python3 idl/codegen/tests/test_convenience_generators.py

修改生成器或夹具后加 --update-golden 更新金样。该测试已被接入 idl-drift-check.yml,作为漂移检查之前的一步;退出码与"Python 环境不可用时的降级流程"见 idl/codegen/tests/README.md。

swift-modality-abi.yaml:模态 ABI 清单

idl/codegen/swift-modality-abi.yaml 是一个清单(manifest)而非 proto,驱动 generate_swift_modality_abi.py 渲染 CppBridge+ModalityProtoABI.swift:一张把每个 rac_* C 符号映射到其请求 / 响应 proto 类型与调用形态(invoke、stream、getWithContext 等)的 dlsym 表。当某个模态新增或变更 C 入口点时,编辑 YAML 而非生成的 Swift;文件头部注释详细记录了每种 kind 值的 wire 形态。

C++ defaults 头文件与默认值池

  • generate_cpp_defaults.py:产出随包发布的公共头文件 core/include/rac/rac_defaults_generated.h(rac_default 注解 → C 宏)。commons 的默认结构体(RAC_LLM_OPTIONS_DEFAULT 等)就是由它组成的,也就是说真正跑推理的 C 层读取的默认值与各 SDK 生成器针对的是同一套注解。
  • generate_defaults_pool.py:一次遍历为全部七种语言目标输出 RADefaultsPool 默认值常量(sdk_defaults.proto 被排除在四个 message 生成器之外,其值以普通常量而非"没人会放到线上"的生成 message 类型到达各 SDK)。它跨语言运行、与 --only 子集无关,输出字节始终一致。

templates/ts_async_iterable.njk:共享 TS 流包装

idl/codegen/templates/ts_async_iterable.njk 是 generate_streams.sh 使用的 Nunjucks 模板,为每个 server-streaming RPC 形态渲染共享的 TypeScript AsyncIterable 流包装器,同时被 React Native 与 Web 两个 SDK 经 @runanywhere/proto-ts 消费(bindings/proto-ts/src/streams/ 下可见产物,如 llm_service_stream.ts、voice_agent_service_stream.ts)。历史上 RN / Web 各有一份字节相同的流脚本且会互相覆盖,现在统一为单一共享脚本渲染,杜绝单边篡改。

漂移检测与 CI 门禁:为什么 bindings 不提交

本仓库的绑定代码全部 gitignore 不提交,fresh clone 在 codegen 运行前没有任何 C++ / Kotlin / Swift / TypeScript / Dart / RN / Python 绑定。./scripts/setup/setup.sh 会先运行 codegen,./run codegen 按需运行。由此带来一个关键问题:旧式"重新生成后 git status 必须为空"的漂移检查对未跟踪文件永远失效(重新生成未跟踪文件总是干净树)。解决方案就是 idl/SCHEMA_LOCK:

  • generate_all.sh 每次运行都会刷新它,记录每个 idl/*.proto 的 basename + 内容 sha256 的再摘要、proto 文件计数、idl/VERSION 与 core/VERSIONS 中固定的 protoc 版本;
  • 由于它是被跟踪的唯一 codegen 产物,编辑 schema 而不重跑 codegen 会让锁过期,CI 失败;修改 schema 却不升 idl/VERSION 同样失败;
  • idl/codegen/generated_trees.txt 是这张表的机器可读版本,并额外列出那些"位于被忽略树内部、但必须保持被跟踪"的 8 个手写文件(如 bindings/proto-ts/src/index.ts、bindings/proto-ts/src/convenience/_errors.ts);check_generated_trees.sh 负责双向校验——生成文件被跟踪要失败,手写文件停止被跟踪也要失败。

日常查看与验证命令:

./idl/codegen/schema_lock.sh --print    # 当前 checkout 是哪个 IDL?形如 "idl 1.3.0 (39 protos, sha256 ...)"
./idl/codegen/ci-drift-check.sh         # 与 CI 完全一致的整套门禁

CI 的 idl-drift-check.yml 采用"先生成、后验证"(generate-then-verify)而非"重新生成并 diff"——后者对忽略文件永远无法失败。此外,工具链本身也是"下载而非假定"的:bootstrap_protoc.sh / bootstrap_wire.sh / bootstrap_pyproto.sh 分别按 core/VERSIONS 中固定的 PROTOC_VERSION / WIRE_VERSION / PYTHON_PROTOBUF_VERSION 解析工具,并以 protoc.sha256 / wire.sha256 做校验和验证,缓存于 ~/.cache/runanywhere/;RAC_PROTOC / RAC_WIRE_COMPILER / RAC_PYTHON 可覆盖,RAC_PROTOC_NO_DOWNLOAD=1 等变量让离线主机大声失败而不是偷偷联网。

实践要点速查

  1. 新建 .proto:先复制样板文件的八个 option,缺一个就是静默错误;记得 import "rac_options.proto"。
  2. 改字段:先想清楚是 patch / minor / major,改完升 idl/VERSION,再跑 ./idl/codegen/generate_all.sh 刷新 idl/SCHEMA_LOCK。
  3. 删字段:加入 reserved(字段号 + 名字双占位),尤其 ModelInfo 子图(.rac-manifest.binpb)与整个 sdk_events.proto。
  4. 加校验/默认值:优先用 rac_* 注解,让四语言便捷访问器生成器替你产出 defaults() / validate() / wireString,而不是手写各 SDK 的扩展。
  5. 拿不准是否 wire 破坏:手工跑 buf lint idl 与 buf breaking idl --against '.git#branch=main,subdir=idl'(注意 buf 不参与 CI,最终裁决者是 SCHEMA_LOCK)。
  6. 改生成器:跑 idl/codegen/tests/test_convenience_generators.py(必要时 --update-golden),再确认 ci-drift-check.sh 通过。

从 schema 编辑到六语言绑定再到底层 C 默认值,idl/ 的这套"单一事实来源 + 固定工具链 + 锁文件门禁"设计,是整个 RunAnywhere SDK 跨平台一致性的地基。

登录后查看全文
runanywhere-sdks