cmake プロジェクトのライブラリを emscripten を使って nodejs とブラウザで動作する npm package にする
概要
すでにタイトルがなろう小説並に重たい。下記 2 エントリの続き物で今回ようやっと納得行くところまで作れたのでブログ記事にしておく。
- CMake の C++ プロジェクトを nodejs, TypeScript の世界に持っていく - フェルマータ
- Emscripten を使って C++ のコードを JavaScript の世界に持っていく - フェルマータ
やることはタイトルに書いてある通りなのだけれど詰みポイントが何箇所かあったのと、結構いろいろ知らないといけないことが多くて全部を書くととてもまとまらないので、結論を先に書いてから個人的にすごくつらかった wasm を webpack に読ませる箇所の回避方法を紹介する。
成果物
とりあえず今回の成果物。タイトル通りのことをやっているが、いかんせん C++ にせよ nodejs にせよ本職の範囲から離れているのでこれがいいやり方かは知らない。動いたからいいやくらいの精神の成果なので参考にする際はご注意を。
詰みポインツと回避方法
実は過去記事で nodejs から使うのはあっさりできていて、簡単だったのだけれど wasm と emscripten の glue コードを webpack (react-app)から読むのが大変だった。大した情報もネット上になかったのでこの問題を解決するのにだいぶ時間がかかった。問題は大まかに2つ。
- emscripten の作成する glue コード(js ファイル)が wasm ファイルに依存しているので webpack で wasm を配信できるようにする必要がある
- emscripten の作成する glue コードそのままだと webpack で配信する wasm ファイルのパスを渡せない
だいたい glue コードのせい。
wasm を webpack で配信する
react-app を利用していたので webpack の設定をいじるために cracoを利用した。 wasm ファイルをみたら javascript/auto
で配信してね、と emscripten の glue コードに nodejs 用の path
, fs
を利用するコードが含まれているので無視するように設定している。
module.exports = { webpack: { configure:{ // See https://github.com/webpack/webpack/issues/6725 module:{ rules: [{ test: /\.wasm$/, type: 'javascript/auto', }] }, resolve:{ fallback: { "path": false, "fs": false } } } } };
wasm が配信できないのはこれで解決。ただしこれだけでもダメで、 webpack が配信する際に wasm ファイルの名前を変えてしまうので emscripten の glue コードにファイル名を渡す必要がある。
glue コードに配信している wasm のファイル名を渡す
なかなか発狂ものなのだが react-app から emscripten の glue コードを直接使っている場合単純にこれができない。なので emscripten のコンパイル時に pre.js
, post.js
によしなな JavaScript を書いてファイル名を渡せるようにする。
- pre.js
var uzumeInitJsPromise = undefined; var factory = function (moduleConfig) { if (uzumeInitJsPromise){ return uzumeInitJsPromise; } uzumeInitJsPromise = new Promise(function (resolveModule, reject) { var Module = typeof moduleConfig !== 'undefined' ? moduleConfig : {}; var originalOnAbortFunction = Module['onAbort']; Module['onAbort'] = function (errorThatCausedAbort) { reject(new Error(errorThatCausedAbort)); if (originalOnAbortFunction){ originalOnAbortFunction(errorThatCausedAbort); } }; Module['postRun'] = Module['postRun'] || []; Module['postRun'].push(function () { resolveModule(Module); }); module = undefined;
- post.js
return Module; }); return uzumeInitJsPromise; } if (typeof exports === 'object' && typeof module === 'object'){ module.exports = factory; module.exports.default = factory; } else if (typeof define === 'function' && define['amd']) { define([], function() { return factory; }); } else if (typeof exports === 'object'){ exports["Module"] = factory; }
もともと emscripten には本当は Module.locateFile
を上書きして wasm のファイル名を解決する方法があるのだがブラウザビルドで利用できないので無理やりこじあけておく。その上で下記のように wasm のファイル名を渡せばよい。
// Required to let webpack 4 know it needs to copy the wasm file to our assets // @ts-ignore // eslint-disable-next-line import/no-webpack-loader-syntax import uzumejsWasm from "!!file-loader?name=uzumewasm-[contenthash].wasm!uzumejs/resources/uzumewasm.wasm"; ...(中略)... const u = await uzume({ locateFile: () => uzumejsWasm });
終わりに
先人の挑戦がなければ無理でした。 nodejs, webpack 初心者にわからんよこんなの…下記の sql.js のコードを見てなんとか理解した感じがある。ここまでやる必要があるし、その上でパッケージ利用者に負担がかかるのか、と悲しい気持ちになりました。とりあえず Rust でやればいいんじゃね?という気持ちになりました。まあ C++, C のコードは TypeScript の世界で動かしたい放題にはなったのでそれはよかったということにしておきます。