本文是动态链接库的第二版,前一版本只适用于Linux,但是这一版本添加了windows相关内容。
本文的内容是我在实现c2db的时候总结出来的。c2db是一个简单的ORM(实现中),它提供了一个接口层,下面有针对各种数据库独立实现的Driver,使用的时候我们只需要动态加载(而不是编译)就行了。针对这种需求,目前我们已经能够提供基本的跨平台实现,这里的平台主要是指posix和windows。
本文不再声称支持C/C++,因为个别原因,我们不得不使用core.runtime中已经跨平台的接口,因此细节部分不再是由我们来实现了。
0x00 基本知识
如果各位有不了解linker的,请先阅读一下这个wiki,如果想要深入,可以阅读《程序员的自我修养》。
这里我们以官方的dmd编译器为例。它对于标准库的默认处理方式是静态链接,但是在linux下同样提供标准库的动态链接版本,也就是libphobos2.so,而windows下,因为就ODR问题尚未得到一个公允的解决方案,因此目前并未提供libphobos2.dll,其实和linux下一样,只需要提供libphobos2.dll就能解决问题了,但是他们担心这样一来,不同的标准库版本,可能会给既有的程序到来兼容性问题。
根据官方的要求,不能同时存在多个runtime实例,这个问题在使用动态连接库的时候尤为突出。
linux下主要有如下2种情况:
- D程序 + C动态链接库,不需要动态链接phobos
- D程序 + D动态链接库,两者在编译的时候都需要动态链接phobos
在windows平台下,dmd只支持静态链接标准库,也就是说不可避免的会出现两个runtime实例,但是GC的存在注定会出现问题,对此,目前的解决方案是使用gc_proxy让dll使用主程序的gc,个人认为这是一个费力不讨好的方案,而且目前依旧存在很多问题。
0x01 接口介绍
上一节,我们阐述了,linux下有libphobos2.so可用,因此我们根本不用担心多个runtime实例的问题,但是windows下,我们不都不考虑多个runtime所带来的影响,因此为了方便使用,我们使用runtime的接口来加载和卸载标准库,它已经帮助我们完整了各平台下的封装。
void* Runtime.loadLibrary(string);
bool Runtime.unloadLibrary(void*);
具体读取动态链接库中的至于如何获取动态链接库中的符号,两者就不同了。
void* dlsym(void*, string);
FARPROC GetProcAddress(void*, string);
似乎也可以封装一下的,不是吗?
0x02 示例概要
如下4个源文件分别有如下作用:
- test.d:一个实现了TT的类Test。
- init.d:导出的函数,用于连接动态链接库和main函数。
- main.d:包含main,获取动态链接库中的方法和对象。
- tt.d:TT接口的声明。
linux下和windows下的代码基本是一样的,只有init.d不同,我们在本节先贴出相同的代码。
tt.d代码:
module tt;
interface TT {
void print();
}
test.d代码:
module test;
class Test : TT {
void print() {
import std.stdio;
writeln("pass");
}
}
main.d代码:
module main;
import core.runtime;
alias Fun = TT function();
// 代码中不包含函数的错误处理
void main() {
version(Posix) {
import core.sys.posix.dlfcn;
void* dll = Runtime.loadLibrary("./mydll.so");
void* ff = dlsym(dll, "init");
}
version(Windows) {
import core.sys.windows.windows;
void* dll = Runtime.loadLibrary("mydll.dll");
FARPROC ff = GetProcAddress(dll, "init");
}
Fun af = cast(Fun)(ff);
TT tt = af();
tt.print();
Runtime.unloadLibrary(dll);
}
代码都比较简单,这里就不详细解释了.
0x03 linux下示例细节
init.d代码:
module init;
import tt;
import test;
extern(C):
TT init() {
return new Test();
}
注意,添加extern(C)是为了简化symbol,如果你熟悉D的mangle,当然,你完全不必这么做。
编译脚本:
dmd -shared -fPIC -defaultlib=libphobos2.so -ofmydll.so init.d test.d
dmd -defaultlib=libphobos2.so -ofmain main.d tt.d
0x04 windows下示例细节
init.d代码:
module init;
import tt;
import test;
extern(C)
export TT init() {
return new Test();
}
import core.runtime;
import core.sys.windows.windows;
HINSTANCE g_hInst;
extern(Windows)
BOOL DllMain(HINSTANCE hInstance, ULONG ulReason, LPVOID pvReserved) {
switch(ulReason) {
case DLL_PROCESS_ATTACH:
Runtime.initialize();
break;
case DLL_PROCESS_DETACH:
Runtime.terminate();
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
default:
assert(false);
}
g_hInst = hInstance;
return true;
}
编译脚本:
dmd -shared -ofmydll.dll test.d init.d
dmd -ofmain main.d tt.d
编译的时候,并不需要加-fPIC,因为Windows下并不是使用PIC这种机制的,具体细节不属于本文内容,请自行查阅。
在官方wiki中,它提示还需要一个def文件才能完成编译,但是我发现,在init前添加export后,似乎没有def文件,也能跑起来。
DllMain是windows下动态链接库中必不可少的,它类似于注册了一个回调,当进程、线程创建或销毁时,都会调用相关代码。这里我们一个PROCESS的两个回调完成runtime的初始和终止,其中自动完成了gc_proxy的设置。而DllMain之前的extern(Windows)是必不可少的,还是和符号的格式有关。
0x05 既存问题
linux下和windows下都存在问题,而且都和异常有关。
在linux中,如果我们没有在main中捕获来自动态链接库中的异常,并且在main底部unload了库,那么就会出现段错误。理由是dlang自带的异常捕获是位于main之外的,而此时动态链接库已经被卸载了,自然访问来自动态链接库的异常就会出现段错误。
在windows中,我们无法捕获任何来自dll中的异常,均会出现错误,这个是由多个runtime实例造成的,属于Druntime的bug,而目前无解。顺便吐槽下,都2年了,还未fix。