Ruby语言的官方解释程序是使用C语言开发的,一般用C语言来编写扩展。D语言和C语言是二进制兼容的,所以可以使用D语言编写Ruby扩展。
一、移植C库到D的一般过程
C使用头文件来处理符号依赖,在D里面链接外部库文件时,要使用extern (C)声明来引入符号,这是一个转换过程。
如何转换一个C头文件到D文件?D文档的htomodule.html有详尽说明。一般的转换过程如下:
1、运行预处理程序处理掉头文件里面的宏。
2、删除经过预处理以后的多余信息。由于C的头文件包含,每个头文件经过预处理以后都会包含一些重复内容,我们需要剔除这部分内容,通过查找行号即可完成。
3、转换相应声明到D声明,这一步可以使用一个c2h程序来完成。
注意预处理程序处理完毕以后,宏函数以及宏定义的常量会被去除,这可能不是你想要的,所以最好的办法可能是手工转换。
另一种调用是在D里面调用动态链接库,这需要你使用implib工具从动态链接库产生一个.lib导入库文件,然后生成D声明,再编译链接即可。如果是在linux下使用共享库,则只需要编写D声明文件,然后直接链接即可。
二、调用C库
转换完毕以后,就可以调用了。如果你只是要测试一下,就可以只声明使用过的外部函数、变量即可。
例如我们要编写Programming Ruby里面的一个Ruby Extension例子,相应的D代码如下:
//
test.d
module test;
import ruby;
extern
(C)
VALUE t_init(VALUE self)
{
VALUE arr
=
rb_ary_new();
rb_iv_set(self,
"
@arr
"
, arr);
return
self;
}
extern
(C)
VALUE t_add(VALUE self, VALUE anObject)
{
VALUE arr
=
rb_iv_get(self,
"
@arr
"
);
rb_ary_push(arr, anObject);
return
arr;
}
extern
(C){
VALUE cTest;
alias VALUE(
*
func_type)();
export
void
Init_Test()
{
cTest
=
rb_define_class(
"
Test
"
, rb_cObject);
rb_define_method(cTest,
"
initialize
"
, cast(func_type)
&
t_init,
0
);
rb_define_method(cTest,
"
add
"
, cast(func_type)
&
t_add,
1
);
}
}
//
extern(C)
和C代码很相似。由于我们只使用了几个外部函数、变量,所以只需要声明这几个符号即可:
//
ruby.d
module ruby;
extern
(C){
alias
ulong
VALUE;
VALUE rb_cObject;
VALUE rb_ary_new ();
VALUE rb_ary_push (VALUE, VALUE);
VALUE rb_iv_set (VALUE,
char
*
, VALUE);
VALUE rb_iv_get (VALUE,
char
*
);
VALUE rb_define_class (
char
*
,VALUE);
void
rb_define_method (VALUE,
char
*
,VALUE(
*
)(),
int
);
}
三、生成动态链接库(Windows DLL)或共享库(Linux so文件)
D语言在Windows上编写DLL,除了要有D源文件以外,还要有DLL定义文件:
//
test.def
LIBRARY Test
DESCRIPTION
'
Test written in D
'
EXETYPE NT
CODE PRELOAD DISCARDABLE
DATA PRELOAD SINGLE
这是一个通用的格式,只是一些描述信息,因为D中可以使用export关键字导出符号,所以不需要在这里声明导出函数,只有在编写COM DLL时才会增加其它一些信息。
另外由于D语言要初始化GC以及其它一些信息,所以还需要在DllMain里面调用初始化及终止代码。由于不同平台的初始化过程不完全相同,这里我简单封闭了一下不同平台的初始化代码:
//
os/library.d
module os.library;
extern
(C){
void
gc_init();
void
gc_term();
version(Windows)
void
_minit();
void
_moduleCtor();
void
_moduleDtor();
void
_moduleUnitTests();
version(linux)
void
_STI_monitor_staticctor();
version(linux)
void
_STI_critical_init();
version(linux)
void
_STD_monitor_staticdtor();
version(linux)
void
_STD_critical_term();
}
extern
(C)
void
d_init()
{
//
writefln("Start init D runtime");
version(linux) _STI_monitor_staticctor();
version(linux) _STI_critical_init();
gc_init();
version(Windows) _minit();
_moduleCtor();
_moduleUnitTests();
//
writefln("init finished");
}
extern
(C)
void
d_fini()
{
//
writefln("Start term D runtime");
_moduleDtor();
gc_term();
version(linux) _STD_critical_term();
version(linux) _STD_monitor_staticdtor();
//
writefln("term finished");
}
现在可以为Windows编写初始化及终止代码:
//
os/dll.d
module os.dll;
private
import os.library;
private
import std.c.windows.windows;
HINSTANCE g_hInst;
extern
(Windows)
BOOL DllMain(HINSTANCE hInstance, ULONG ulReason, LPVOID pvReserved)
{
switch
(ulReason)
{
case
DLL_PROCESS_ATTACH:
d_init();
break
;
case
DLL_PROCESS_DETACH:
d_fini();
break
;
case
DLL_THREAD_ATTACH:
case
DLL_THREAD_DETACH:
//
Multiple threads not supported yet
return
false
;
}
g_hInst
=
hInstance;
return
true
;
}
由于Linux共享库并没有标准的入口函数(或是我不知道它),这里使用gcc扩展的初始、终止代码,不过是以C语言实现的:
//
os/so.c
#include
<
ruby.h
>
static
void
so_init(
void
) __attribute__((constructor));
static
void
so_fini(
void
) __attribute__((destructor));
extern
void
d_init(
void
);
extern
void
d_fini(
void
);
void
so_init(
void
)
{
d_init();
}
void
so_fini(
void
)
{
d_fini();
}
现在可以尝试编译链接,在Linux上编译命令如下:
gcc
-
o os
/
so.o
-
c os
/
so.c
-
I
/
usr
/
lib
/
ruby
/
1.8
/
i686
-
linux
gdc
-
o Test.so test.d os
/
so.o ruby.d
-
shared
-
fPIC
-
lruby
你可以在irb下测试:
require
'
Test
'
test
=
Test.
new
test.add(
1
)
test.add(
2
)
test.add(
"
a
"
)
可以看到add总是返回一个array,与期望结果一致。
使用gdc是因为dmd在linux上无法生成共享库。
在Windows上的编译命令如下:
dmd
-
oftest.dll test.d test.def ruby18.lib os
/
dll.d os
/
library.d
ruby18.lib是使用implib从msvcrt-ruby18.dll导出的,这个编译过程很顺利,不过不幸的是它运行有一些问题,大概是一些初始值错误,我暂时还没有找到原因。或许也应该尝试一下gdc,不过我不知道如何从.DLL文件导出一个gdc支持的ELF格式的导入库文件。
四、项目打算
打算建立一个rubyd项目,除了转换ruby头文件以外,还要作一些扩展,比如转换D类到ruby类,这方面已有借鉴,比如dsource.org上的pyd项目。
由于以前在建立项目方面有过失败经历(asgard项目由于兴趣转移和其它原因比如语法丑陋等而未能进行),这次还是保守一些,先完成D声明的转换,我已经使用工具转换了所有头文件,不过正如前面所说,宏函数和宏常量都丢失了,所以需要重新手工转换。
五、其它问题
1、如何从.DLL文件导出一个gdc支持的ELF格式的导入库文件?如果你知道可以告诉我,先谢过了。
2、dmd生成可执行文件问题不大,生成动态链接库或共享库有很大缺陷,特别是不能生成共享库,我可不想再找一个只能再Windows上正常运行的编译器。如何让它改进这些方面?
3、gdc使用dmd前端和gcc后端,应该会成熟一些,不过一般会比dmd前端版本稍低,而且gdc发布版本不是很频繁,大概4-5个dmd版本才会有一个gdc版本(初略计算),所以一些新特性不能够及时加入进来。