最近更新: 2010-11-25

Vala - load dynamic library / shared object

Vala 有許多途徑可以整合 C 語言程式,例如在《Vala 程式語言入門》中,我示範了在源碼層級上整合 Vala 程式碼與 C 程式碼。除此之外,我們更常面臨的情況則是要在 Vala 程式碼中調用二進位碼函數庫的內容。在這方面, Vala 也提供了適當的支持。Vala 可以透過連結或是動態載入的方式連結函數庫,調用函數庫的內容。這也讓我們易於整合既有的 C 語言函數庫。

本文首先以 C 語言撰寫一個 md5sum() 並將其存入一個動態函數庫(dynamic library, Unix界的傳統說法稱為 shared object)。再分別透過動態連結以及動態載入兩種途徑,于 Vala 程式碼中調用此函數。

建立撰寫 md5sum 函數庫

首先,我以 C 語言撰寫一個 md5sum(),並建立一個儲存這個函數的動態函數庫,命名為 md5sum (在 Unix/Linux 中,這個動態函數庫的檔名慣例是 libmd5sum.so;在 Windows 中則是 md5sum.dll)。

#include <stdio.h>
#include <openssl/md5.h>

unsigned char *
md5sum(const unsigned char *d, unsigned long len, unsigned char *md)
{
    printf("d = [%s], len = [%lu]\n", d, len);
    unsigned char bufstr[MD5_DIGEST_LENGTH * 2];
    unsigned char buf[MD5_DIGEST_LENGTH];
    
    MD5(d, len, buf);

    int i;
    unsigned char *p1, *p2;
    for (i = 0, p1 = buf, p2 = md; 
        i < MD5_DIGEST_LENGTH; 
        ++i, ++p1, p2+=2) 
    {
        sprintf(p2, "%02x", *p1);
    }
    *p2 = '\0';
    //printf("%s\n", md);

    return md;
}

接著編譯並產生 libmd5sum.so 。在 Ubuntu 系統中,它需要額外連結 crypto 函數庫。

$ make libmd5sum.so
gcc -shared -fPIC -lcrypto -o libmd5sum.so md5sum.c

$ export LD_LIBRARY_PATH=.

Linux 系統預設儲放與尋找函數庫的路徑列表是 /usr/lib 以及 /lib (細節請看 manpage ldconfig)。不論是編譯時的連結動作,亦或是執行時的載入動作,預設情形都是往前述的路徑列表中尋找函數庫。但我不打算將剛剛產生的 libmd5sum.so 放在系統指定路徑下。所以我使用環境變數 LD_LIBRARY_PATH 指定額外的函數庫搜尋路徑為當前路徑。接下來的範例皆沿用此設定。

使用動態連結的方式調用 md5sum

在現代作業系統中,作業系統提供了一層透明的動態連結機制。透過這個機制,暗中幫我們處理了執行時才載入函數庫以及分配符號位址的複雜工作。這使得我們不用修改任何程式碼,僅需透過編譯動作,就能使用動態連結機制調用動態函數庫中的函數。

link_md5sum.vala 是一個調用 md5sum() 計算字串雜湊值的範例程式。編譯器可以靜態連結 md5sum(),也可以動態連結 md5sum()。差別僅在編譯器的參數,而不需修改程式源碼。

const uint MD5_DIGEST_STRING_LENGTH = 32;

// link extern symbolic
[CCode (cname="md5sum")]
static extern unowned uchar* md5sum(uchar *d, ulong len, uchar *md);

void main() {
    var s = "hello 石頭成";
    var buf = s.to_utf8();
    var mdbuf = new uchar[MD5_DIGEST_STRING_LENGTH + 1];

    stdout.printf("1[%s]\n", (string) md5sum(buf, buf.length, mdbuf) );
    stdout.printf("2[%s]\n", (string) mdbuf);
}

在《Vala 程式語言入門》中示範的編譯動作,就是透過靜態連結的方式調用 md5sum()。如果將 valac 的參數改為 -X -lmd5sum 並略去 md5usm.c 這個成員檔,最後就會由 gcc 透過動態連結的方式,連結 libmd5sum.so 這個動態函數庫。不需修改任何程式源碼。編譯與執行結果如下所示:

$ make link_md5sum
valac -X -lcrypto -X -L. -X -lmd5sum -o link_md5sum link_md5sum.vala

$ ./link_md5sum 
d = [hello 石頭成], len = [15]
1[19d63df7a195f5a3a847bd3a54831b97]
2[19d63df7a195f5a3a847bd3a54831b97]

如果執行程式時,系統回應找不到 libmd5sum.so (libmd5sum.so: cannot open shared object file: No such file or directory),這表示你並未使用環境變數 LD_LIBRARY_PATH 指定額外的函數庫搜尋路徑。系統並不會主動在當前路徑中尋找動態函數庫。

