🪝

Wasm を扱う Custom Hooks を書く

2023/12/26に公開

概要

React で Wasm を扱う簡単な Custom Hooks を書く. なお, Wasm は Rust コードを wasmbuild でビルドしたものを使用する.

環境

  • React 18.1.0
  • Deno 1.38.5
  • wasmbuild 0.15.4
  • rustc 1.70.0

Custom Hooks の概要

wasmbuild は <crate-name>_bg.wasm とそのバインディングである <crate-name>.generated.js を生成する. クレート名が wasm のとき, これらは以下のようにして使用できる.

import { instantiate } from 'wasm.generated.js';

const exports = await instantiate({
  url: new URL('wasm_bg.wasm', location.origin),
});
const { add } = exports;

console.log(add(1, 2)); // 3

この処理を Context で隠蔽し, Custom Hooks として使用できるようにする.

Custom Hooks の実装

Context Provider として WasmProvider を, Custom Hooks として useWasm を実装する. Context の型は, wasm ファイルの読み込み状況とその状況に応じてエクスポートされたオブジェクトまたはエラーを持つ WasmState とする. また, WasmProvidersrc として wasm ファイルの URL を受け取るようにする.

import { instantiate } from 'wasm.generated.js';

const WasmLoadState = {
   Ready: 'ready',
   Loading: 'loading',
   Error: 'error',
} as const;
type WasmLoadState = (typeof WasmLoadState)[keyof typeof WasmLoadState];
type WasmExports = Awaited<ReturnType<typeof instantiate>>;
type WasmState = {
   loadState: typeof WasmLoadState.Loading;
} | {
   loadState: typeof WasmLoadState.Error;
   error: Error;
} | {
   loadState: typeof WasmLoadState.Ready;
   exports: WasmExports;
};
type WasmProviderProps = {
   src: string;
   children: ReactNode;
};

const WasmProvider: FC<WasmProviderProps> = ({ src, children }) => {
   ...
};

instantiate がエクスポートされたオブジェクトを返す非同期関数であるため, 上記のように AwaitedReturnTypeWasmExports を定義できる.

WasmProviderinstantiate の状況に合わせて WasmState を更新し, それを WasmContext に渡せばよいので, 以下のように実装できる.

const initialWasmState: WasmState = {
   loadState: WasmLoadState.Loading,
};
const WasmContext = createContext<WasmState>(initialWasmState);

const WasmProvider: FC<WasmProviderProps> = ({ src, children }) => {
   const [wasmState, setWasmState] = useState<WasmState>(initialWasmState);

   useEffect(() => {
      (async () => {
         try {
            const exports = await instantiate({
               url: new URL(src),
            });
            setWasmState({
               loadState: WasmLoadState.Ready,
               exports,
            });
         } catch (e) {
            setWasmState({
               loadState: WasmLoadState.Error,
               error: e,
            });
         }
      })();
   }, [src]);

   return (
      <WasmContext.Provider value={wasmState}>
         {children}
      </WasmContext.Provider>
   );
};

useWasmWasmContext を使用して WasmState を取得するだけである.

const useWasm = () => useContext(WasmContext);

全ての実装は以下のようになる. (長いので畳んでおく)

useWasm.tsx
import React, {
   createContext,
   FC,
   ReactNode,
   useContext,
   useEffect,
   useState,
} from 'react';
import { instantiate } from 'wasm';

const WasmLoadState = {
   Ready: 'ready',
   Loading: 'loading',
   Error: 'error',
} as const;
type WasmLoadState = (typeof WasmLoadState)[keyof typeof WasmLoadState];
type WasmProviderProps = {
   src: string;
   children: ReactNode;
};
type WasmExports = Awaited<ReturnType<typeof instantiate>>;
type WasmState = {
   loadState: typeof WasmLoadState.Loading;
} | {
   loadState: typeof WasmLoadState.Error;
   error: Error;
} | {
   loadState: typeof WasmLoadState.Ready;
   exports: WasmExports;
};

const initialWasmState: WasmState = {
   loadState: WasmLoadState.Loading,
};
const WasmContext = createContext<WasmState>(initialWasmState);

const WasmProvider: FC<WasmProviderProps> = ({ src, children }) => {
   const [wasmState, setWasmState] = useState<WasmState>(initialWasmState);

   useEffect(() => {
      (async () => {
         try {
            const exports = await instantiate({
               url: new URL(src),
            });
            setWasmState({
               loadState: WasmLoadState.Ready,
               exports,
            });
         } catch (e) {
            setWasmState({
               loadState: WasmLoadState.Error,
               error: e,
            });
         }
      })();
   }, [src]);

   return (
      <WasmContext.Provider value={wasmState}>
         {children}
      </WasmContext.Provider>
   );
};

const useWasm = () => useContext(WasmContext);

export { WasmLoadState, WasmProvider };
export default useWasm;

上記の実装のメリットとして

  • Context を使用しているため, 複数コンポーネントに渡って Wasm を使用する場合でも一度のインスタンス化で済む.
  • wasmbuild のグルーコードによる型付けの恩恵を受けられる.

ことが挙げられる.

使用例

以下のようにして使う.

App.tsx
import React from 'react';
import ComponentWithWasm from './ComponentWithWasm';
import { WasmProvider } from './useWasm';

const App = () => {
   return (
      <WasmProvider src='wasm_bg.wasm'>
         <ComponentWithWasm />
      </WasmProvider>
   );
};

export default App;
ComponentWithWasm.tsx
import React from 'react';
import useWasm, { WasmLoadState } from './useWasm';

const ComponentWithWasm = () => {
   const wasm = useWasm();

   return (
      <div>
         {wasm.loadState === WasmLoadState.Loading && (
            <p>Loading...</p>
         )}
         {wasm.loadState === WasmLoadState.Error && (
            <p>Error: {wasm.error.message}</p>
         )}
         {wasm.loadState === WasmLoadState.Ready && (
            <p>1 + 2 = {wasm.exports.add(1, 2)}</p>
         )}
      </div>
   );
};

export default ComponentWithWasm;

最後に

wasmbuild の生成物を使用する Custom Hooks が見つからなかったため作成した. ローディングメッセージとエラーメッセージだけ表示すれば, 他は通常の関数と同様に扱えるため, 非常に便利である.

参考文献

GitHubで編集を提案

Discussion