mrbgems(mruby扩展)

译者 Lanza Schneider

mrbgems 是一个便于使用 C 语言和 ruby 脚本来扩展 mruby 的库管理器,本章会专门介绍它。

使用 mrbgems

首先要在构建配置文件中启用它。 你可以用如下的方式,启用一个特定的 GEM

conf.gem '/path/to/your/gem/dir'

也可以用相对路径指定一个 GEM

conf.gem 'examples/mrbgems/ruby_extension_example'

在这种情况下,

  • 如果你的构建配置文件位于 build_config 目录,那么指定 GEM 的路径会相对于 MRUBY_ROOT(即 mruby 源代码的根目录)。
  • 否则,GEM 的路径会相对于构建配置文件的所在路径。

mruby 同样支持托管于 GIT repo 的 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'

你还可以用 :path 参数来指定 repo 的子目录:

conf.gem github: 'mruby/mruby', path: 'mrbgems/mruby-socket'

你还可以通过使用 :mgem 来启用 mgem-list 中的 GEM:

conf.gem :mgem => 'mruby-yaml'
conf.gem :mgem => 'yaml' # 'mruby-' prefix could be omitted

可以指定 :checksum_hash 参数来选择一个特定的提交版本:

conf.gem mgem: 'mruby-redis', checksum_hash: '3446d19fc4a3f9697b5ddbf2a904f301c42f2f4e'

如果有缺失的依赖项,mruby 就会引用 core 或 mgem-list 当中的 GEM。

要在构建时从远程 GIT 仓库中提取所有 gem, 调用 rake -p, 或 rake --pull-gems

注: :bitbucket 只在 git 方式指定 GEM 时有效。

GemBox

在某些情况下,你可能会想要一次性地添加若干个 mrbgems 到 mruby 中;在这种场合下,你肯定希望用一个配置文件记录这些 mrbgems 并且复用它,而不是在每个构建配置文件中都写一次要添加的 mrbgems。 若干 mrbgems 的集合叫作 GemBox,它记录了要加载的 mrbgems 列表 (同样通过 conf.gem 的方式,但包装在一个 MRuby::GemBox 对象中)。 GemBox 通过 config.gembox 'boxname' 添加到 mruby 中。

举例,下列代码创建一个 GemBox 并将 mruby-timemrbgems-example 包含其中:

MRuby::GemBox.new do |conf|
  conf.gem "#{root}/mrbgems/mruby-time"
  conf.gem :github => 'masuidrive/mrbgems-example'
end

如上所述,GemBox 和 MRuby::Build 的设定方式相同。GemBox 必须存储在以 .gembox 为扩展名的文件,并放在 mrbgems 目录下以便让 mruby 引用。

我们把上述例子的 GemBox 存储为 custom.gembox 并放在 mruby 的 * mrbgems * 目录下,以及在配置文件中添加如下代码:

conf.gembox 'custom'

这会使 custom GemBox 在构建过程中被加载,也就是 mruby-timemrbgems-example 这两个 GEM 被添加到当前构建中。

如果你需要的话,也可以把 GemBox 放在 mruby 源代码目录之外,这种情况下用绝对路径就可以了:

conf.gembox "#{ENV ["HOME"]}/mygemboxes/custom"

有两个 GemBox 是 mruby 自带的: default 包含了几个 mruby 的核心组件; full-core 则包含了 mrbgems 目录下的所有 GEM

GEM 的结构

一个最完整的 GEM 目录结构如下:

+- GEM_NAME         <- GEM 名称
   |
   +- include/      <- 用于扩展 mruby 的头文件 (这个目录会添加到相应的编译器搜索路径)
   |
   +- mrblib/       <- Ruby 扩展源代码
   |
   +- src/          <- C 扩展源代码
   |
   +- test/         <- 测试代码 (Ruby)
   |
   +- mrbgem.rake   <- GEM 说明文件
   |
   +- README.md     <- GEM 自述文件

mrblib 目录下只包含纯 Ruby 源代码,而 src 目录包含的是 C/C++ 源代码。include 目录则存放 C/C++ 头文件。test 将会存放用于 mrbtest 测试时使用的 C/C++ 和 Ruby 源代码。mrbgem.rake 会存放关于 C 和 Ruby 扩展的信息 (比如一些编译 flags). README.md 则是 GEM 的一些简述文本.

构建过程

mrbgems 需要一个叫作 mrbgem.rake 的说明文件存放在 GEM 目录下,该文件的典型内容如下:

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

构建过程将会根据该文件的设置来编译 C 和 Ruby 源代码,编译结果会被归档到 lib/libmruby.a 静态库中。因此 GEM 的功能会被添加到像 mruby 或是 mirb 这样的应用产品上。

可以在 MRuby::Gem::Specification 对象中添加的描述:

  • spec.licensespec.licenses (一个或多个该 GEM 的授权协议 (的列表),如 "MIT")
  • spec.authorspec.authors (一个或多个开发者名称 (的列表))
  • spec.version (当前 GEM 的版本)
  • spec.description (细节描述)
  • spec.summary
    • 对 GEM 功能的简述 (往往只有一行)
    • 在 mruby 构建完成时将会显示在构建结果处
  • spec.homepage (该 GEM 的主页)
  • spec.requirements (关于该 GEM 所需外部依赖项的提示说明)

