🌊

[環境構築]React+TypeScriptでWebAssembly005。C++,OpenCVでWasmビルドの開発環境を構築

2024/01/27に公開

<- React+TypeScriptでWebAssembly004
                    React+TypeScriptでWebAssembly006 ->


Abstract

やっと出来た!! Windows,OpenCV,C++,CMake,VSCode,WebAssermblyの開発環境。

①Windows,C++,OpenCVで開発→ ②Ubuntuでwasmビルド→ ③React+TypeScriptで開発&実行の順序で開発する。
とはいえ、[①Windows,C++,OpenCVで開発→ ②Ubuntuでwasmビルド]の部分は[環境構築]WindowsでC++開発->Ubuntuでwasmビルドの開発環境(OpenCV,C++,CMake)を構築してみた。を参照。
残りjs/wasmのReact+TypeScript組み込み環境を構築する。

結論

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

前提

手順

不要ファイルの削除

  • public/wasm/hello.wasm
  • src/asm-cpp/hello.cpp
  • src/asm-cpp/hello.js
    今回は使わないので削除
  • 下記コードも削除
App.tsx(3行目)
-  3: import { helloif } from './asm-cpp/helloif';

①WindowsでC++開発。

Windows, OpenCV, Cmake, VSCode。
C++はコミット済のを使うので割愛。

②Wasmビルド

Ubuntuでwasmビルド -> 動作確認まで実施する。

②-1. Ubuntuでテンプレートのソースコード一式を取得

ソースコード一式を取得
$ cd ~
$ git clone https://github.com/aaaa1597/cppwasm_template.git
$ mv cppwasm_template reacttscppwasm_tmplate
$ cd reacttscppwasm_tmplate

②-2. Emscriptenでwasmビルド。

srcをビルド
$ cd ~/reacttscppwasm_tmplate/wasm
$ mkdir build && cd build
$ emcmake cmake ..
$ emmake make
$ mv cppmain.js ..   # ← これが出来る。
$ mv cppmain.wasm .. # ← これが出来る。

②-3. Webカメラの設定(内蔵カメラならこの手順は不要)

②-4.pythonコマンドでサーバ起動->動作確認。

サーバ起動
$ cd ~/reacttscppwasm_tmplate/wasm
$ python3 -m http.server 8080
  • ブラウザから http://localhost:8080/ にアクセスする。

    Ubuntu環境でビルド成功!! 動作確認も成功!!

③React+TypeScriptで開発。

Windowsで。

③-0.テンプレートのソースコード一式を取得

適当にプロジェクトフォルダを作る。
$ cd D:\Products\React.js\
$ git clone https://github.com/aaaa1597/cppwasm_template.git
$ ren cppwasm_template reacttscppwasm_tmplate
$ cd D:\Products\React.js\reacttscppwasm_tmplate

③-1. npm installを実行。

適当にプロジェクトフォルダを作る。
$ cd D:\Products\React.js\reacttscppwasm_tmplate/reactts
$ npm install

③-2. VSCodeで開く。

reacttscppwasm_tmplate\reacttsフォルダで右クリック。

③-3.cppmain.jsの修正。

③-3.1 見やすくするためにcppmain.jsをフォーマット。

Prettier コードフォマッターをインストールして、

     ↓
Shift + Ctrl + P → Format Document (Forced)を実行

     ↓
いい感じに整形してくれる。

③-3.2 Moduleのエクスポート化

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

③-3.3 cppmain.wasmの読込み元を修正。

cppmain.wasmをpuclic\wasmに配置するのを前提に。

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

③-4. カメラ読込み処理の実装

カメラパラメータの定義

App.tsx(4-10行目)
+  4:const constraints: MediaStreamConstraints = {
+  5:  audio: false,
+  6:  video: {
+  7:    width:  640,
+  8:    height: 480,
+  9:  },
+ 10:};

カメラ入力をvideoタグで表示するための準備

