一些关于 C 库的有趣 Ruby Gems

February 01, 2019 16:53


First

以前有写过 Ruby 与 C、Golang 共享库的一些相关文章,对于接口简单、没有复杂数据结构的库来说,直接编译 Ruby 动态库,或是使用 FFI 加载,都是比较方便的。

直到我遇到了很复杂的接口时,那时真的是写 FFI 代码还不如直接用 C 封装成 Ruby 动态库来 require。比如说 LibGraphQL。然后在研究这方面的时候,发现了一些比较实用和有趣的 Gems。都是很老的了,基本没有更新了,但其实现思路还是让我受益不少。

RubyInline

以前在了解 FFI 时,就知道 rubyinline 了, 但一直没有去研究它的实现。看了下源代码,发现很多有趣的东西。

require "inline"
class MyTest
  inline do |builder|
    builder.c "
      long factorial(int max) {
        int i=max, result=1;
        while (i >= 2) { result *= i--; }
        return result;
      }"
  end
end
puts MyTest.new.factorial 5

这是它的一个示例代码。它是怎么在 Ruby 里面写 C 代码并直接使用 Ruby Code 调用的呢?

查源代码会发现,这个 Gem 会先替换 C 的基础类型为 Ruby VALUE ,如果使用 builder.c_raw 则不会替换; 然后把识别到的 C 方法名字 factorial 通过 Ruby 的 C 方法添加到 MyTest 类上。最终生成下面的 C 代码在 ~/.ruby_inline 目录下

#include "ruby.h"

# line 5 "a.rb"
static VALUE factorial(VALUE self, VALUE _max) {
  int max = FIX2INT(_max);
        int i=max, result=1;
        while (i >= 2) { result *= i--; }
        return LONG2NUM(result);
      }


#ifdef __cplusplus
extern "C" {
#endif
  void Init_Inline_MyTest_cb89593d1f9fe3ecdb2178215eee80ae() {
    VALUE c = rb_cObject;
    c = rb_const_get(c, rb_intern("MyTest"));

    rb_define_method(c, "factorial", (VALUE(*)(ANYARGS))factorial, 1);
  }
#ifdef __cplusplus
}
#endif

接着,把上面的代码编译为 Ruby 动态库以供 require 方法直接加载。 编译时,通过 RbConfig::CONFIG 来获取一些系统相关的编译参数。 如果 RubyInline Ruby 代码加入了相关的 headers 或是 dynamic library 依赖,同样会在编译时自动加上。

这个库基本就是实现了,通过模式匹配替换变量类型,识别 C 方法名为 Ruby 类加上对应的方法。 然后生成对应的 Ruby C 代码,然后自动链接编译 Ruby Dynamic Library 并 require。

基于这么模式,我们可以想到:其它非 C 的语言,但支持生成 C 动态库的应该也是同样可以转成这样的方式来写的。

ffi-gen

ffi-gen 又是一个有趣的库。

require "ffi_gen"

FFIGen.generate(
  module_name: "Clang",
  ffi_lib:     "clang",
  headers:     ["clang-c/Index.h"],
  cflags:      `llvm-config --cflags`.split(" "),
  prefixes:    ["clang_", "CX"],
  output:      "clang-c/index.rb"
)

上面的官方示例代码做的事就是,解析 clang-c/Index.h 这个 C 的头文件, 生成生成一个 Clang 的基于 Ruby FFI 接口定义的 ruby module,输出到 clang-c/index.rb。 然后就可以直接拿这个生成的文件用了不再有 ffi-gen 什么事了。

它会生成对应的 methods, structures, unions, enumerations and callbacks。 直接都不用手写 FFI 接口定义了。这个库关键点就是解析 C Code。 它是通过 clang 这个 C 库的 Ruby FFI bindings 来解析的。而 clang 这个 Ruby bindings 还是他自己生成。 然后就是把 C AST 输入成 Ruby FFI 代码了。基本是一个苦力活了。而且这个 Gem 还会输入相关的代码注释。 相当于文档也生成出来了。

作者测试过生成 Cairo, CEF, Clang, LibSSH2, LLVM, OpenGL, SQLite3 这些库。 但是这么牛逼的库上次更新都在 4 年前了,Clang 版本还是停留在 3.5。说明中也只提到 Ruby 1.8 兼容。

我自己试用了下,在 Ruby 2.4.5 下,还是可以生成和使用。但也不知道是不是真的没问题。 想想,生成出一个几千行的库。除了注释、空格应该也有近千行了。这么多的代码要是手动 FFI 定义实在是太累了。

END

好好学习,天天向上!

Comments: