🎃

React+TypeScriptで、Emscripten製jsモジュールを実行してみた

2024/01/23に公開

Abstract

前回の記事では、App.tsxから直接wasmを読込んで実行した。
けど、規模が大きいプロブラムだと、この方法は相当めんどい。
で、実は、Emscriptenはビルドするとき*.jsも一緒に出力してくれてて、それを使わない手はない。全然楽。
ということで、今回は、Emscripten製jsを、TypeScriptで読み込んで実行してみた。

結論

今回の成果物はココ↓
https://github.com/aaaa1597/ReactTs-WebAsm004

前提

手順

0. 不要ファイルの削除

  • src/asm-cpp/helloif.d.ts
  • src/asm-cpp/helloif.js
    今回は使わないので削除
App.tsx(3行目)
-  3: import { helloif } from './asm-cpp/helloif';

今回は使わないので削除

1. Emscripten製jsの読込み

1-1. Emscriptenでビルド出力したhello.jsのオブジェクトModuleをexportにする。

hello.js(15行目)
- 15: var Module = typeof Module != 'undefined' ? Module : {};
+ 15: export var Module = typeof Module != 'undefined' ? Module : {};

1-2. hello.wasmの配置場所に修正。

hello.js(715行目)
- 715:   wasmBinaryFile = 'hello.wasm';
+ 715:   wasmBinaryFile = 'wasm/hello.wasm';

1-3. App.tsxでhello.jsを読み込む

