Ruby 使用 Fiddle 调用 C 函数

cichol · March 24, 2015 · Last by xiao__liang replied at September 22, 2016 · 6191 hits

中英文关于 Fiddle 的资料好像都挺少,之前为了调用 win32api 去研究了一下,现在把一些经验记在这里。

先贴上两段代码:

C 代码:

#include <stdio.h>

int hw(char* str,int n){
    printf("%s\n", str);
    return n;
}

typedef struct{
    char* string;
    int integer;
} Test;

void hw_s(Test *t){
    t->integer = 1000;
    t->string[0] = 'r';
}

// compiled by
// gcc hw.c --share -o hw.so

Ruby 代码:

require 'fiddle'
require 'fiddle/import'

module Lib
  extend Fiddle::Importer
  dlload './hw.so'
  extern 'int hw(char*, int)'
  extern 'void hw_s(*)'

  Struct_Test = struct 'char* string, int integer'
end

a = Lib.hw("Hello World.", 9)   #=> 打印 Hello World.
a   #=>9

s = Lib::Struct_Test.malloc
s.string = "test"
Lib.hw_s(s)
s.integer   #=>1000
s.string.to_s   #=>rest
Fiddle.free(s.to_i)

以上是两个简单的调用例子,写了两个 C 函数,hw 打印输入的字符串,然后把输入的 int 原样返回,hw_s 接受一个结构体指针,然后对所指的结构做一些修改。

Ruby 方面,建立了一个 Lib 模块,然后扩展了Fiddle::Importer,Importer 封装了 Fiddle 的部件,可以用 DSL 的语法引入 C 库。

我们首先用 dlload 导入编译好了的动态链接库,然后用 extern 声明了两个函数原型。

extern 语句会对参数字符串进行解析,取出函数名、返回值类型和参数类型,用这些参数创建一个Fiddle::Function对象,也就是用于调用的 C 函数。并且给 Lib 模块注册了方法,使其可以通过类似Lib.hw()的方式调用这个函数。

这里 hw_s 函数接受的是一个结构体,而我在参数里只写了一个*符号,实际上去看看解析用的代码:

when /\*/, /\[\s*\]/
  return TYPE_VOIDP

可以发现所有[] *都会被转化成空指针型,所以指针的类型是无所谓的,hw 参数里的 char 实际上也可以省略。

先不管结构体的创建,看看 hw 函数的调用。

给 hw 传一个字符串,运行一下发现已经被打印出来了,第二个参数传入了 9,也可以发现这个 9 被返回到了 a 里。代码很直观,这样去调用 C 函数就不用像 require 那样特意去做一层封装。

不过常见的情况参数可能不会如此简单,像 win32api 很多要传一个结构体,那就涉及到结构体的构建了。

再次回到 Lib 模块,最后那句代码就用 struct 方法声明了一个结构体。struct 接受描述成员信息的字符串数组,也可以接受一个字符串,字符串的话一开始会被split(/\s*,\s*/),也就变成一个数组了。

接着解析器会把成员的类型和名字抽取出来,用Fiddle::CStructBuilder.create创建一个结构体类,然后赋值给 Struct_Test,这个类包含 malloc 方法,可以用来申请内存,然后结构体的成员也会被注册为实例方法,就可以对结构体的成员进行操作了。

底下的代码用 malloc 分配了一个结构体 s,然后把 s.string 设成了字符串 test,接着调用 hw_s。调用结束后检查结构体,发现 s.integer 已经被赋值为 1000,而 s.string 的第一个字符也按照 C 代码所写的被修改了。

接下来就是比较重要的一步了,用 malloc 分配的内存必须要进行回收,不然会造成内存泄露。

while true
  Lib::Struct_Test.malloc
end

尝试运行这段代码,打开任务管理器,可以看到占用内存一直飙升。

这里我直接用 Fiddle.free 方法回收内存,这个方法接受一个地址,而 s.to_i 会返回 s 的地址。似乎 Fiddle 类里的 to_i 都是返回地址,这应该是个惯例。

但是这样做的话,每次都要手动回收,不就和写 C 没什么差别了,有没有办法让 Ruby 的 GC 进行回收呢?

当然是可以的,去翻翻 Fiddle 的源码,结构体之类的创建都是对Fiddle::Point.new(address, size, freefunc)的封装,这个函数可以带上一个 freefunc 参数,freefunc 会在 GC 回收 Pointer 时调用,所以释放内存可以在 freefunc 里做,这样申请的内存就可以自动回收了。

