©️

node-ffi-napiが何をしているかをざっくり把握する

2023/09/25に公開

背景

ネイティブコードをNode.jsから利用したい場合がある(Node.jsで提供されていないOS機能を利用するなど)。
この場合、N-APIをエントリーポイントにしたネイティブコードを記述して、node-gypでビルドすることでNode.jsから呼び出せるようになる。

しかし、すでにビルド済みのライブラリを使いたい場合はこの手段は取れない。
そこで登場するのがnode-ffi-napiである。

node-ffi-napiは、内部でlibffiをラップしたネイティブコードを抱えており、これを使って、外部ライブラリを呼び出すことができる。
今回は、コードを読んで挙動を確認する。

ffi.Libraryを実行したとき、node-ffi-napiは何をしているか

  1. ライブラリファイルをdlopenする

エントリーポイントはlibrary.jsである。
まず、引数で指定したライブラリファイルを読み込んで、DynamicLibraryオブジェクトを作成する。
その内部処理はdynamic_library.jsを参照。
node-ffi-napiは、それ自体がネイティブアドオンを抱えており、dlfcn.hのdlopenなどをバインドしている。
これを使って、引数に指定したライブラリファイルをdlopenして関数をメモリに展開する。

  1. ライブラリに含まれる関数のポインタを探す

作成したDynamicLibraryとdlfcn.hのdlsymを使って、引数で指定した各関数の関数ポインタを取得する。

  1. Node.jsとネイティブコードの間の型のマッピングを行う

続いて、ref-napicoerceType関数を使って、引数と戻り値について、Node.jsとネイティブコードの間の型のマッピングを行う。

  1. CIFを作成する

続けて、bindings.ffi_prep_cifを呼び出してCIFを作成する。
CIFとは、libffiのffi_prep_cifで作成されるもので、実際の呼び出し(ffi_call)の前処理として呼び出して作成する必要のあるものである。

  1. 実行可能なJS関数を作成して呼び出し元に返す

これらを揃えてForeignFunction関数を呼び出すと、内部ではbindings.ffi_callが呼び出され、アプリには実行可能なJS関数が返されて利用できるようになる。


補足

dlfcn.h とは

ダイナミックローディングをサポートするライブラリ。
以下のような機能を提供する。

  • dlopen: 指定された動的ライブラリをロードする
  • dlsym: ロードされたライブラリから指定されたシンボル(関数や変数など)のアドレスを取得する
  • dlclose: dlopenでロードされたライブラリをアンロードする

ref-napi とは

https://github.com/node-ffi-napi/ref-napi

refとは、Cのポインタ型を扱うためのライブラリ。
例えば、char*型の値をメモリアクセスして取得し、JSのBufferに詰め込み、最終的にstring型に変換して返す、ということができる。
元々のrefは、N-APIを使わず、Node.jsが提供するV8 JavaScriptエンジンとlibuvのAPIを直接使用してC++でのバインディングを作成していたが、現在はN-APIを使うものが使用されるようになっているのでref-napiという名称になっている。
refはrefTypeのインターフェースと一部の型について定義する。これをffiのCIFの作成に利用する。
refが全ての型のrefTypeの定義をサポートしているわけではない。例えばwchar_t型の場合はref-wchar-napiというライブラリで、wchar_tのrefTypeを定義し、それをffi-napiに渡すことができる。

読み込んだライブラリファイルのライフサイクル

ライブラリファイルをdlopenした場合、dlcloseするか、親のプロセスが落ちるかする限りは生きる。
ここで、dlcloseは呼ばれないので、親のプロセス(=ffiを呼び出したNode.jsのプロセス)と共に残り続ける。
また、dlopenを複数回呼んでも、ライブラリがメモリ空間に複数作られることはない。
ただしリファレンスカウンタが増えるなどの副作用があるし、ffi.Libraryを呼び出すオーバーヘッドもあるので、呼び出し側でシングルトンで実装する方が無難である。

Discussion