App.tsx(3行目)
-  3: import { helloif } from './asm-cpp/helloif';
+  3: import * as hWasm from './asm-cpp/hello';
   4:
   5: function App() {
   6:   const [count, setCount] = useState(0)
   7: 
+  8:   console.log("hWasm.Module add---", hWasm.Module.__Z3addii(11, 22));
+  9:   console.log("hWasm.Module sub---", hWasm.Module.__Z3subii(11, 22));

1-4. この状態で実行すると下記エラーがでる!!

fsがない。pathがない。

Compiled with problems:
ERROR in ./src/asm-cpp/hello.js 75:11-24
Module not found: Error: Can't resolve 'fs' in 'D:\Products\React.js\ReactTs-WebAsm004\src\asm-cpp'
ERROR in ./src/asm-cpp/hello.js 76:17-32
Module not found: Error: Can't resolve 'path' in 'D:\Products\React.js\ReactTs-WebAsm004\src\asm-cpp'

BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.
This is no longer the case. Verify if you need this module and configure a polyfill for it.

If you want to include a polyfill, you need to:
	- add a fallback 'resolve.fallback: { "path": require.resolve("path-browserify") }'
	- install 'path-browserify'
If you don't want to include a polyfill, you can use an empty module like this:
	resolve.fallback: { "path": false }

1-5. エラー解決のためpackage.jsonに下記を追加

たぶん、osは余計な気がする。

package.json
  "dependencies": {
  ~略~
  },
+  "browser": {
+    "fs": false,
+    "os": false,
+    "path": false
+  },

1-6. この状態で実行すると下記エラーがでる!!

hWasm.Module初期化前に、_Z3addiiを呼んでる!!

Aborted(Assertion failed: native function `_Z3addii` called before runtime initialization)
RuntimeError: Aborted(Assertion failed: native function `_Z3addii` called before runtime initialization)
    at abort (http://localhost:3000/static/js/bundle.js:704:11)
    at assert (http://localhost:3000/static/js/bundle.js:450:5)
    at Object.__Z3addii (http://localhost:3000/static/js/bundle.js:768:5)
    at App (http://localhost:3000/static/js/bundle.js:32:86)
    at renderWithHooks (http://localhost:3000/static/js/bundle.js:21386:22)
    at mountIndeterminateComponent (http://localhost:3000/static/js/bundle.js:24670:17)
    at beginWork (http://localhost:3000/static/js/bundle.js:25966:20)
    at HTMLUnknownElement.callCallback (http://localhost:3000/static/js/bundle.js:10982:18)
    at Object.invokeGuardedCallbackDev (http://localhost:3000/static/js/bundle.js:11026:20)
    at invokeGuardedCallback (http://localhost:3000/static/js/bundle.js:11083:35)

1-7. App.tsxで読み込む

requireで読み込む様に修正。

App.tsx(3行目)
-  1: import React, { useState } from 'react';  // useState()は使わない。
-  1: import React, { useRef } from 'react';    // useRef()を使う。
-  3: import * as hWasm from './asm-cpp/hello'; // 追加したけど削除。requireで読込む。
   3: 
   4: function App() {
-  5: const [count, setCount] = useState(0)  // useState()は使わない。
+  5: const refWasm = useRef<any>()          // useRef()を使う。
   6: 
+  7: const hWasm = require('./asm-cpp/hello.js');
+  8: hWasm.Module.onRuntimeInitialized = () => {
+  9:   console.log("hWasm.Module add---", hWasm.Module.__Z3addii(11, 22));
+ 10:   console.log("hWasm.Module sub---", hWasm.Module.__Z3subii(11, 22));
+ 11:   refWasm.current = hWasm.Module
+ 12: }

7行目のrequireで、Emscripten製jsモジュール(hello.js)を読み込む。
8行目の関数で、初期化完了を待って、戻りをrefWasmに設定する。以降は、refWasmで操作できるようになる。

2. この状態で実行すると下記エラーがでる!!

eslintエラーs

eslintエラー
ERROR in [eslint]
src\App.tsx
  Line 7:16:  Require statement not part of import statement  @typescript-eslint/no-var-requires

src\asm-cpp\hello.js
  Line 80:12:    Require statement not part of import statement     @typescript-eslint/no-var-requires
  Line 81:18:    Require statement not part of import statement     @typescript-eslint/no-var-requires
  Line 145:13:   'read' is not defined                              no-undef
  Line 150:29:   'readbuffer' is not defined                        no-undef
  Line 152:16:   'read' is not defined                              no-undef
  Line 162:39:   Unexpected empty arrow function                    @typescript-eslint/no-empty-function
  Line 171:18:   'scriptArgs' is not defined                        no-undef
  Line 173:18:   'arguments' is not defined                         no-undef
  Line 195:9:    'quit' is not defined                              no-undef
  Line 203:40:   Read-only global 'console' should not be modified  no-global-assign
  Line 205:125:  'printErr' is not defined                          no-undef
  Line 567:10:   Unexpected constant condition                      no-constant-condition
  Line 890:7:    Expected to return a value in method 'get'         getter-return
  Line 966:7:    Expected to return a value in method 'get'         getter-return
  Line 1015:7:   Expected a 'break' statement before 'case'         no-fallthrough
  Line 1045:7:   Expected a 'break' statement before 'case'         no-fallthrough

eslintが出力するエラーを無視する。

2-1. Require statement not part of import statement @typescript-eslint/no-var-requires

.eslintrc.js
    "rules": {
+       "@typescript-eslint/no-var-requires": "off",
    }

2-2. 'xxx' is not defined     no-undef

.eslintrc.js
+	"globals": {
+		"read": false,
+		"readbuffer": false,
+		"scriptArgs": false,
+		"arguments": false,
+		"quit": false,
+		"printErr": false,
+	},
    "rules": {

2-3. Unexpected empty arrow function @typescript-eslint/no-empty-function

.eslintrc.js
    "rules": {
       "@typescript-eslint/no-var-requires": "off",
+      "@typescript-eslint/no-empty-function": "off",
    }

2-4. Read-only global 'console' should not be modified no-global-assign

.eslintrc.js
    "rules": {
       "@typescript-eslint/no-var-requires": "off",
       "@typescript-eslint/no-empty-function": "off",
+      "no-global-assign": "off",
    }

2-5. Unexpected constant condition     no-constant-condition

.eslintrc.js
    "rules": {
       "@typescript-eslint/no-var-requires": "off",
       "@typescript-eslint/no-empty-function": "off",
       "no-global-assign": "off",
+      "no-constant-condition": ["error", { "checkLoops": false }],
    }

2-6. Expected to return a value in method 'get'   getter-return

.eslintrc.js
    "rules": {
       "@typescript-eslint/no-var-requires": "off",
       "@typescript-eslint/no-empty-function": "off",
       "no-global-assign": "off",
       "no-constant-condition": ["error", { "checkLoops": false }],
+      "getter-return": "off",
    }

2-7. Expected a 'break' statement before 'case' no-fallthrough

.eslintrc.js
    "rules": {
       "@typescript-eslint/no-var-requires": "off",
       "@typescript-eslint/no-empty-function": "off",
       "no-global-assign": "off",
       "no-constant-condition": ["error", { "checkLoops": false }],
       "getter-return": "off",
+      "no-fallthrough": "off",
    }

修正完了。

  1. 実行

    出来た!!
    見た目地味だけど、WebAssemblyが実現できてるぞ!!
    Wasm.ModuleのAny型になるので、扱いにくいかな~と思ったけど、そうでもなくって。
    型定期ファイル(.d.ts)使うのは、困ってからでいいや。

次は、OpenCVのWebAssemblyをやる!!


function addRunDependency(id) {
runDependencies++;
Module["monitorRunDependencies"] ? .(runDependencies);
if (id) {
assert(!runDependencyTracking[id]);
runDependencyTracking[id] = 1;
if (runDependencyWatcher === null && typeof setInterval != "undefined") {
runDependencyWatcher = setInterval(() => {
if (ABORT) {
clearInterval(runDependencyWatcher);
runDependencyWatcher = null;
return
}
var shown = false;
for (var dep in runDependencyTracking) {
if (!shown) {
shown = true;
err("still waiting on run dependencies:")
}
err(dependency: ${dep})
}
if (shown) {
err("(end of list)")
}
}, 1e4)
}
} else {
err("warning: run dependency added without ID")
}
}

Discussion