实际上这个函数 Ruby 已经提供了一份,Fiddle::RUBY_FREE记录了 free() 函数的地址,我们可以直接创建一个函数对象:

free_func = Fiddle::Function.new(Fiddle::RUBY_FREE, [TYPE_VOIDP], TYPE_VOID)

不过麻烦的是,Fiddle::CStructBuilder并没有提供传入 freefunc 的方法,而是在内部直接把 freefunc=nil 处理了,对于这一点我很不理解,不过没关系,我们可以用一个猴子补丁来解决。

module Fiddle
  module CStructBuilder
    def create(klass, types, members)
      new_class = Class.new(klass){
        define_method(:initialize){|addr|
          #添加free_func
          free_func = Fiddle::Function.new(Fiddle::RUBY_FREE, [TYPE_VOIDP], TYPE_VOID)
          #@entity = klass.entity_class.new(addr, types)
          @entity = klass.entity_class.new(addr, types, free_func)
          @entity.assign_names(members)
        }
        define_method(:to_ptr){ @entity }
        define_method(:to_i){ @entity.to_i }
        members.each{|name|
          define_method(name){ @entity[name] }
          define_method(name + "="){|val| @entity[name] = val }
        }
      }
      size = klass.entity_class.size(types)
      new_class.module_eval(<<-EOS, __FILE__, __LINE__+1)
        def new_class.size()
          #{size}
        end
        def new_class.malloc()
          addr = Fiddle.malloc(#{size})
          new(addr)
        end
      EOS
      return new_class
    end
    module_function :create
  end
end

现在再执行那个 while 循环,内存占用维持稳定,问题解决。

顺便附上一个调用 win32api 的实例: https://github.com/CicholGricenchos/ShenmeGUI/blob/master/lib/shenmegui/file_dialog.rb

本文的代码在: https://github.com/CicholGricenchos/tricks/tree/master/fiddle 我的.so 是在 windows 下编译的,可能不太正宗= =


在了解到这个科技之后,当然是要尝试把一些工作转到 C 去做,于是想试试用 C 来进行排序。但是很快遇到了问题,Ruby 的 Array 对象没办法直接传给 C 函数,尽管都是 Fixnum,Fiddle 类也没有相关的转换方法。

于是我只能用 pack 来把数组打包成字符串,虽然测试是成功了,但 pack 和 unpack 的过程耗费了大量的时间。不知道有没有什么更好的方法。

代码也贴上:

C:

#include <stdio.h>
#include <stdlib.h>

void merge(int *a, int *aux, int lo, int mid, int hi){
    int i = lo, j = mid+1, k;
    for(k = lo; k <= hi; ++k)   aux[k] = a[k];
    for(k = lo; k <= hi; ++k){
        if(i>mid)   a[k] = aux[j++];
        else if (j>hi)  a[k] = aux[i++];
        else if (aux[j]<aux[i]) a[k] = aux[j++];
        else a[k] = aux[i++];
    }
}

void merge_sort(int *data, int size){
    int *temp = (int*)malloc(sizeof(int) * size);
    int sz, lo, hi;
    for(sz = 1; sz < size; sz *= 2){
        for(lo = 0; lo < size-sz; lo += sz+sz){
            hi = size-1;
            if(lo+sz+sz-1 < hi) hi = lo+sz+sz-1;
            merge(data, temp, lo, lo+sz-1, hi);
        }
    }
    free(temp);
}

Ruby:

require 'fiddle'
require 'fiddle/import'
require 'benchmark'

module Lib
  extend Fiddle::Importer
  dlload 'sort.so'
  extern 'void merge_sort(int[], int)'
end

def sort_by_c(arr)
  pack = arr.pack('l' * arr.size)
  Lib.merge_sort(pack, arr.size)
  pack.unpack('l' * arr.size)
end

def sort_by_ruby(arr)
  arr.sort_by{|x| x}
end

a = (1..10000000).to_a.shuffle

p Benchmark.realtime{ sort_by_c(a.dup) } #=> 5.959973 其中pack及unpack可用去 2.364577
p Benchmark.realtime{ sort_by_ruby(a.dup) } #=> 24.802533

虽然如此,性能提升还是挺明显的 : -)

正需要这方面的资料,谢谢楼主

You need to Sign in before reply, if you don't have an account, please Sign up first.