mruby 的 mrbgems 扩展机制完整指南:从 Gem 接入、依赖管理到 C/Ruby 混合扩展
【免费下载链接】h2oH2O - the optimized HTTP/1, HTTP/2, HTTP/3 server项目地址: https://gitcode.com/gh_mirrors/h2/h2o
mrbgems 是 mruby 官方提供的库管理器,用于以标准化方式将 C 扩展与 Ruby 扩展集成进 mruby 构建。本文以 mrbgems.md 为主体,结合本仓库deps/mruby及其 mrbgems 目录中的真实代码,系统讲解如何在构建配置中接入第三方 Gem、如何用 GemBox 批量组合扩展、如何编写 C 扩展与 Ruby 扩展、如何管理依赖与冲突,以及如何构建可执行的二进制 Gem,帮助读者完整掌握 mruby 扩展开发与定制裁剪能力。
mrbgems 是什么
mrbgems 是 mruby 的库管理器,目标是把 C 语言扩展和 Ruby 语言扩展以统一、标准化的方式集成进 mruby。按惯例,每个 mrbgem 的名字都以mruby-前缀开头,例如提供Time类功能的 Gem 就命名为mruby-time。
本仓库的 deps/mruby/mrbgems 目录下就内置了大量核心 Gem,如mruby-time、mruby-io、mruby-socket、mruby-compiler,以及二进制类 Gemmruby-bin-mruby、mruby-bin-mirb、mruby-bin-mrbc、mruby-bin-strip等;同时deps/下还捆绑了 H2O 实际使用的外部 mrbgem(如 mruby-digest、mruby-env、mruby-input-stream、mruby-onig-regexp 等)。这些 Gem 均遵循统一的mrbgem.rake规范文件组织,是理解 mrbgems 机制的最佳范例。
在构建配置中启用与添加 mrbgem
mrbgems 需要显式激活:在构建配置文件中调用conf.gem即可把某个 Gem 加入构建。
通过本地路径添加
conf.gem '/path/to/your/gem/dir'也可以使用相对路径:
conf.gem 'examples/mrbgems/ruby_extension_example'相对路径的解析规则取决于构建配置文件的位置:
- 若构建配置文件位于
build_config目录,则相对路径以MRUBY_ROOT为基准; - 否则,相对路径以构建配置文件所在目录为基准。
在 H2O 的 mruby 中,构建配置统一位于 deps/mruby/build_config 目录,其中 default.rb 是被 build_config.rb 推荐的默认配置入口,其注释中给出了多种conf.gem用法示例:
# conf.gem 'examples/mrbgems/ruby_extension_example' # conf.gem 'examples/mrbgems/c_extension_example' do |g| # g.cc.flags << '-g' # append cflags in this gem # end # conf.gem :core => 'mruby-eval' # conf.gem :mgem => 'mruby-onig-regexp' # conf.gem :github => 'mattn/mruby-onig-regexp' # conf.gem :git => 'git@github.com:mattn/mruby-onig-regexp.git', :branch => 'master', :options => '-v'可以看到,conf.gem既支持路径字符串,也支持:core、:mgem、:github、:git等哈希形式的取源方式,还支持传入代码块对单个 Gem 的编译参数做局部定制。
从 Git 仓库远程取 Gem
conf.gem :git => 'https://github.com/masuidrive/mrbgems-example.git', :branch => 'master' conf.gem :github => 'masuidrive/mrbgems-example', :branch => 'master' conf.gem :bitbucket => 'mruby/mrbgems-example', :branch => 'master'注意::bitbucket选项只支持 git 协议,当前版本不支持 hg。
如果 Gem 位于仓库的子目录,可用:path指定子目录:
conf.gem github: 'mruby/mruby', path: 'mrbgems/mruby-socket'本仓库的mruby-bin-mirb、mruby-bin-mrbc等 Gem 正是以:core形式被 default.gembox 引入的:
conf.gem :core => "mruby-bin-mrbc":core表示从 mruby 自带的mrbgems目录中查找对应名称的 Gem。
从 mgem-list 索引取 Gem
mgem-list 是 mruby 社区的 Gem 索引仓库,通过:mgem选项可以直接引用其中的 Gem:
conf.gem :mgem => 'mruby-yaml' conf.gem :mgem => 'yaml' # 'mruby-' 前缀可以省略固定依赖的提交版本
需要固定某个 Gem 的检出提交时,使用:checksum_hash选项:
conf.gem mgem: 'mruby-redis', checksum_hash: '3446d19fc4a3f9697b5ddbf2a904f301c42f2f4e'缺失依赖的自动解析
如果构建中缺少某个 Gem 的依赖,mrbgems 的依赖求解器会从 mruby 核心 Gem(:core)或 mgem-list 中自动引用对应 mrbgem。
同名 Git Gem 冲突与 canonical 选项
当多个基于 git 的 Gem 具有相同的基础名(即默认检出目录名)时,除非它们的仓库 URL、分支名和提交 ID(即 checksum hash)完全一致,否则会报错。可以通过先显式导入自己偏好的版本并设置canonical: true来绕过:
conf.gem github: 'me/mruby-yaml', branch: 'my-hacked-branch', canonical: true设置后,系统会(在多数情况下静默地)忽略后续针对同名 Gem 的克隆尝试。
需要注意:canonical只影响从 git 克隆 Gem 这一环节,并不解决版本冲突。如果 Gem 的 rakefile 中声明的版本与某个依赖不兼容,构建仍然会失败。
GemBox:批量组合 mrbgem
当需要一次性加入一批 mrbgem,或希望按不同配置切换 Gem 组合时,可以使用 GemBox。GemBox 是一个文件,内部以与config.gem相同的格式列出要加载的 mrbgem,但整体包裹在MRuby::GemBox对象中,通过config.gembox 'boxname'加载。
下面是一个包含mruby-time与mrbgems-example的 GemBox 示例:
MRuby::GemBox.new do |conf| conf.gem "#{root}/mrbgems/mruby-time" conf.gem :github => 'masuidrive/mrbgems-example' endGemBox 与MRuby::Build使用相同的约定。要让 mruby 识别,GemBox 文件必须以.gembox扩展名保存在mrbgems目录中。把上面的内容保存为mrbgems/custom.gembox后,在构建配置文件的 build 块中加入:
conf.gembox 'custom'构建时customGemBox 即被读取,mruby-time与mrbgems-example会被加入构建。
如果想把 GemBox 放在 mruby 目录之外,则必须使用绝对路径:
conf.gembox "#{ENV["HOME"]}/mygemboxes/custom"mruby 自带的两个 GemBox
mruby 内置两个 GemBox:
- default.gembox:包含 mruby 的若干核心组件。其实现依次引用了
stdlib、stdlib-ext、stdlib-io、math、metaprog五个 gembox,并加入mruby-bin-mrbc、mruby-bin-mirb、mruby-bin-mruby、mruby-bin-strip、mruby-bin-config五个可执行 Gem; - full-core.gembox:包含
mrbgems目录下除mruby-bin-debugger、mruby-test之外的所有 Gem。其实现直接扫描目录:
MRuby::GemBox.new do |conf| Dir.glob("#{root}/mrbgems/mruby-*/mrbgem.rake") do |x| g = File.basename File.dirname x conf.gem :core => g unless g =~ /^mruby-(?:bin-debugger|test)$/ end end在 deps/mruby/build_config/default.rb 中正是通过conf.gembox 'default'引入整套默认 Gem 集。此外mrbgems目录下还提供了default-no-fpu.gembox、default-no-stdio.gembox、math.gembox、stdlib.gembox等更细粒度的组合,方便按平台与需求裁剪。
GEM 目录结构
一个最完整的 mrbgem 目录结构如下:
+- GEM_NAME <- Gem 名称 | +- README.md <- Gem 说明文档 | +- mrbgem.rake <- Gem 规格文件(Specification) | +- include/ <- Ruby 扩展所需的头文件(会被导出) | +- mrblib/ <- Ruby 扩展源码 | +- src/ <- C 扩展源码 | +- tools/ <- 可执行程序源码(C) | +- test/ <- 测试代码(Ruby)mrblib目录存放用于扩展 mruby 的纯 Ruby 文件;src目录存放用于扩展 mruby 的 C/C++ 文件;include目录存放 C/C++ 头文件;test目录存放供mrbtest使用的 C/C++ 与纯 Ruby 测试文件;mrbgem.rake是编译 C 与 Ruby 文件所需的规格描述;README.md是 Gem 的简要说明。
以本仓库为例:mruby-time 遵循mrblib + src + mrbgem.rake结构,其 C 实现在 src/time.c 中通过 mruby C API 定义Time类;mruby-bin-mirb 则展示了tools/目录的二进制 Gem 结构。
构建流程与 mrbgem.rake 规格文件
mrbgems 要求每个 Gem 目录下必须存在名为mrbgem.rake的规格文件。一个典型的规格文件如下:
MRuby::Gem::Specification.new('c_and_ruby_extension_example') do |spec| spec.license = 'MIT' spec.author = 'mruby developers' spec.summary = 'Example mrbgem using C and Ruby' end构建过程会根据该规格编译 Object 文件与 Ruby 文件,编译结果被加入lib/libmruby.a。这个静态库把 Gem 的功能暴露给mruby、mirb等工具。
信息属性
MRuby::Gem::Specification内可设置以下信息属性:
spec.license或spec.licenses(该 Gem 采用的一个或多个许可证)spec.author或spec.authors(开发者名字或列表)spec.version(当前版本号)spec.description(详细描述)spec.summary(一行短描述,设置后会在 rake 的构建摘要中打印)spec.homepage(主页)spec.requirements(供用户了解的外部需求)
license与author是每个 Gem 必填的属性!
仓库中的真实 Gem 均遵循这一规范,例如 mruby-time/mrbgem.rake:
MRuby::Gem::Specification.new('mruby-time') do |spec| spec.license = 'MIT' spec.author = 'mruby developers' spec.summary = 'standard Time class' end而 mruby-env/mrbgem.rake 展示了authors(复数)与version的用法:
MRuby::Gem::Specification.new('mruby-env') do |spec| spec.license = 'MIT' spec.authors = 'Internet Initiative Japan Inc.' end声明依赖:add_dependency
当 Gem 依赖其他 Gem 时,使用spec.add_dependency(gem, *requirements[, default_get_info]):
MRuby::Gem::Specification.new('c_and_ruby_extension_example') do |spec| spec.license = 'MIT' spec.author = 'mruby developers' # 添加依赖 mruby-parser,版本必须介于 1.0.0 与 1.5.2 之间 spec.add_dependency('mruby-parser', '>= 1.0.0', '<= 1.5.2') # 使用 GitHub 上任意版本的 mruby-uv spec.add_dependency('mruby-uv', '>= 0.0.0', :github => 'mattn/mruby-uv') # 使用 GitHub 上最新版 mruby-onig-regexp(版本要求可省略) spec.add_dependency('mruby-onig-regexp', :github => 'mattn/mruby-onig-regexp') # 仅测试时激活的额外 gem spec.add_test_dependency('mruby-process', :github => 'iij/mruby-process') end版本要求与默认 Gem 信息都是可选的。仓库中的实际例子可见 mruby-bin-mrbc/mrbgem.rake:
MRuby::Gem::Specification.new 'mruby-bin-mrbc' do |spec| spec.license = 'MIT' spec.author = 'mruby developers' spec.summary = 'mruby compiler executable' spec.add_dependency 'mruby-compiler', :core => 'mruby-compiler' ... end以及 mruby-onig-regexp/mrbgem.rake 中的add_dependency 'mruby-string-ext', core: 'mruby-string-ext'。
版本运算符
版本要求支持以下运算符:
=:等于!=:不等于>:大于<:小于>=:大于等于<=:小于等于~>:大于等于当前版本,且小于下一个主版本- 例 1:
'~> 2.2.2'表示>= 2.2.2且< 2.3.0 - 例 2:
'~> 2.2'表示>= 2.2.0且< 3.0.0
- 例 1:
当传入多个版本要求时,依赖必须同时满足全部要求。
默认 Gem 信息与覆盖
可以在add_dependency的最后一个参数传入Hash作为默认 Gem 信息,用于在构建配置中未定义该依赖时使用。其格式与MRuby::Build#gem方法的参数相同,只是不能作为路径型 Gem 位置使用。
当需要某个特定版本的依赖时,在构建配置中使用MRuby::Build#gem即可覆盖默认 Gem。
声明冲突:add_conflict
如果存在互相冲突的 Gem,使用spec.add_conflict(gem, *requirements),其中requirements参数与add_dependency一致:
MRuby::Gem::Specification.new 'some-regexp-binding' do |spec| spec.license = 'BSD' spec.author = 'John Doe' spec.add_conflict 'mruby-onig-regexp', '> 0.0.0' spec.add_conflict 'mruby-hs-regexp' spec.add_conflict 'mruby-pcre-regexp' spec.add_conflict 'mruby-regexp-pcre' end复杂构建选项
当 Gem 有更复杂的构建需求时,可在规格中使用以下选项:
spec.cc.flags(C 编译器参数)spec.cc.defines(C 编译器宏定义)spec.cc.include_paths(C 编译器头文件搜索路径)spec.linker.flags(链接器参数)spec.linker.libraries(链接库)spec.linker.library_paths(链接器附加库路径)spec.bins(生成的可执行文件名)spec.rbfiles(要编译的 Ruby 文件)spec.objs(要编译的 Object 文件)spec.test_rbfiles(集成进 mrbtest 的 Ruby 测试文件)spec.test_objs(集成进 mrbtest 的 Object 测试文件)spec.test_preload(mrbtest 的初始化文件)
此外还可以用spec.mruby.cc与spec.mruby.linker为编译器和链接器添加全局参数。
仓库中 mruby-onig-regexp/mrbgem.rake 是复杂构建的典型范例:它先探测系统是否已安装onigmo/oniguruma库(通过search_package与search_header_path),找不到时则通过spec.bundle_onigmo方法自动解包捆绑的onigmo-6.2.0.tar.gz、调用 autotools 构建静态库、把产物对象并入libmruby.a,并动态追加cc.defines与cc.include_paths,完整展示了cc、linker、bins、add_dependency等规格选项的组合用法。
include_paths 与依赖传播
Gem 可以把 include 路径导出给依赖它的其他 Gem。默认情况下,/...绝对路径.../{GEM_NAME}/include会被导出,因此不建议把 Gem 的本地头文件放在include/下。
导出具有追溯性:例如 B 依赖 C、A 依赖 B 时,A 也能获得 C 导出的 include 路径。被导出的 include_paths 会自动追加到 Gem 本地的 include_paths 中;如果构建需求更复杂,可以使用spec.export_include_paths访问器。
C 扩展
mruby 可通过 C API 集成 C 库实现扩展。
前提条件
mrbgems 要求实现名为mrb_YOURGEMNAME_gem_init(mrb_state)的 C 函数,其中YOURGEMNAME替换为 Gem 名称。若 Gem 名为c_extension_example,初始化函数如下:
void mrb_c_extension_example_gem_init(mrb_state* mrb) { struct RClass *class_cextension = mrb_define_module(mrb, "CExtension"); mrb_define_class_method(mrb, class_cextension, "c_method", mrb_c_method, MRB_ARGS_NONE()); }Finalize 终结函数
mrbgems 还要求实现mrb_YOURGEMNAME_gem_final(mrb_state)终结函数,用于释放资源:
void mrb_c_extension_example_gem_final(mrb_state* mrb) { free(someone); }从源码结构看,gem_init/gem_final与 mruby 核心的类定义宏(如mrb_define_module、mrb_define_class_method)配合,构成了 C 扩展的标准初始化/清理生命周期。
目录结构示例
+- c_extension_example/ | +- README.md (可选) | +- src/ | | | +- example.c <- C 扩展源码 | +- test/ | | | +- example.rb <- C 扩展测试代码 | +- mrbgem.rake <- Gem 规格文件以仓库中的 mruby-time 为例,其 src/time.c 通过 mruby C API 定义了完整的Time类(含时间获取、格式化、时区转换等功能),并针对不同平台处理gettimeofday、gmtime_r等可移植性问题,是研究 C 扩展实现细节的优质样本。
Ruby 扩展
mruby 也可以用纯 Ruby 扩展,支持覆写已有类或新增类。把所有 Ruby 文件放入mrblib目录即可。
前提条件
无。
目录结构示例
+- ruby_extension_example/ | +- README.md (可选) | +- mrblib/ | | | +- example.rb <- Ruby 扩展源码 | +- test/ | | | +- example.rb <- Ruby 扩展测试代码 | +- mrbgem.rake <- Gem 规格文件C 与 Ruby 混合扩展
mruby 可以同时用 C 和 Ruby 扩展:Ruby 文件放入mrblib,C 文件放入src。
关键执行顺序:mrblib下的 mruby 代码会在 Gem 的 Cinit函数被调用之后执行。因此必须保证mruby 脚本依赖 C 代码,而C 代码不依赖 mruby 脚本。
目录结构示例
+- c_and_ruby_extension_example/ | +- README.md (可选) | +- mrblib/ | | | +- example.rb <- Ruby 扩展源码 | +- src/ | | | +- example.c <- C 扩展源码 | +- test/ | | | +- example.rb <- C 与 Ruby 扩展测试代码 | +- mrbgem.rake <- Gem 规格文件本仓库 H2O 的 mruby 处理程序(lib/handler 下的相关 mruby 集成)正是依托这类混合扩展机制,将 C 层的请求处理能力与 Ruby 层的业务脚本结合起来。
二进制 Gem(Binary Gems)
部分 Gem 会在bin目录下生成可执行文件,这类 Gem 称为二进制 Gem。其命名惯例以mruby-bin为前缀,例如mruby-bin-mirb和mruby-bin-strip。
在mrbgem.rake中通过spec.bins指定可执行文件名。入口函数main()应位于tools/<bin>/*.c中(<bin>为可执行文件名)。<bin>目录下的 C 文件会被编译并链接进可执行文件,但不会包含进libmruby.a;而mrblib与src下的文件则会进入libmruby.a。
强烈建议二进制 Gem 不要包含mrblib和src目录,以将普通 Gem 与二进制 Gem 明确分离。
目录结构示例
+- mruby-bin-example/ | +- README.md (可选) | +- bintest/ | | | +- example.rb <- 二进制 Gem 测试代码 | +- mrbgem.rake <- Gem 规格文件 | +- mrblib/ <- Ruby 扩展源码(可选) | +- src/ <- C 扩展源码(可选) | +- tools/ | +- example/ <- 可执行文件名称目录 | +- example.c <- 可执行文件源码(含 main)仓库中的真实范例是 mruby-bin-mrbc/mrbgem.rake:它通过spec.bins声明生成mrbc可执行文件,把tools/mrbc/*.c编译链接为build/bin/mrbc,并将mrbc加入build.bins,同时通过add_dependency 'mruby-compiler', :core => 'mruby-compiler'依赖编译器核心 Gem。类似的还有 mruby-bin-mirb/mrbgem.rake,它额外展示了根据宿主环境探测 readline/linenoise 并动态调整cc.defines与linker.libraries的自动化处理。
在 H2O 中的实际应用
本仓库将 mruby(含 mrbgems 机制)作为嵌入式脚本引擎集成于 H2O Web 服务器中:
- 构建层面,mruby 通过 build_config.rb 指向 build_config/default.rb,再由
conf.gembox 'default'加载 default.gembox; - Gem 层面,H2O 在 deps 目录下捆绑了
mruby-digest、mruby-env、mruby-input-stream、mruby-json、mruby-onig-regexp、mruby-require、mruby-class-new-fiber-safe、mruby-dir、mruby-errno、mruby-file-stat等 mrbgem,其mrbgem.rake规格(如 mruby-env/mrbgem.rake、mruby-input-stream/mrbgem.rake)全部遵循本文所述的规范编写; - 使用层面,H2O 的 mruby 处理程序(
lib/handler/mruby相关源码)通过这些 Gem 获得文件系统、环境变量、正则表达式、JSON、摘要算法等能力,并配合mruby-require实现脚本加载。
如果需要自定义 H2O 内嵌 mruby 的扩展集,可按本文方法编辑构建配置:复制 build_config/default.rb 为自定义配置,用conf.gem/conf.gembox增删 Gem,再通过rake MRUBY_CONFIG=/path/to/myconfig.rb编译,即可得到按需裁剪的 mruby 运行时。
总结
mrbgems 为 mruby 提供了一套完整、标准化的扩展管理体系:conf.gem负责把本地或远程(git/github/bitbucket/mgem)的 Gem 接入构建,MRuby::GemBox支持批量组合与按配置切换,mrbgem.rake规格文件统一描述 Gem 的信息、依赖、冲突与编译选项,gem_init/gem_final规定了 C 扩展的生命周期,mrblib/src/include/tools/test目录划分了各类源码与测试的职责。无论是为 mruby 开发一个简单的Time类扩展,还是像 H2O 一样把整套脚本运行时嵌入服务器,掌握 mrbgems 都是定制与扩展 mruby 的基础。
【免费下载链接】h2oH2O - the optimized HTTP/1, HTTP/2, HTTP/3 server项目地址: https://gitcode.com/gh_mirrors/h2/h2o
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考