分享 Ruby 扩展库两种写法

gyorou · 2014年12月04日 · 最后由 gaolei14 回复于 2017年01月12日 · 9504 次阅读

算是学习心得?欢迎指正。

考虑 ruby 扩展的情况一般有两种,一种是实现某个东西的时候,ruby 的代码是在是太慢。但是又实在是贪恋 ruby 便利的书写方法。 另一种情况则是想给用 C,C++ 实现的功能函数加上一层 ruby 的 wrap,使得能够在 ruby 程序中方便调用这些功能。

一般方法

就实现 ruby 的 C 语言扩展而言,首先当然是要准备好用 c 语言实现的源码。 比如有这么一个简单的加法函数。

int add(a, b)
     int a, b;
{
  return( a + b );
}

接下来就要用 ruby 中的一套数据结构来对这个函数进行包裹。

//warpper.c
#include "ruby.h"

int add(int a, int b);
int add(a, b)
     int a, b;
{
  return( a + b );
}

VALUE wrap_add(self, aa, bb)
     VALUE self, aa, bb;
{
  int a, b, result;

  a = FIX2INT(aa);
  b = FIX2INT(bb);
  result = add(a,b);
  return INT2FIX(result);
}

void Init_test()
{
  VALUE module;

  module = rb_define_module("Test");
  rb_define_module_function(module, "add", wrap_add, 2);
}

如果你觉得看这个代码比较吃力,则可以去读一下ruby 源码解读的内容。

  • VALUE c 语言中 ruby 对象数据结构的地址。其实际上是一个 unsigned int 类型。ruby 的所有扩展库函数必须返回 VALUE。一切 ruby 的对象的数据结构在声明时候也必须是 VALUE。

  • FIX2INT 将 ruby 的 Fixnum 转换成 C 语言的 int 类型。

  • INT2FIX 和上面反过来。

  • rb_define_module 定义模块并初始化。

  • rb_define_module_function(module, "add", wrap_add, 2) 向 module 指向的模块中添加名为 add 的方法,这个方法的 c 语言实体函数为 wrap_add,带两个参数。注意这个两个参数是 ruby 中 add 方法的参数,而 warp_add 的第一个参数为 self,所以我们在 ruby 中不管何时何地都有一个 self。

当然还要有很多其他常用的,比如

  • NUM2DBL(value) ruby 中的 Num 转换为 c 的 double

  • STR2CSTR(value) ruby 中的 String 转换为 C 的 char *

  • rb_str_new2(s) 用 char *s 指向的字符串新建一个 ruby 的 String 对象。并自动计算其长度。

等等等等

warp 函数的处理大抵就是一下步骤

  1. 将参数从 ruby 对象转换成 C 的变量

  2. 用 C 的变量调用 C 的函数进行计算,获取 C 的结果变量。

  3. 将 C 的结果变量转换成 ruby 的对象并返回。

最后在 void Init_xxx() 中,我们可以做的事情有

  • rb_define_class() 定义一个类
  • rb_define_class_under() 在类中定义类
  • rb_define_module() 定义一个模块
  • rb_define_module_under() 在模块中定义模块
  • rb_define_module_function() 在模块中定义一个方法
  • rb_define_class_function 在类中定义方法
  • ……

其实不宜把逻辑写的太复杂,注意用 C 语言实现的,通常只是很消耗计算时间的那一小块内容。其他的一些衔接可以继续用 ruby 实现。

写好上面的 wrap.c 之后,在同一个目录中新建一个文件命名为 extconf.rb

#extconf.rb
require "mkmf"
create_makefile("hoge")

接下来执行ruby extconf.rb。ruby 就会生成一个扩展库的 makefile,然后执行make 即可以得到一个名为 hoge 的 ruby 库。这个 hoge 库根据不同环境可能名称不同。linux 下为.so 文件,而 OS X 下则是.bundle 文件。

swig

如果熟悉 ruby 的 C 语言源码,并且我们是白手起家写一个东西或者,我们要写的代码很简单,那么上述方法自然是很顺利。当我们要将一个大的 C 库包裹上 ruby 的接口,显然上面的方法就有些繁琐而力不从心了。

我们仔细考虑一下,其实,wrap 函数无非就是在做 ruby 的对象到 C 的数据类型的变换。这些变化当然可以通过某种方法自动完成。

于是 swig 就是这样一个工具。

下面就用 swig 来写一个 NLPIR 中文分词的 ruby 的 wrapper。

环境

  • linux ubuntu 12.04 32-bit

  • ruby 2.0.0

  • gcc v4.6

首先从这里下载最新的接口包。将里面的 data 文件夹,linx32 目录里面的 libNLPIR.so 放到当前目录中来。然后将这里的两个文件也放到当前目录中。

注意,NLPIR.h 是经过修改过的头文件,不知道为什么包里自带的头文件对 gcc 兼容性极差。

注意看 swig 的写法。

