动态链接库 for D

本文是动态链接库的第二版,前一版本只适用于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种情况:

  1. D程序 + C动态链接库,不需要动态链接phobos
  2. 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。

说两句: