首页
/ 深入 protobuf 仓库的 Ruby 绑定:google-protobuf 双后端架构与源码构建完整实战

深入 protobuf 仓库的 Ruby 绑定:google-protobuf 双后端架构与源码构建完整实战

2026-09-06 12:17:16作者:幸俭卉

本文基于 protobuf 仓库中的 ruby/ 目录文档与源码,系统讲解 Google Protocol Buffers 的 Ruby 扩展:如何安装和使用 google-protobuf gem、如何用 protoc --ruby_out.proto 文件生成 Ruby 代码、仓库内 FFI 与平台原生双实现后端的切换机制,以及如何从源码构建该 gem 并运行完整的测试套件。读完后,你既能把 Ruby 绑定接入实际项目,也能理解其底层构建管线(代码生成、C 扩展编译、FFI 库编译)的每一个环节。

1. 这个目录是什么:Ruby 扩展的总体定位

ruby/README.md 开宗明义:该目录包含在 Ruby 中实现 Protocol Buffers 功能的扩展(extension)。其工作方式分两层:

  • 生成代码层:Ruby 扩展使用 protoc 生成的 Ruby 代码,这些代码通过一套 Ruby DSL(领域特定语言)来定义 message 与 enum 类型。README 明确指出:你 可以 直接用这套 DSL 手写类型定义,但官方推荐使用 protoc 的 Ruby 代码生成功能,配合 .proto 文件使用;
  • 安装关系ruby/ 目录内的构建流程只负责安装 Ruby 扩展本身。要获得从 .proto 生成 Ruby 代码的能力,还必须另外安装 protoc——仓库给出了直接可行的构建方式:
$ bazel build //:protoc

仓库当前的 gem 规格 ruby/google-protobuf.gemspec 中声明版本为 4.37.0,要求 Ruby >= 3.2s.required_ruby_version = '>= 3.2'),运行期依赖 rake ~> 13.3。目录整体结构如下(摘自仓库文件树):

ruby/
├── ext/google/protobuf_c/   # C 扩展源码(含 ruby-upb.c/h、glue.c 等)
├── lib/google/
│   ├── protobuf.rb          # 入口:实现选择逻辑
│   ├── protobuf_ffi.rb      # FFI 后端入口
│   ├── protobuf_native.rb   # 平台原生后端入口
│   └── protobuf/ffi/        # FFI 后端的 Ruby 实现(descriptor、message、map...)
├── src/main/java/           # JRuby 平台使用的 Java 服务桥接代码
├── tests/                   # test-unit 测试 + 测试用 .proto 文件
├── Rakefile                 # 构建/测试/gem 打包管线
├── Gemfile
└── defs.bzl                 # Bazel 规则封装

从源码结构看,lib/google/protobuf/ffi/ 下按 descriptor、field、message、map、repeated_field 等维度组织了 FFI 后端的完整 Ruby 对象模型,与 ext/google/protobuf_c/ 下的 C 源文件(convert.cdefs.cmessage.cshared_message.cruby-upb.c 等)构成"Ruby 层 + C 层"的两层实现。

2. 从 Gem 安装

这是绝大多数用户的路径。README 给出了两种方式,先确认你需要的 Protocol Buffers 版本,然后:

方式一:写入 Gemfile(Bundler 项目推荐)

gem 'google-protobuf'

方式二:直接安装预打包 gem

$ gem install [--prerelease] google-protobuf

--prerelease 用于安装预发布版本(对应 README 第 8 节描述的 .pre 版本号规则)。

2.1 是否还需要 protoc?

README 对此的结论是"看情况":

  • 如果你的 message 类型描述直接写在 Ruby DSL 中,就不需要 protoc;
  • 如果希望.proto 文件生成 Ruby DSL,就需要安装 Protocol Buffers 本体。README 注明:最新 release 附带的 protoc 支持 --ruby_out 选项来生成 Ruby 代码。

生成的产物是 *_pb.rb 文件。这一点可以在构建脚本中得到印证:ruby/Rakefile 中为每个 well-known proto 定义了生成任务,输出基名由 .proto 替换为 _pb.rb

output_basename = File.basename(proto_file).sub(/\.proto$/, "_pb.rb")
# ...
sh "#{protoc_command} -I../src --ruby_out=#{tmp_protoc_out} #{input_file}"

