ネイティブモジュールを書く理由
Yukimasa Funaoka氏(以下、Funaoka):まず、自己紹介をします。私は、株式会社ディー・エヌ・エーでフロントエンドの業務をしている、船岡と申します。
本日は、どういった目的でネイティブモジュールを書くのか、具体的にRustで作るにはどうすればよいのか、そしてクロスプラットフォーム対応と、実際に書いてみて困ったことなどを紹介したいと思っています。
まず、ネイティブモジュールを書く理由ですが、ネイティブモジュールは、JS(JavaScript)の世界を超えて、ネイティブのバイナリとNode(Node.js)との架け橋になって、実行できます。
ネイティブモジュールのメリットとして、まず処理速度が挙げられると思います。
最近はesbuildが話題ですが、以前からあるものだと画像処理ライブラリのsharpなども、速度を重視したネイティブモジュールとして使われています。
次に、移植性です。C++で書かれたライブラリだと、JSで書き直すとけっこう大変ですが、ネイティブモジュールであれば、簡単にNodeから使えるというメリットがあります。 node-sassは、内部的にはLibSassという、C++のライブラリを利用して書かれています。
最後に、シリアル通信のNode Serialportライブラリなど、OS準拠のネイティブからしか扱えない機能を利用できるというメリットがあります。
ほかには、GPUを使うケースや、JSから扱えない機能を使いたい時は、ネイティブモジュールを使う必要があります。
Rustでネイティブモジュールを書く方法
今回は、処理速度を上げるためのネイティブモジュールを書きました。実際のコードは出せませんが、代わりにサンプルを用意しています。RustのImageライブラリで、画像を読み込んで、rgbaのExcelデータを返すシンプルなものです。サンプルリポジトリと資料は、(スライドの)左下のURLにあります。
それでは実際に、napi-rsというライブラリを使って、Rustでネイティブモジュールを作っていきます。
以前のネイティブモジュールは、生のV8のコードを書く必要があり、Nodeのバージョンが変わるたびに使えなくなったりして、使いづらいものでした。
今では、名前が変わってNode-APIと呼んだりすることがありますが、これが後方互換のあるインターフェイスを実装しているため、すごく便利に使えようになりました。これをRustから作れるようにしたものが、napi-rsというライブラリです。
具体的には、ビルドや、各種ワークフローを実行するnapi/cli(Node.js)。Rustからnapiを使うためのcrate、Nodeでのパッケージに当たる、napiとnapi-deriveがあります。
シンプルな使い方としては、napi newを実行すると、initのように、さまざまなもののファイルが生成されます。このあたりはドキュメントを見れば、すぐにわかると思うので、今回は実際のコードでポイントになる部分を紹介したいと思います。
まず、Nodeのパッケージ実装に当たる、Cargo.tomlというファイルについて説明します。crate-typeは、ふだんRustではあまり書かないものだと思いますが、これを”cdylib”として動的リンクライブラリで、かつ他言語から扱えるようにビルドしています。
依存関係については、napiのバージョンのところで、featuresというオプションを指定できます。ここで、ターゲットになるnapiのバージョンや拡張機能を設定できます。
これについてはドキュメントがありますので、そちらを参照してください。
ライブラリ本体のコードには、napi_deriveにderiveという、#で書く部分の実装があります。これはjs_functionという関数定義で、括弧の数値は引数の数を示しています。実際に何個の引数で呼び出されたかは、コンテキストのlengthプロパティを参照することで確認できます。
もう1つが、module.exportsで、これはいわゆるJSでおなじみのmodule.exportsそのままです。wasmで書く時には、上のjs_functionを書くだけで済み、module.exportsを書く必要がありません。wasmで書く場合とnapiを使う場合ではちょっと違うため、忘れるという罠にハマりがちだと感じました。
あとは、napi buildというコマンドを叩けば、実際にNodeから使えるネイティブモジュールが完成します。基本的な作り方としては、以上です。
クロスプラットフォーム対応はどうするか
クロスプラットフォームの対応は、ネイティブモジュールはやはりネイティブバイナリなので、基本的にはビルドしたプラットフォームに依存します。例えば、Linuxの64bitでビルドしたものは、Linuxの64bitでしか使えません。
ただ、インストールのたびにビルドするのは時間がかかるし、そもそもビルド環境が整っていなければインストールすらできないこともあります。実際にnpmを実装した時に、エラーでこけるケースを体験された方も多いのではないでしょうか。
これに対する対処として、バイナリの配布があります。全部突っ込むというのは、node_modulesが太ってしまうし、あまりよくありません。
postinstallなどで、インストールする時にダウンロードする方法もありますが、ローカルでレジストリを運用していたり、インターネットを使ってはいけない環境だったりで、インストールできない場合があります。
optionalDependenciesを利用する方法
そのほかに、optionalDependenciesを利用する方法がありますので、今回はこれを使いたいと思います。optionalDependenciesはインストールを試みて、失敗したらスキップするという挙動をします。
そのため、各プラットフォーム用のパッケージを並べておくことで、別のプラットフォーム用のパッケージがすべてスキップされて、今のプラットフォーム用のパッケージだけがインストールされる挙動になります。
wasm版にfallbackする方法
ただ、これだけでは十分ではなくて、想定外のプラットフォームからインストールされることも考えられます。これに対しては、諦めてインストール時にビルドするという手もありますが、そのほかにwasmを用意しておいて、これにfallbackする方法もあります。ネイティブの機能を使う場合はできませんが、今回は後者で対応することにしました。
これをwasmで書くことで、ブラウザーで動くライブラリにできるので一石二鳥です。ネイティブに比べて速度面では劣りますが、ここは妥協します。
wasmの書き方については、今回Rustの話ということで割愛します。ただ、具体的にこれを実現するためには、ライブラリ本体とnapi用とwasm用の、3つのcrateを用意するのがシンプルです。
napiやwasmから、ライブラリ本体のcrateを呼び出す場合は、バージョンを書くところに相対パスでパスを指定します。それによってローカルのcrateが作成できるので、分割できます。
Rustで書いて困ったこと
今回、napiをRustで書く際のnapi-rsの使い方、書き方がわからないため、とても困りました。サンプルコードがないし、なんならテストすらないし。一部のメソッドは、GitHub全体でコード検索しても、使用例が数えるほどしかないという、すごく悲しい状況になっています。
今回は、仕方なくソースコードを見ながら実装しましたが、napiを初めて触る人だと、けっこう大変かもしれないなと感じました。
crateがnot foundというエラーになってしまって、テストが書けないという問題も発生しました。これはcrate_typeをcdylibにしたことが原因で、Rust用のcrate_typeがない場合は、rustcが定義を見つけられないため、crateが見つからないというエラーになってしまいます。
これについてはRust用のライブラリのデフォルトである、rlibというcrate_typeを追加することで解決できます。
最後に、Error: Module did not self-registerというエラー。これはNode.jsからrequireした時に出るエラーですが、ググるとまったく別の要因のものばかり出てきて、すごくわかりづらいため、特定するのに非常に時間がかかりました。
これは先ほども述べましたが、module_exportsが足りていない場合に発生するエラーです。同じエラーでハマった人は、ぜひこれを参考にしてもらえればと思います。
最後にまとめです。Rustでネイティブモジュールを作るのは、現状けっこう大変だと感じました。C++で書くnapiは事例もあって、サンプルコードもそこらへんに転がっていてすごく楽なので、今書くのであればまだC++のほうがちょっと楽だと感じました。
ただ、最初に言ったように、ネイティブモジュールのメリットは非常に大きいものがあります。機会があればぜひ書いてみてもらいたいと思います。
以上で発表を終わります。ありがとうございました。
質疑応答
司会者:ありがとうございました。まだRustでnapiを書くこと自体を、やられている人は少ないのでしょうね。C++は、公式ドキュメントとかに、内容がありますが。
Funaoka:そうですね。
司会者:でも、非常にチャレンジングなことをされていておもしろかったです。もし機会があれば、Rustでネイティブバインドしたやつと、WebAssemblyにしたやつで、それぞれ試した時にどちらがどれだけいいとか、ビルド時間はどっちが速いかなども含めて、検証できるとおもしろいかもなと思いました。というか、やってみたいなと思いました(笑)。
Funaoka:そうですね。iswasmfastを見るとどっちが速いか、みたいなことをやっていますが、場合によるって感じですね。
司会者:実行時間でどっちが速いかも気になりますが、ビルド時間とかも含めると、どっちが速いのかなとかも気になっています。ありがとうございました。
Funaoka:ありがとうございました。