其中,licenseauthor 是必填项。

如果该 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

版本号并不是必须的 如果要填写,请看如下列表:

  • '=': 表示等于
  • '!=': 表示不等于
  • '>': 表示大于
  • '<': 表示小于
  • '>=': 表示大于或等于
  • '<=': 表示小于或等于
  • '~>': 是大于或等于主版本号,但是小于下一个次版本号
    • 示例 1: '~> 2.2.2' 表示 '>= 2.2.2' 且 '< 2.3.0'
    • 示例 2: '~> 2.2' 表示 '>= 2.2.0' 且 '< 3.0.0'

当你传递了多个版本需求时,依赖关系必须满足所有的版本需求。

构建配置中没有添加默认 gem 时,你可以使用它作为依赖。 当 add_dependency 调用的最后一个参数是 Hash 类型时,它将被视为默认的 gem 信息。其格式与方法 MRuby::Build#gem 的参数相同,但不能作为 gem 的路径来处理。

当需要特殊版本的依赖项时,在构建配置中使用 MRuby::Build#gem 来覆盖默认的 gem。

和依赖项相反,如果有不兼容其它 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 编译器 flags)
  • spec.cc.defines (C 编译器 defines)
  • spec.cc.include_paths (C 编译器的搜索路径)
  • spec.linker.flags (链接器 flags)
  • spec.linker.libraries (链接时的附加依赖库)
  • spec.linker.library_paths (链接时的附加依赖库的搜索路径)
  • spec.bins (生成的可执行文件)
  • spec.rbfiles (要编译的 Ruby 文件)
  • spec.objs (要编译产出的目标文件)
  • spec.test_rbfiles (测试时要编译的 Ruby 文件)
  • spec.test_objs (测试时要编译产出的目标文件)
  • spec.test_preload (mrbtest 时要加载的初始文件)

你也可以通过 spec.mruby.ccspec.mruby.linker 直接向整个构建过程添加额外的编译 / 链接选项。

包含目录和依赖项

GEM 可以把 include 目录导出给其它依赖于该 GEM 的其它 GEM 来使用。 默认情况下,/...absolute path.../{GEM_NAME}/include 会被导出。 所以推荐把该 GEM 用到的头文件放入 include/ 目录下。

这些导出都是可传递的,比如: 当 B 依赖 C 而 A 又依赖 B, A 也就可以用到 C 所导出的 include 目录。

导出的 include 目录会被 rake 程序自动添加到 GEM 本地的 include_paths 选项。 你也可以通过操作 spec.export_include_paths 选项来手动控制更复杂的构建情况。

C 扩展

mruby 可以用 C 语言 编写扩展,也就是你可以通过 mruby 的 C API 来把 C 库的功能传入 mruby。

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 ());
}

译者注:如果你使用 C++ 编写扩展,别忘了在该函数前添加 extern "C" 来确保该函数匹配 C ABI。

C 扩展的卸载

mrbgems 需要你编写一个名为 mrb_YOURGEMNAME_gem_final (mrb_state) 的 C 函数。YOURGEMNAME 是该 GEM 的名称。如果你的 GEM 名为 c_extension_example, 你的初始化函数应该写成这样:

void
mrb_c_extension_example_gem_final (mrb_state* mrb) {
  free (someone);
}

示例

+- c_extension_example/
   |
   +- src/
   |  |
   |  +- example.c         <- C 扩展的源代码
   |
   +- test/
   |  |
   |  +- example.rb        <- 测试用的 Ruby 代码
   |
   +- mrbgem.rake          <- GEM 说明文件
   |
   +- README.md

Ruby 扩展

mruby 也可以用纯 Ruby 代码进行扩展。这就便于你覆盖一些现存的类或者干脆创建一些新的类。这种扩展方式只要把所有 Ruby 源代码放入 GEM 的 mrblib 目录下即可。

示例

+- ruby_extension_example/
   |
   +- mrblib/
   |  |
   |  +- example.rb        <- Ruby 扩展源代码
   |
   +- test/
   |  |
   |  +- example.rb        <- 测试用的 Ruby 代码
   |
   +- mrbgem.rake          <- GEM 说明文件
   |
   +- README.md

C 和 Ruby 的混合扩展

你可以同时使用 C 和 Ruby 代码来扩展 mruby 的功能。

mrblib 目录下的代码会在 C 函数 gem_init 执行后调用。所以要确保 *mruby 脚本 * 依赖 *C 代码 * 而不是 *C 代码 * 依赖 *mruby 脚本 *。

示例

+- c_and_ruby_extension_example/
   |
   +- mrblib/
   |  |
   |  +- example.rb        <- Ruby 扩展源代码
   |
   +- src/
   |  |
   |  +- example.c         <- C 扩展的源代码
   |
   +- test/
   |  |
   |  +- example.rb        <- 测试用的 Ruby 代码
   |
   +- mrbgem.rake          <- GEM 说明文件
   |
   +- README.md