%module NLPIR
%{
#define SWIG_FILE_WITH_INIT
#include "NLPIR.h"
%}
#define POS_MAP_NUMBER 4
#define ICT_POS_MAP_FIRST 1
#define ICT_POS_MAP_SECOND 0
#define PKU_POS_MAP_SECOND 2
#define PKU_POS_MAP_FIRST 3
#define  POS_SIZE 40

#define GBK_CODE 0
#define UTF8_CODE GBK_CODE+1
#define BIG5_CODE GBK_CODE+2
#define GBK_FANTI_CODE GBK_CODE+3

int NLPIR_Init(const char * sDataPath=0,int encode=GBK_CODE,const char*sLicenceCode=0);

bool NLPIR_Exit();

const char * NLPIR_ParagraphProcess(const char *sParagraph,int bPOStagged=1);

const result_t * NLPIR_ParagraphProcessA(const char *sParagraph,int *pResultCount,bool bUserDict=true);

int NLPIR_GetParagraphProcessAWordCount(const char *sParagraph);

void NLPIR_ParagraphProcessAW(int nCount,result_t * result);

double NLPIR_FileProcess(const char *sSourceFilename,const char *sResultFilename,int bPOStagged=1);

unsigned int NLPIR_ImportUserDict(const char *sFilename);

int NLPIR_AddUserWord(const char *sWord);

int NLPIR_SaveTheUsrDic();

int NLPIR_DelUsrWord(const char *sWord);

double NLPIR_GetUniProb(const char *sWord);

bool NLPIR_IsWord(const char *sWord);

const char * NLPIR_GetKeyWords(const char *sLine,int nMaxKeyLimit=50,bool bWeightOut=false);

const char * NLPIR_GetFileKeyWords(const char *sFilename,int nMaxKeyLimit=50,bool bWeightOut=false);

const char * NLPIR_GetNewWords(const char *sLine,int nMaxKeyLimit=50,bool bWeightOut=false);

const char * NLPIR_GetFileNewWords(const char *sFilename,int nMaxKeyLimit=50,bool bWeightOut=false);

unsigned long NLPIR_FingerPrint(const char *sLine);

int NLPIR_SetPOSmap(int nPOSmap);

CNLPIR* GetActiveInstance();

bool NLPIR_NWI_Start();

int  NLPIR_NWI_AddFile(const char *sFilename);

bool NLPIR_NWI_AddMem(const char *sText);

bool NLPIR_NWI_Complete();

const char * NLPIR_NWI_GetResult(bool bWeightOut=false);

unsigned int  NLPIR_NWI_Result2UserDict();

其中的中括号涵盖了头文件的位置,然后下面则是我们需要进行包裹的函数。

然后就执行 swig -c++ -ruby nlpir.i 这个时候会在当前目录下生成一个 nlpir.c_xx 的文件,其实这个就是我们刚才自己写的 wrap 的本体,而 swig 从函数的返回值,参数中自动帮我们生成了这些内容。

在当前目录下新建一个 extconf.rb,内容为

require 'mkmf'

$CFLAGS+="-Wall"
$LOCAL_LIBS+="./libNLPIR.so"
$libs = append_library($libs, "supc++")
create_makefile('Nlpir')

注意其中加上 c++ 的运行环境,这个包用纯 c 编译会出错。

接下来和刚才一样了,执行ruby extconf.rb,然后 make。

这个时候显示 make 成功,目录下多出来一个 Nlpir.so 文件。这个就是我们生成的 ruby 扩展库了。我们试着用 irb 来加载。

irb(main):001:0> require "./NLPIR"
=> true
irb(main):002:0> NLPIR.NLPIR_Init
=> 1
irb(main):003:0> NLPIR.NLPIR_ParagraphProcess("吃不到葡萄说葡萄酸。")
=> "\xE5\x90/n \x83\xE4/n \xB8\x8D/n \xE5\x88/n \xB0\xE8/v \x91\xA1/n \xE8\x90/n \x84\xE8/n \xAF\xB4/n \xE8\x91/n \xA1\xE8/m \x90\x84/n \xE9\x85/n \xB8\xE3/v \x80\x82/n "

啊,忘记将默认编码改成 utf-8 了。

恩,其实我是故意的。我就是要来说一句, 至今默认 GBK 是 NLPIR 这个工具的硬伤。

赞一个,灭零

看到很多 Ruby 库是用ffi来写拓展的,不知楼主试用过么? https://github.com/ffi/ffi

赞....楼主出品必属精品

#2 楼 @spacewander 没试过,可能是我参考的地方太老了…

#4 楼 @gyorou 的确很老,k&r 的 c 语法

匿名 #6 2014年12月21日

:plus1:

请问楼主用什么集成测试 C 扩展?

Hoe : https://github.com/jbarnette/hoe-debugging

#6 楼 @chanshunli 哈哈,没考虑过这么实用的问题…… 感谢分享。

使用 mkmf,在 Mac 下使用第三方的 framework,怎么写都找不到这个 framework?有能解决的么?

需要 登录 后方可回复, 如果你还没有账号请 注册新账号