使用動態載入的方式調用 md5sum

POSIX dlopen

Unix 系統很久以前就實現了動態連結與載入機制,而相關的 API 則在大約十年前被規範於 POSIX 之中。Linux 傳統上就使用這套 API 提供動態連結與載入能力。關於動態連結載入器的 API 文件,請看 manpage dlopen。

本節示範 Vala 如何透過 POSIX 的動態連結載入 API,調用動態函數庫。由於動態連結載入 API 被定義在 dl 函數庫中,所以這個動作本身就是前一節方法的應用。我將應用前一節說明的方法,連結 dl 函數庫的 API。再透過這些 API 載入 libmd5sum.so 。

本節的範例程式碼 dlopen_md5sum.vala ,內容如下:

// POSIX dlopen functions.
//#include <dlfcn.h>
[CCode (cname="dlopen")]
extern unowned void *dlopen(string filename, int flag);
[CCode (cname="dlerror")]
extern unowned string dlerror();
[CCode (cname="dlsym")]
extern unowned void *dlsym(void *handle, string symbol);

const int RTLD_LAZY = 0x00001; /* Lazy function call binding. */
// end POSIX dlopen functions.

//#include <openssl/md5.h>
const uint MD5_DIGEST_LENGTH = 16;
const uint MD5_DIGEST_STRING_LENGTH = MD5_DIGEST_LENGTH * 2;

// dynamic load from shared library
delegate unowned uchar*Chksum(uchar *d, ulong len, uchar *result);
Chksum md5sum = null;

int main(string[] args) {
    // 動態載入時,程序員要自行處理函數庫的載入工作以及符號的指派工作。
    // begin load and link
    string md5sum_lib_path = 
        "%s/lib%s.so".printf(Path.get_dirname(args[0]), "md5sum");
                            // 共享函數庫與程式同目錄
    stdout.printf("Load %s\n", md5sum_lib_path);

    var module = dlopen(md5sum_lib_path, RTLD_LAZY);
    if (module == null) {
        stdout.printf("error: %s\n", dlerror());
        return 1;
    }

    md5sum = (Chksum) dlsym(module, "md5sum");
    if (md5sum == null) {
        stdout.printf("error: %s\n", dlerror());
        return 1;
    }
    // end load and link

    var s = "hello 石頭成";
    var buf = s.to_utf8();
    var mdbuf = new uchar[MD5_DIGEST_STRING_LENGTH + 1];

    stdout.printf("1[%s]\n", (string) md5sum(buf, buf.length, mdbuf) );
    stdout.printf("2[%s]\n", (string) mdbuf);
    return 0;
}

首先,我先按照 POSIX 規範的動態連結載入 API 內容(man dlopen),用 Vala 語法重新宣告 dlopen(), dlsym() 等函數。接著我使用 Vala 的委派語法,宣告一個 Chksum 委派型態,內容參考 md5sum() 的介面。以此委派型態,定義一個 md5sum 委派變數。接著調用 dlopen() 載入 libmd5sum.so ,將函數庫中的 md5sum() 指派給 md5sum。最後就可以透過這個委派變數調用實際的 md5sum()函數。

當程序員試圖使用動態載入方法時,程序員要自行處理函數庫載入工作以及符號的指派工作。從前兩節的範例程式碼中,我們不難看出作業系統那一層透明的動態連結機制,暗中幫程序員處理了多少事。

編譯與執行結果如下所示:

$ make dlopen_md5sum
valac -X -ldl -o dlopen_md5sum dlopen_md5sum.vala

$ ./dlopen_md5sum 
Load ./libmd5sum.so
d = [hello 石頭成], len = [15]
1[19d63df7a195f5a3a847bd3a54831b97]
2[19d63df7a195f5a3a847bd3a54831b97]

GLib gmodule

GLib 提供了一個 gmodule 函數庫,簡化程序員動態載入函數庫的處理工作(在Linux中,gmodule 底層實際就是 POSIX dlopen functions)。而 Vala 已經提供整合 GLib gmodule 的 Module 元件,所以在 Vala 中可以直接使用 Module 元件載入動態函數庫。這就不需要像上一節那樣,還得自己再宣告一次 POSIX dlopen 的介面。

本節的範例程式碼 gmodule_open_md5sum.vala ,內容如下:

using Module; //--pkg gmodule-2.0

const uint MD5_DIGEST_LENGTH = 16;
const uint MD5_DIGEST_STRING_LENGTH = MD5_DIGEST_LENGTH * 2;

