中英文关于 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
虽然如此,性能提升还是挺明显的 : -)