Emscripten 「module.cwrap is not a function」エラーではまる

2020/10/15

C++ の class を Emscripten で WebAssembly に変換して JavaScript の class に変換するのを書いてみようと思い、以下のブログ記事を参考に github で公開されていたサンプルを動かしてみたんですが、「Uncaught TypeError: module.cwrap is not a function」というエラーではまりました。Emscripten のバージョンは 2.0.6 です。

http://blog.shogonir.jp/entry/2017/05/21/032648

とりあえず、エラー名でググってみると、stack overflow でコンパイルオプションに以下のオプションを付与しないといけないという情報を発見しました。

-s "EXTRA_EXPORTED_RUNTIME_METHODS=['cwrap']"

https://stackoverflow.com/questions/56040210/uncaught-typeerror-module-cwrap-is-not-a-function

確かにサンプルのコンパイルスクリプトを覗いてみるとこのオプションがなかったので付与してコンパイル、実行し直しました。しかし、エラーは直らず。

デバッグしてみると、以下の Module の onRuntimeInitialized 内で module に cwrap が定義されていないようで undefined となっていました。

fetch('share.wasm')
  .then(response => response.arrayBuffer())
  .then(buffer => new Uint8Array(buffer))
  .then(binary => {
    module = Module({
      wasmBinary: binary,
      onRuntimeInitialized: () => {
        var angleToPoint = module.cwrap('angleToPoint', 'number', ['number', 'number']);
        var freePoint = module.cwrap('freePoint', null, ['number']);
        var pointer = angleToPoint(30);
        var point = pointerToPoint(pointer);
        console.log(point);
        freePoint(pointer);
      }
    });
  });

onRuntimeInitialized が呼ばれたタイミングで module 変数は Promise になっていました。そこで、以下のようにサンプルを書き換えてみたところ、エラーが発生しなくなり、サンプルが動作するようになりました。

fetch('share.wasm')
  .then(response => response.arrayBuffer())
  .then(buffer => new Uint8Array(buffer))
  .then(binary => {
    Module({
      wasmBinary: binary,
    }).then(result =>{
      module = result;
      var angleToPoint = module.cwrap('angleToPoint', 'number', ['number', 'number']);
      var freePoint = module.cwrap('freePoint', null, ['number']);
      var pointer = angleToPoint(30);
      var point = pointerToPoint(pointer);
      console.log(point);
      freePoint(pointer);
    });
  });

ただ、これって onRuntimeInitialized 無視してるので、たまたま成功してるだけで場合によっては失敗するのでは?と思い、以下のようにした方がいいのかよく分からなくなりました。

fetch('share.wasm')
  .then(response => response.arrayBuffer())
  .then(buffer => new Uint8Array(buffer))
  .then(binary => {
    var promise = Module({
      wasmBinary: binary,
      onRuntimeInitialized: () => {
        promise.then(result => {
          module = result;
          var angleToPoint = module.cwrap('angleToPoint', 'number', ['number', 'number']);
          var freePoint = module.cwrap('freePoint', null, ['number']);
          var pointer = angleToPoint(30);
          var point = pointerToPoint(pointer);
          console.log(point);
          freePoint(pointer);
        });
      }
    });
  });

ちょっと調べてみると、Emscripten の FAQ (https://emscripten.org/docs/getting_started/FAQ.html)に以下の記述が。

Another option is to use the MODULARIZE option, using -s MODULARIZE=1. That puts all of the generated JavaScript into a factory function, which you can call to create an instance of your module. The factory function returns a Promise that resolves with the module instance. The promise is resolved once it’s safe to call the compiled code, i.e. after the compiled code has been downloaded and instantiated. For example, if you build with -s MODULARIZE=1 -s 'EXPORT_NAME="createMyModule"', then you can do this:

createMyModule(/* optional default settings */).then(function(Module) {
  // this is reached when everything is ready, and you can call methods on Module
});

つまりは、MODULARIZE オプションを有効にしている場合は、onRuntimeInitilized は使わずに、この Promise が解決されたら安全にコンパイル済みのコードを呼べるよってことみたいです。

参考にしたサンプルはもともと MODULARIZE オプションは有効になっていたけど、JavaScript 側は onRuntimeInitialized() で初期化済みを検知していて、コンパイルオプションと実装がちぐはぐになっていたようです(ただのミスか、昔のバージョンと今のバージョンで挙動が違ったとかかもしれません)。ということで、最終的には以下のような記述に変更するので問題なさそうですね。

fetch('share.wasm')
  .then(response => response.arrayBuffer())
  .then(buffer => new Uint8Array(buffer))
  .then(binary => {
    Module({
      wasmBinary: binary,
    }).then(result =>{
      module = result;
      var angleToPoint = module.cwrap('angleToPoint', 'number', ['number', 'number']);
      var freePoint = module.cwrap('freePoint', null, ['number']);
      var pointer = angleToPoint(30);
      var point = pointerToPoint(pointer);
      console.log(point);
      freePoint(pointer);
    });
  });

  このエントリーをはてなブックマークに追加    

<<「Web 開発」の記事一覧に戻る

関連記事