// dynamic load from shared library
delegate unowned uchar*Chksum(uchar *d, ulong len, uchar *result);
Chksum md5sum = null;

int main(string[] args) {
    // 動態載入時,程序員要自行處理函數庫的載入工作以及符號的指派工作。
    // begin load and link
    string md5sum_lib_path = 
        Module.build_path(Path.get_dirname(args[0]), "md5sum");
                          // 共享函數庫與程式同目錄
    stdout.printf("Load %s\n", md5sum_lib_path);

    var module = Module.open(md5sum_lib_path, ModuleFlags.BIND_LAZY);
    if (module == null) {
        stdout.printf("error: %s\n", Module.error());
        return 1;
    }
    
    void *func_point = null;
    if (module.symbol("md5sum", out func_point) != true) {
        stdout.printf("error: %s\n", Module.error());
        return 1;
    }
    md5sum = (Chksum) func_point;
    // end load and link

    var s = "hello 石頭成";
    var buf = s.to_utf8();
    var mdbuf = new uchar[MD5_DIGEST_STRING_LENGTH + 1];

    stdout.printf("1[%s]\n", (string) md5sum(buf, buf.length, mdbuf) );
    stdout.printf("2[%s]\n", (string) mdbuf);
    return 0;
}

Module 元件的使用方法與 dlopen 的差異不大,基本上就是把 dlopen() 換成 Module.open()dlsym() 換成 Module.symbol()。此外,由於我們使用的是純粹的 Vala 元件,所以編譯時的參數也不需要額外傳遞給 gcc 的選項。編譯與執行結果如下所示:

$ make gmodule_open_md5sum
valac --pkg gmodule-2.0 -o gmodule_open_md5sum gmodule_open_md5sum.vala

$ ./gmodule_open_md5sum 
Load ./libmd5sum.so
d = [hello 石頭成], len = [15]
1[19d63df7a195f5a3a847bd3a54831b97]
2[19d63df7a195f5a3a847bd3a54831b97]

參考資源

編譯動態函數庫以及連接它的方式,牽涉到許多編譯器參數。為了簡化操作動作,本文所範例的編譯動作我都編在一份 Makefile。下列即為本文的 Makefile 內容。


libmd5sum.so: md5sum.c
	gcc -shared -fPIC -lcrypto -o $@ $?

link_md5sum: link_md5sum.vala libmd5sum.so
	valac -X -lcrypto -X -L. -X -lmd5sum -o $@ $<

dlopen_md5sum: dlopen_md5sum.vala libmd5sum.so
	valac -X -ldl -o $@ $<

gmodule_open_md5sum: gmodule_open_md5sum.vala libmd5sum.so
	valac --pkg gmodule-2.0 -o $@ $<

c_dlopen_md5sum: c_dlopen_md5sum.c libmd5sum.so
	gcc -ldl -o $@ $<

clean:
	rm -f libmd5sum.so link_md5sum dlopen_md5sum gmodule_open_md5sum c_dlopen_md5sum

如果你想要將 C 語言撰寫的函數庫轉為一個正式的 Vala 元件,那麼你需要準備一份 Vala API 文件(.vapi)。請參考 Vala Bindings

對 C 語言呼叫 dlopen() 的方法有興趣的讀者,下列為 C 語言範例。此範例與 dlopen_md5sum.vala 的用途相同。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/md5.h>

#include <dlfcn.h>
// POSIX dlopen functions.

typedef unsigned char uchar;

int main(void) {
    // 動態載入時,程序員要自行處理函數庫的載入工作以及符號的指派工作。
    // begin load and link
    void *module = NULL;
    uchar *(*chksum)(const uchar*, unsigned long, uchar*) = NULL;

    module = dlopen("./libmd5sum.so", RTLD_LAZY);
    if ( !module ) {
        printf("error: %s\n", dlerror());
        return 1;
    }

    chksum = dlsym(module, "md5sum");

    if ( !chksum ) {
        printf("error: %s\n", dlerror());
        return 1;
    }
    // end load and link

    uchar *s = "hello 石頭成";
    uchar mdbuf[MD5_DIGEST_LENGTH * 2 + 1];

    printf("1[%s]\n", chksum(s, strlen(s), mdbuf) );
    printf("2[%s]\n", mdbuf);
    return 0;
}

$ make c_dlopen_md5sum
gcc -ldl -o c_dlopen_md5sum c_dlopen_md5sum.c

樂多舊網址: http://blog.roodo.com/rocksaying/archives/14551049.html

樂多舊回應
未留名 (#comment-21420371)
Thu, 25 Nov 2010 19:56:05 +0800
非常感謝!!每次閱讀您的文章總是受益匪淺!
原本不知道關於 POSIX 的 Dynamic Linker 機制,今天又上了一課。