nptclのブログ

Common Lisp処理系nptの開発メモです。https://github.com/nptcl/npt

nptでsoファイルを読み込む

前回nptでDLLファイルを読み込む - nptclのブログの続きです。
前の投稿ではnptにDLLファイルを読み込ませましたが、 soファイルもやりたいな、ということでやりました。

個人的にはsoファイルの作成は初めてなので、 いろいろと勉強になりました。

1. nptのコンパイルから

nptのコンパイルの方法が変わります。
次のオプションをつけてください。

リンク時は-ldlが必要になるかと思います。
コンパイルスクリプトは修正して普通に利用できるようにしています。
利用可能かどうかは次のようにして調べます。

$ npt --version-script | grep dynamic-link
dynamic-link    true

2. soファイルの作成

それではsoファイルを作りましょう。
やり方はDLLと同じですが、詳しく書いていきます。

下記のファイルを用意してください。

  • npt/develop/dlfile/
    • lispdl.c
    • lispdl.h

ひな形を示します。

#include "lispdl.h"

int lisp_dlfile_main(lisp_dlfile_array ptr)
{
    return lisp_dlfile_update(ptr);
}

もし初期化と解放処理が必要な場合は、次のようにするそうです。

#include "lispdl.h"

void init(void) __attribute__((constructor));
void fini(void) __attribute__((destructor));

void init(void)
{
    /* 初期化 */
}

void fini(void)
{
    /* 解放 */
}

int lisp_dlfile_main(lisp_dlfile_array ptr)
{
    return lisp_dlfile_update(ptr);
}

lisp_dlfile_main関数は、nptが真っ先に呼び出す初期化用の関数です。
この関数でやることは、lisp_dlfile_update関数を呼び出すだけです。
lisp_dlfile_update関数は、呼び出し元のnpt環境から色んな機能をso内部に持ってくる命令です。
nptはsoファイルを読み込むと まずはlisp_dlfile_main関数を呼び出そうとします。

#includeしているlispdl.hは、 amalgamationが提供するlisp.hのsoバージョンなので、 nptの説明書に記載されている方法で開発できます。
単に"Hello"という文字列を返却する関数hello_を作ってみます。
公開する関数は、必ず脱出関数にしてください。

#include "lispdl.h"

int lisp_dlfile_main(lisp_dlfile_array ptr)
{
    return lisp_dlfile_update(ptr);
}

int hello_(addr rest)
{
    addr control, x;

    lisp_push_control(&control);
    x = Lisp_hold();
    lisp_string8_(x, "Hello");
    lisp_set_result_control(x);

    return lisp_pop_control_(control);
}

終わりです。
ではコンパイルをしましょう。
上記のファイルがaaa.cであるとします。

$ cc -fPIC -shared -o aaa.so lispdl.c aaa.c 

いろいろと知らない引数が出てきました。
-fPICは再配置可能なコードを出すんだそうです。
-fpicもあるらしいですが、上記のように大文字にしておけば安全です。
-sharedは、soファイル出力用だそうです。
lispdl.cは忘れないでくださいね。
忘れると、後でなんでdlfileで失敗するんだって延々と悩むことになります。

成功すれば、aaa.soファイルが出力されます。

3. nptで実行

nptを起動してください。
同じディレクトリにaaa.soをコピーしてください。

まずは操作が面倒なのでnpt-systemパッケージをuseします。

* (use-package 'npt)

soを読み込むには、dlfile関数を使用します。
dlfileの第一引数にopenを指定します。

* (setq x (dlfile 'open "./aaa.so"))
#<PAPER 0 ...>

Windows版とは違い、"aaa.so"ではなく、 "./aaa.so"のようにしないと失敗しました。
そういえば大昔はカレントディレクト.PATHに入ってたの知ってますか?
セキュリティホールになったので廃止されたんです。
それを思い出してしまいました。

あとはWindows版と同じです。

* (setq y (dlfile 'call x "hello_"))
#<PAPER 0 ...>
* (dlcall y)
"Hello"

soの開放は次の通り。

* (dlfile 'close x)

4. その他の機能

DLLとsoのどちらも共通した話のなのですが、 openして放置したらそれはリークです。
例えばこんな感じ。

* (dlfile 'open "./aaa.so")
#<PAPER 0 #x80174ee28>
* (dlfile 'open "./aaa.so")
#<PAPER 0 #x80174f8d8>
* (dlfile 'open "./aaa.so")
#<PAPER 0 #x801750388>
* (dlfile 'open "./aaa.so")
#<PAPER 0 #x801750e38>
*

こうなると、プロセスを終了させない限り aaa.soの残骸がメモリに残り続けるわけです。
それはちょっとどうかと思ったので、一応救済措置を設けました。
dlfilelistを指定して下さい。

* (dlfile 'list)
(#<PAPER 0 #x801750e38> #<PAPER 0 #x801750388> #<PAPER 0 #x80174f8d8>
 #<PAPER 0 #x80174ee28>)

こんな感じで、closeされていない、open中のdlfileの一覧が得られます。
もし(dlfile :close x)でcloseされた場合は、この一覧に出てきません。

リークに困っているなら、このリストにあるオブジェクトをcloseしていけばいいわけです。
それにしたって情報がなさすぎなので、 せめて何のファイルをopenしたのかくらいの情報を得る命令を追加しました。
dlfileinfo指定すると、引数の情報を出力します。

例えばこんな感じ。

* (setq x (dlfile 'open "./aaa.so"))
#<PAPER 0 #x801752dd0>
* (dlfile 'info x)
#P"./aaa.so"
T

第一返却値は、dlfileオブジェクトの場合はパス名を返却します。
第二返却値は、closeされていなかったらTを返却します。

何も考えずに全部閉じたい場合は次のようにしてください。

* (mapcar (lambda (x) (dlfile 'close x)) (dlfile 'list))
(T T T T T)

全部閉じた場合はこんな感じになります。

* (dlfile 'list)
NIL