Rakefile 中还体现了 protoc 的解析优先级,可作为本地开发时的参考:优先使用环境变量 PROTOC 指定的路径;否则探测仓库根下 Bazel 构建产物 ../bazel-bin/protoc(可用时即说明你执行过 bazel build //:protoc);两者都没有则回退到 PATH 中的 protoc

2.2 完整使用示例

README 给出的最小可用示例如下(完整继承,可直接复制到 Ruby 项目中运行):

require 'google/protobuf'

# generated from my_proto_types.proto with protoc:
#  $ protoc --ruby_out=. my_proto_types.proto
require 'my_proto_types'

mymessage = MyTestMessage.new(:field1 => 42, :field2 => ["a", "b", "c"])
mymessage.field1 = 43
mymessage.field2.push("d")
mymessage.field3 = SubMessage.new(:foo => 100)

encoded_data = MyTestMessage.encode(mymessage)
decoded = MyTestMessage.decode(encoded_data)
assert_equal mymessage, decoded
puts "JSON:"
puts MyTestMessage.encode_json(mymessage)

示例覆盖了 Ruby 绑定的核心用法面:

API 说明
MyTestMessage.new(hash) 用符号名 hash 初始化字段(标量、repeated、嵌套 message 均可)
msg.field1 = 43 标量字段的读写
msg.field2.push("d") repeated 字段以类数组方式追加元素
msg.field3 = SubMessage.new(:foo => 100) 嵌套 message 字段的赋值
MyTestMessage.encode(msg) / decode(data) 二进制编解码
MyTestMessage.encode_json(msg) 编码为 JSON 字符串

这些模块级 API 在入口文件 ruby/lib/google/protobuf.rb 中有对应实现:Google::Protobuf.encode/decode/encode_json/decode_json 统一委托给具体 message 类的 to_proto/decode/to_json 方法,生成的 _pb.rb 类同时支持类方法调用与 msg.to_proto 实例调用两种风格。

3. 双后端架构:NATIVE 与 FFI 实现的选择机制

这是 ruby/README.md 中信息量最大、也最值得源码级验证的部分。README 说明:

Protocol Buffers 有一个新的实验性后端,使用 ffi gem 在多种 Ruby 解释器上提供基于 UPB 的统一 C 实现。目前 FFI 实现是 opt-in(需显式开启) 的。只要满足以下任一条件,就会回退到传统平台原生实现(CRuby 上的 MRI 原生扩展、JRuby 上基于 Java 的实现):

  1. ffiffi-compiler 两个 gem 未安装;
  2. 环境变量 PROTOCOL_BUFFERS_RUBY_IMPLEMENTATION 的值不是 FFI(大小写不敏感);
  3. FFI 在运行时无法加载原生库。

这段描述可以在入口文件 ruby/lib/google/protobuf.rb 中找到逐条对应的源码实现(第 20–59 行):

PREFER_FFI = case ENV['PROTOCOL_BUFFERS_RUBY_IMPLEMENTATION']
             when nil, "", /^native$/i
               false
             when /^ffi$/i
               true
             else
               warn "Unexpected value `#{...}` for environment variable ..."
               false
             end

IMPLEMENTATION = if PREFER_FFI
  begin
    require 'google/protobuf_ffi'
    :FFI
  rescue LoadError
    warn "Caught exception `#{$!.message}` while loading FFI implementation ..."
    warn "Falling back to native implementation."
    require 'google/protobuf_native'
    :NATIVE
  end
else
  require 'google/protobuf_native'
  :NATIVE
end

从源码可以读出 README 三条回退规则的落地方式:

  • 规则 2(环境变量)PREFER_FFIcase 分支严格解析取值——nil/空串/NATIVE 映射为 false,仅 FFI(不区分大小写)映射为 true,其他任何值都会打印警告并回退为 false
  • 规则 1 与规则 3(gem 缺失 / 原生库加载失败):统一收敛在 require 'google/protobuf_ffi'rescue LoadError 分支中——ruby/lib/google/protobuf_ffi.rb 第一行就是 require 'ffi-compiler/loader',ffi/ffi-compiler gem 不存在或原生库加载失败都会以 LoadError 形式触发兜底的 require 'google/protobuf_native',并打印回退警告;
  • 最终状态可查询Google::Protobuf::IMPLEMENTATION 会保存 :FFI:NATIVE 符号,运行时可据此判断当前实际生效的后端。

两条后端的差异在各自的入口文件中一目了然:

  • ruby/lib/google/protobuf_native.rb:JRuby(RUBY_PLATFORM == "java")走 protobuf_java 桥接;CRuby 则按主版本号 require "google/#{RUBY_VERSION.sub(/\.\d+$/, '')}/protobuf_c"(找不到时回退到不带版本目录的 protobuf_c)——这就是 README 所说"CRuby 基于 MRI 原生扩展"的实现;
  • ruby/lib/google/protobuf_ffi.rb:加载 google/protobuf/ffi/ 下完整的 Ruby 对象模型(descriptor 池、message、map、repeated_field 等),底层通过 FFI 绑定 UPB 的 C 实现。

仓库自带测试 ruby/tests/implementation.rb 正是针对这套选择逻辑的验证用例:它断言 IMPLEMENTATIONPREFER_FFI 一致,并按环境变量取值分别验证 :FFI / :NATIVE 是否被正确激活(不满足前置条件时 omit 跳过)。该测试在 Bazel 侧注册为 //ruby/tests:implementation(见 ruby/tests/BUILD.bazel)。

依赖声明也与 README 一致:ruby/google-protobuf.gemspecffiffi-compiler(均为 ~>1)在 CRuby 平台是 development 依赖(可选),而 ruby/Gemfile 中则对 JRuby 平台将其提升为必需依赖(platforms: %i[jruby])——因为 FFI 是 JRuby 上统一实现的关键。

4. 从源码构建 Gem

4.1 前置依赖

README 列出的构建要求:

构建 CRuby 扩展:

  • Rake
  • Bundler
  • Ruby 开发头文件(development headers)
  • C 编译器

构建 JRuby 扩展:

  • Maven
  • 最新版本的 protobuf Java 库(README 指向 ../java/README.md,即仓库根目录下的 java/README.md
  • 通过 rbenv 或 RVM 安装 JRuby

4.2 标准构建步骤

先用 rbenv 或 RVM 切换到目标 Ruby 平台,然后:

# 安装构建工具
$ gem install bundler
$ bundle

# 构建并打包 gem
$ rake
$ rake clobber_package gem
$ gem install `ls pkg/google-protobuf-*.gem`

这里的 rake(即 rake build)背后是一条完整的构建管线。从 ruby/Rakefiletask :build => [:clean, :genproto, :copy_third_party, :compile, :generate_stubs, :"ffi-protobuf:default"] 可以看到 rake 实际依次执行了 6 个环节,这解释了为什么直接跑 rake 需要 C 编译器与 protoc:

  1. :clean:清除上次生成的 *_pb.rbpkg/tmp/ext/google/protobuf_c/ 下的平台构建目录;
  2. :genproto:调用 protoc 生成全部 well-known types(any.protodescriptor.prototimestamp.protostruct.proto 等 12 个)与 15 个测试 proto 的 Ruby 代码。注意 Rakefile 中的布局规则:google/protobuf 子目录(如 compiler/plugin.proto)生成的 _pb.rb 统一平铺到 lib/google/protobuf/ 下;
  3. :copy_third_party:把 third_party/utf8_range 下的 utf8_range.h/.c、SSE/NEON 两个 .inc 与 LICENSE 拷贝进 ext/google/protobuf_c/third_party/utf8_range/——UTF-8 校验逻辑需要这份内嵌的 C 库;
  4. :compileRake::ExtensionTask 编译 C 扩展(扩展目录 ext/google/protobuf_c,产物落位 lib/google),并声明支持交叉编译到 x86-mingw32x64-mingw-ucrtx86_64-linuxx86-linuxx86_64-darwinarm64-darwin 等平台;非 macOS 平台会置 no_native = true(不在本机编译原生部分,靠交叉编译);
  5. :generate_stubs:执行 ruby/generate_stubs.rb 生成 lib/stubs/ 下的存根文件;
  6. :ffi-protobuf:default:编译 FFI 后端所需的两份原生库。细节在 ruby/lib/google/tasks/ffi.rake:先用 FFI::Compiler::CompileTask 单独编译 ruby-upb(定义 UPB_BUILD_API,在 darwin/linux 上加 -fvisibility=hidden 控制符号可见性),再编译 protobuf_c_ffi;两者共用 -std=gnu99 -O3 -DNDEBUG 编译选项。若未安装 ffi-compiler,该环节会优雅降级为警告(Skipping build of FFI; gem install ffi-compiler to enable.)并跳过——与 README 的回退规则 1 相互呼应。

4.3 调试构建(gdb)

如果你打算用 gdb 调试 protobuf_c 的 Ruby 绑定,README 给出了带调试符号的构建方式——在构建原生扩展时设置 PROTOBUF_CONFIG 环境变量:

$ PROTOBUF_CONFIG=dbg rake

4.4 运行测试

用 Rake 运行全部 specs:

$ rake test

Rakefile 中 Rake::TestTask 会收集 tests/*.rb(排除 gc_test.rbcommon_tests.rb——前者必须独立运行以确保生成文件未被其他测试提前引入,后者是公共测试助手,经 ruby/tests/BUILD.bazel 可见它在 Bazel 侧作为 rb_library 被各测试引用)。

用 FFI 后端运行 specs:

$ PROTOCOL_BUFFERS_RUBY_IMPLEMENTATION=FFI rake test

4.5 使用 Bazel 构建与测试

README 提供了一条替代路径:从仓库根目录(注意不是 ruby 目录)执行:

$ bazel test //ruby/tests/...

针对 FFI 实现的测试:

$ bazel test //ruby/tests/... //ruby:ffi_enabled --test_env=PROTOCOL_BUFFERS_RUBY_IMPLEMENTATION=FFI

这里 --test_env 正是向测试进程注入第 3 节所述的环境变量开关,//ruby:ffi_enabled 配置项控制 Bazel 侧构建出 FFI 相关产物。测试目标本身在 ruby/tests/BUILD.bazel 中以 rb_test 规则逐一声明,覆盖面相当完整:implementation(后端选择逻辑)、basic / basic_proto2encode_decode_testgc_testgenerated_code_testrepeated_field_testutf8service_testwell_known_types_teststressoom_testmemory_test 等;测试 proto 由 ruby/defs.bzl 中的 internal_ruby_proto_library(对 internal_ruby_proto_library + rb_library 的封装规则)统一生成。Rake 与 Bazel 两套入口共享同一批测试文件,只是 proto 生成方式不同(protoc 命令行 vs Bazel 规则)。

4.6 关于内置 UPB 库的版本说明

README 特别注明:该 gem 将 UPB 的解析与序列化库以**单文件 amalgamation( amalgamated 合并源码)**形式打包,当前与 UPB 仓库 git commit 535bc2fe2f2b467f59347ffc9449e11e47791257 保持同步。这一点对排障有意义:当 FFI 后端出现序列化层面的问题且与本仓库 C 源码无关时,可以从这个 commit 定位到具体的 UPB 实现状态。本仓库内 UPB 的完整源码位于 upb/ 目录(含 wire/message/mini_descriptor/reflection/ 等子模块),而 Ruby 绑定侧对应的编译入口是 ruby/ext/google/protobuf_c/ruby-upb.c

5. 版本号规则(Version Number Scheme)

README 的最后一节完整定义了 gem 的版本号方案,它是 Protocol Buffers 总版本号与 Ruby 特有规则的混合体。根本约束是:Gem 不允许同版本号重复上传,因此需要在版本号中附加"上传序号"(upload version),并对 alpha、pre 等字母标签做特殊格式化(避免使用连字符)。规则逐条拆解:

第一步——确定前缀:取 Protocol Buffers 的版本号并把连字符换成点号。

  • 总版本 3.0.0-alpha-2 → 前缀 3.0.0.alpha.2
  • 正式发布 3.0.0 → 前缀就是 3.0.0

第二步——追加上传序号

  • 首次上传:3.0.0.alpha.2.03.0.0.0
  • 若需要修复问题重新上传同一个版本,序号递增:3.0.0.alpha.2.13.0.0.1

第三步——预发布追加 pre 标签:若处于预发布阶段,在末尾追加 .pre3.0.0.alpha.3.0.pre。标签刻意放在末尾,这样按版本号排序时,预发布构建会恰好落在"上一正式版本"与"当前正式版本"之间。

README 总结:整套规则就是为了配合 RubyGems 的 Gem::Version 排序语义,保证 release 版本号能按真实发布顺序正确排序。

从 gemspec 侧可以看到这条规则的现实产物:ruby/google-protobuf.gemspec 当前 s.version = "4.37.0"(一个不带上传序号的"干净"发布版本,即该总版本的首次上传),且文件内还有一行注释揭示了 tag 转换约定——把 X.Y.Z.rc.N 形式的版本号映射为 git tag vX.Y.Z-rcNgit_tag = "v#{s.version.to_s.sub('.rc.', '-rc')}")。

6. 小结:从文档到源码的对应关系

README 主题 仓库中的源码证据
protoc --ruby_out 生成 Ruby 代码 ruby/Rakefile:genproto 任务
gem 安装与版本要求 ruby/google-protobuf.gemspec(4.37.0,Ruby ≥ 3.2)
FFI / NATIVE 双后端与回退三规则 ruby/lib/google/protobuf.rb 第 20–59 行、ruby/lib/google/protobuf_ffi.rb
后端选择逻辑的测试验证 ruby/tests/implementation.rb
rake 构建管线(genproto→copy_third_party→compile→stubs→ffi) ruby/Rakefileruby/lib/google/tasks/ffi.rake
Bazel 测试入口 ruby/tests/BUILD.bazelruby/defs.bzl
UPB 统一 C 实现 upb/ 目录、ruby/ext/google/protobuf_c/ruby-upb.c

实际使用建议:生产环境直接 gem 'google-protobuf' 安装并配合 protoc --ruby_out 生成代码即可;需要启用实验性 FFI 后端时设置 PROTOCOL_BUFFERS_RUBY_IMPLEMENTATION=FFI(并确保已安装 ffiffi-compiler gem);从源码构建时按第 4 节的 bundle && rake && rake clobber_package gem 流程操作,或在仓库根目录用 bazel test //ruby/tests/... 走 Bazel 路线。

登录后查看全文
热门项目推荐
相关项目推荐