App.tsx(20-22行目)
  20:function App() {
  21:  const refWasm = useRef<any>(null)
+ 22:  const videoRef = useRef<HTMLVideoElement>(null);

カメラ入力をvideoタグで表示するための準備2

App.tsx(36-37行目)
+ 36:  /* 初期化 */
+ 37:  useEffect(() => {
+ 38:    const openCamera = async () => {
+ 39:      const video = videoRef.current;
+ 40:      const canvas = canvasRef.current;
+ 41:      if (video && canvas) {
+ 42:        canvas.width = 640;
+ 43:        canvas.height = 480;
+ 44:        video.width = 640;
+ 45:        video.height = 480;
+ 46:        const stream = await navigator.mediaDevices.getUserMedia(constraints);
                   // ↑ここで、カメラ使用権限を求めて、
+ 47:        video.srcObject = stream;
                   // ↑取得したstreamをvideoタグに設定する
+ 48:        video.play();
+ 49:        ctxRef.current = canvas.getContext('2d');
+ 50:        loop();
+ 51:      }
+ 52:    };
+ 53:    openCamera();

カメラのためのHTMLElementを配置する。

App.tsx(77-83行目)
+ 77:  return (
+ 78:    <div className="App">
+ 79:      hello world!!
+ 80:      <video autoPlay playsInline={true} ref={videoRef} />
           ## ↑ videoタグ
+ 81:      <canvas width="640" height="480" ref={canvasRef} />
           ## ↑ canvasタグ videoタグの画像データを加工してここに表示する。
+ 82:    </div>
+ 83:  );

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

fsがない。pathがない。

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

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 }

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

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

③-7. App.tsxでcppmain.jsを読み込みを実装

cppmain.jsは、Emscriptenのビルド生成物だけど、App.tsxで読込みできるように、"③-2.2"でexport化したので読み込むことが出来る。

requireで読み込む様に修正。

App.tsx(21,28-34行目)
  21:  const refWasm = useRef<any>(null)

+ 28:  /* wasm読込み */
- 29:  const hWasm = require('./asm-cpp/hello.js');
+ 29:  const h = require('./cppmain.js');
  30:  h.Module.onRuntimeInitialized = () => {  // ← requireでのjs読込み完了通知
+ 31:    console.log("Wasm loaded.");
+ 32:    h.Module["canvas"] = canvasRef.current // cppmain.js内でもcanvasにいろいろやってるので設定。
  33:    refWasm.current = h.Module             // wasmハンドラを保持っとく
+ 34:  }

④. OpenCV実行処理を実装

cppmain.jsを読み込んで取得した、wasmのハンドラから自作のcpp関数を呼ぶ処理を実装する。

App.tsx(58-75行目)
+ 58:    /* ループ */
+ 59:    const loop = () => {
+ 60:      const ctx = ctxRef.current;
+ 61:      const video = videoRef.current;
+ 62:      const canvas = canvasRef.current;
+ 63:      const wasm = refWasm.current;
+ 64:      if(ctx && video && canvas && wasm) {
+ 65:        ctx.drawImage(video, 0, 0);
             //  ↑videoから画像を取得
+ 66:        const data = ctx.getImageData(0, 0, canvas.width, canvas.height);
             //  ↑取得したデータを取り出す
+ 67:        const buffer = wasm._creata_buffer(data.data.length);
             //  ↑自作関数を呼び出し
+ 68:        wasm.HEAPU8.set(data.data, buffer);
+ 69:        wasm._Convert(buffer, data.width, data.height, cnt);
             //  ↑自作関数を呼び出し
+ 70:        wasm._destroy_buffer(buffer);
             //  ↑自作関数を呼び出し
+ 71:        cnt++;
+ 72:        if(cnt>=200) cnt = 0;
+ 73:      }
+ 74:      loopRef.current = requestAnimationFrame(loop);
+ 75:    }

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

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",
    }

修正完了。

App.tsx全体

App.tsx
import React, { useEffect, useRef } from 'react';
import './App.css';

const constraints: MediaStreamConstraints = {
  audio: false,
  video: {
    width:  640,
    height: 480 ,
  },
 };

function App() {
  const refWasm = useRef<any>(null)
  const videoRef = useRef<HTMLVideoElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const ctxRef = useRef<CanvasRenderingContext2D|null>();
  const loopRef = useRef(0);
  let cnt = 0;

  /* wasm読込み */
  const h = require('./cppmain.js');
  h.Module.onRuntimeInitialized = () => {
    console.log("Wasm loaded.");
    h.Module["canvas"] = canvasRef.current
    refWasm.current = h.Module
  }
 
  /* 初期化 */
  useEffect(() => {
    const openCamera = async () => {
      const video = videoRef.current;
      const canvas = canvasRef.current;
      if (video && canvas) {
        canvas.width = 640;
        canvas.height = 480;
        video.width = 640;
        video.height = 480;
        const stream = await navigator.mediaDevices.getUserMedia(constraints);
        video.srcObject = stream;
        video.play();
        ctxRef.current = canvas.getContext('2d');
        loop();
      }
    };
    openCamera();

    return () => cancelAnimationFrame(loopRef.current);
  }, [])

  /* ループ */
  const loop = () => {
    const ctx = ctxRef.current;
    const video = videoRef.current;
    const canvas = canvasRef.current;
    const wasm = refWasm.current;
    if(ctx && video && canvas && wasm) {
      ctx.drawImage(video, 0, 0);
      const data = ctx.getImageData(0, 0, canvas.width, canvas.height);
      const buffer = wasm._creata_buffer(data.data.length);
      wasm.HEAPU8.set(data.data, buffer);
      wasm._Convert(buffer, data.width, data.height, cnt);
      wasm._destroy_buffer(buffer);
      cnt++;
      if(cnt>=200) cnt = 0;
    }
    loopRef.current = requestAnimationFrame(loop);
  }

  return (
    <div className="App">
      hello world!!
      <video autoPlay playsInline={true} ref={videoRef} />
      <canvas width="640" height="480" ref={canvasRef} />
    </div>
  );
}

export default App;
  1. 実行

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


<- React+TypeScriptでWebAssembly004
                    React+TypeScriptでWebAssembly006 ->

Discussion