nptでDLLファイルを読み込む
1. はじめに
nptのWindows版で、DLLを読み込む機能を作りました。
FFIでは無いので注意。
npt用にDLLを作成すると読み込めるというものです。
【追記】soも作りました! nptでsoファイルを読み込む - nptclのブログ
npt for Windowsは、なんとなくうまく動いているように見えます。
さらにDLLを呼び出せるようにできれば、
Windowsで簡単な仕事をさせることができるのではと思いました。
もうちょっとウィンドウの操作をちゃんと作ればいいような気がしています。
履歴とかコピーとか、あとプロンプトの操作もアップデートして行きたいです。
2. DLLの作成
まずはDLL作成から。
下記のファイルを用意してください。
npt/develop/dlfile/
lispdl.c
lispdl.h
これらのファイルを用いて、DLLを作成していきます。
Visual Studioでもgccでもなんでも良いので、
DllMain
を作成できる環境を用意して下さい。
注意点を先に示しますが、「呼出規約」をnpt本体と合わせる必要があります。
Windows上では、だいたいcdecl
とstdcall
の二種類の呼出規約が混ざって存在しています。
呼出規約ってなんなのって話ですが、
関数を呼ぶときと呼ばれるときにどういう動きにするかを
CPUの動作レベルで決めたインターフェイスの事です。
ちょっと詳しく説明します。
Microsoftが提供しているCコンパイラはCL.EXE
だと思うのですが、
このコンパイラで何も指定をしなかった場合はcdecl
が使用されます。
バイナリで配布しているnpt32.exe
とnpt64.exe
もcdecl
です。
nptがcdecl
ならDLLで公開する関数もcdecl
にしてください。
ネットを色々見ると、DLLの関数はみんなstdcall
か
あるいはCALLBACK
みたいなマクロを用いています。
そうする理由は、Windowsが標準で提供してるAPIが全部stdcall
だからです。
CALLBACK
とかWINAPI
などのマクロも確か全部stdcall
のはず。
だからDLLはstdcall
しか作れないのかとばかり思ってました。
どうもそんなことはないようです。
cdecl
で公開してください。
それが嫌ならば、npt本体をstdcall
で作成し直してください。
(もしかしたらnptをstdcall
で作るの無理かも)
cdecl
とstdcall
は一見して似通っているので
適当に設定しても動くことがあります。
でも、これがとんでもないバグというか、問題を引き起こします。
絶対に合わせましょう。
ついでに言っておきますが、ビット数も合わせてください。
nptが64bitなら、DLLも64bitでコンパイルする必要があります。
それでは、簡単な例を示します。
#include "lispdl.h" #include <Windows.h> BOOL WINAPI DllMain(HINSTANCE hInst, DWORD fdwReason, LPVOID lpReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: case DLL_PROCESS_DETACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: return TRUE; } } __declspec(dllexport) int __cdecl lisp_dlfile_main(lisp_dlfile_array ptr) { return lisp_dlfile_update(ptr); }
DllMain
関数は何もしてないので説明は不要かと思います。
lisp_dlfile_main
関数は、npt
が真っ先に呼び出す初期化用の関数です。
呼出規約を__cdecl
で指定しています。
先ほど説明した通り、npt本体がstdcall
の場合は、__stdcall
を指定してください。
この関数でやることは、lisp_dlfile_update
関数を呼び出すだけです。
lisp_dlfile_update
関数は、呼び出し元のnpt
環境から色んな機能をDLL内部に持ってくる命令です。
後で説明しますが、npt
はDLLを読み込むと
まずはlisp_dlfile_main
関数を呼び出そうとします。
【参考】以前はlisp_dlfile_main
ではなく、lisp_dllmain
という名前でした。
【参考】いちおうlisp_dllmain
でも、今のところ動きます。
これでDLLを作成する準備ができました。
なにか作ってみましょう。
#include
しているlispdl.h
は、
amalgamationが提供するlisp.h
のDLLバージョンなので、
nptの説明書に記載されている方法で開発できます。
単に"Hello"
という文字列を返却する関数hello_
を作ってみます。
公開する関数は、必ず脱出関数にしてください。
__declspec(dllexport) int __cdecl 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); }
順に説明します。
__declspec(dllexport)
は、DLLで公開するための修飾。
int
は、脱出関数なので返却値がint
。
__cdecl
は呼出規約。
hello_
は関数名であり、何でもよいです。
(addr rest)
の引数は、addr
を一つにしてください。
引数はいくつかの形から選ぶことができるのですが、
ここではあんまり説明しません。
作成したhello_
関数の内容は、だいたい次のような感じになります。
(defun hello_ (&rest rest) "Hello")
あと今更ですが__declspec(dllexport)
ではなく
defファイルを用意する方法もあるらしいです。
詳しくは知りません。
以上でDLLファイルが作成できるはずです。
ここではaaa.dll
という名前で作成したことにして話を進めます。
3. nptで実行
npt for Windowsを起動してください。
たぶんコマンドプロンプトのnptでも問題ないです。
あと、同じディレクトリにaaa.dll
をコピーしてください。
まずは操作が面倒なのでnpt-system
パッケージをuse
します。
* (use-package 'npt)
最近、npt-system
にnpt
というnicknameを付けました。
けっこう便利です。
DLLを読み込むには、dlfile
関数を使用します。
dlfile
の第一引数にopen
を指定します。
* (dlfile 'open "aaa.dll") ;; ★注意 #<PAPER 0 ...>
dlfile
のopen
は、指定したDLLファイルを読み込みます。
DLLファイルと認識できたら、最初にlisp_dlfile_main
関数を探して呼び出します。
もしlisp_dlfile_main
関数が存在しなかった場合は例外が発生します。
またlisp_dlfile_main
関数を実行した結果、0以外が返却された場合も例外です。
全てが成功した場合は、PAPERオブジェクトが返却されます。
この返却されたオブジェクトがないと何もできませんので、 何かの変数に代入してください。
* (setq x (dlfile 'open "aaa.dll")) #<PAPER 0 ...>
DLLから関数を呼び出すためには、まずは関数を探します。
次のように実行してください。
* (setq y (dlfile 'call x "hello_")) #<PAPER 0 ...>
DLL内にhello_
という関数が存在しなかった場合は例外が発生します。
成功した場合は、PAPERオブジェクトが返却されます。
open
の時と同様、何かの変数に格納してください。
注意してほしいのは、返却値は関数ではないということです。
呼出しを行う場合は、dlcall
関数を使用します。
* (dlcall y) "Hello"
DLLで作成したコードが実行されたことが分かります。
DLLを開放する処理もあります。
dlfile
にclose
を指定してください。
* (dlfile 'close x)
これでDLLは解放されます。
このときdlcall
でy
を呼び出すと例外が発生します。
dlfile
とdlcall
によって返却されたオブジェクトは、
そのプロセス限りの使い捨てだということを覚えておいてください。
返却値はPAPERオブジェクトなのでcore
ファイルやfasl
ファイルで保存できます。
しかし保存されたものを使用しても正しく動作はしませんし、
おそらくはプロセスが壊れます。
3. 引数の形
後で仕様を変更するかもしれないので暫定ですが、引数の形を指定できます。
標準ではextend-rest
という型であり、
dlcall
に渡された引数がすべてhello_
の第一引数に指定されます。
例えば、
* (dlcall y 10 20 30)
として呼び出した場合は、DLL関数の
__declspec(dllexport) int __cdecl hello_(addr rest) { ... }
引数rest
に、(10 20 30)
のコピーが渡されます。
本当はコピーではなく、内部のデータをそのまま渡したかったのですが、
lisp.h
の関数がdynamic-extentのデータに対応してなかったので、
仕方なくコピーしました。
暫定と言っているのはこういう部分です。
変更した例を示します。
まずはDLL関数から。
__declspec(dllexport) int __cdecl arg2_(addr x, addr y) { if (lisp_stdout8_("Value1: ~S~%", x, NULL)) return 1; if (lisp_stdout8_("Value2: ~S~%", y, NULL)) return 1; lisp_set_result_control(Lisp_nil()); return 0; }
この関数を取得するときに、extend-var2
引数を指定します。
* (setq x (dlfile 'open "aaa.dll")) * (setq y (dlfile 'call x "arg2_" 'extend-var2))
実行は次の通り。
* (dlcall y 10 20) Value1: 10 Value2: 20 NIL
extend-var2
は、引数が2つでないとエラーが発生します。
型にどのような種類があるのかは、process_calltype.c
に書かれていますが、
extend-rest
という名前も不格好であり後で変えるかもしれません。