node-ffi-napiが何をしているかをざっくり把握する
背景
ネイティブコードをNode.jsから利用したい場合がある(Node.jsで提供されていないOS機能を利用するなど)。
この場合、N-APIをエントリーポイントにしたネイティブコードを記述して、node-gypでビルドすることでNode.jsから呼び出せるようになる。
しかし、すでにビルド済みのライブラリを使いたい場合はこの手段は取れない。
そこで登場するのがnode-ffi-napiである。
node-ffi-napiは、内部でlibffiをラップしたネイティブコードを抱えており、これを使って、外部ライブラリを呼び出すことができる。
今回は、コードを読んで挙動を確認する。
ffi.Libraryを実行したとき、node-ffi-napiは何をしているか
const library = ffi.Library("/path/to/library.dll", {
addValue: ['int', ['int', 'int']],
}
ffi-napiのエントリーポイントはlibrary.jsである。
(1) ライブラリファイルをdlopenする
dl = new DynamicLibrary(libfile || null, RTLD_NOW);
引数で指定したライブラリファイルを読み込んで、dlopenを実行して関数をメモリに展開する。
node-ffi-napiは、それ自体がネイティブアドオンを抱えており、dlfcn.hのdlopenなどをバインドしている。
これを使って、引数に指定したライブラリファイルをdlopenして関数をメモリに展開する。
その情報を含んだ DynamicLibrary
オブジェクトを作成して返す。
(2) ライブラリに含まれる関数のポインタを探す
const fptr = dl.get(func);
作成したDynamicLibrary
とdlfcn.hのdlsymを使って、引数で指定した各関数の関数ポインタを取得する。
(3) Node.jsとネイティブコードの間の型のマッピングを行う
returnType = ref.coerceType(returnType);
argTypes = argTypes.map(ref.coerceType);
続いて、ref-napiの coerceType
関数を使って、引数と戻り値について、Node.jsとネイティブコードの間の型のマッピングを行う。
ffi.Libraryに渡した引数の ['int', ['int', 'int']]
がここで型オブジェクトに変換される。
(4) CIFを作成する
const cif = CIF(returnType, argTypes, abi);
続けてCIFを作成する。
CIFとは、実際の関数呼び出し(ffi_call)の前処理として呼び出して作成する必要のあるものである。
libffiのffi_prep_cif
をバインドしているので呼び出して作成する。
ffi_prep_cifには、引数の数・引数の型・戻り値の型を渡す。
「型」というのは1つ前のステップでrefが生成したTypeインターフェースの実装である。
(5) 呼び出し可能なJS関数を作成して呼び出し元に返す
return _ForeignFunction(cif, funcPtr, returnType, argTypes)
ffi.Libraryの呼び出し元にJS関数を返す。
これでネイティブコードを実行できる。
実行した時の内部処理は以下の通りとなる。
ffi_callを使ってネイティブコード内の関数を実行する→関数の戻り値のポインタがBufferに格納される→refを使ってポインタから値を取り出して返す。
補足
dlfcn.h
とは
ダイナミックローディングをサポートするライブラリ。
以下のような機能を提供する。
- dlopen: 指定された動的ライブラリをロードする
- dlsym: ロードされたライブラリから指定されたシンボル(関数や変数など)のアドレスを取得する
- dlclose: dlopenでロードされたライブラリをアンロードする
ref-napi とは
refとは、Node.jsでポインタを扱うためのライブラリ。
ffiの文脈ではネイティブコードとNode.jsでの型変換を担当する。
例えば、char*型の値をメモリアクセスして取得し、JSのBufferオブジェクトに詰め込み、最終的にstring型に変換して返す、ということができる。
refはrefTypeのインターフェースと一部の型について定義する。refTypeとは、ffiのCIFの作成に利用できるものである。
一方で、refが全ての型のrefTypeの定義をサポートしているわけではない。サポートしている型はこちらを参照。例えばwchar_t型の場合はref-wchar-napiというライブラリで、wchar_tのrefTypeを定義し、それをffi-napiに渡すことができる。
元々のrefは、N-APIを使わず、Node.jsが提供するV8 JavaScriptエンジンとlibuvのAPIを直接使用してC++でのバインディングを作成していたが、現在はN-APIを使うものが使用されるようになっているのでref-napiという名称になっている。
歴史的にはネイティブコードとNode.jsでの型変換はnode-ffiの中で行われていたが、より効率的に扱うことのできるrefライブラリに切り出された。
他の用途としては、Bufferに詰め込んだポインタが参照する変数を読み出したりできる。
読み込んだライブラリファイルのライフサイクル
ライブラリファイルをdlopenした場合、dlcloseするか、親のプロセスが落ちるかする限りは生きる。
ここで、dlcloseは呼ばれないので、親のプロセス(=ffiを呼び出したNode.jsのプロセス)と共に残り続ける。
また、dlopenを複数回呼んでも、ライブラリがメモリ空間に複数作られることはない。
ただしリファレンスカウンタが増えるなどの副作用があるし、ffi.Libraryを呼び出すオーバーヘッドもあるので、呼び出し側でシングルトンで実装する方が無難である。
Discussion