Nuxt3でwasmを実行する

computer program language text NuxtJS
Photo by Jorge Jesus on Pexels.com
この記事は約14分で読めます。

Webブラウザ上でネイティブコードを実行する仕組みにWebAssembly(wasm)という仕組みがあります。高度な画像処理などの計算をネイティブで高速に実行することでアプリケーションのパフォーマンスが向上しユーザー体験の向上が期待できます。

WebAssembly | MDN
WebAssembly は現代のウェブブラウザーで実行できるコードの一種です。ネイティブに近いパフォーマンスで動作する、コンパクトなバイナリー形式の低レベルなアセンブリー風言語です。さらに、 C/C++、C# や Rust などの言語のコン...

Nuxt3にはexperimental機能でありますがデフォルトでwasmをロードするための仕組みが用意されています。今回はこのwasmロード機能を使って、nuxtのプログラム内でwasmのプログラムを実行する方法を見ていきます。

環境

  • macOS Catalina 10.15.7
  • emcc 3.1.24
  • Nuxt 3.2.3

Nuxt3でのwasmのロード

Nuxt3(nitro)内部ではrollupのwasmプラグインを使用しています。拡張子がwasmのファイルのimport宣言を検出するとrollupによりwasmファイルのloadが実行されコードがtransformされます。

@rollup/plugin-wasm

rollupのplugin-wasmは、wasmのロードをへルパーコードに置換します。4つのtargetがあり、それぞれ以下のようになっています。

node nodejs想定、Base64からのデコード。
auto auto-inline、node、browserの混合(デフォルト)
auto-inline nodeとbrowserの混合
browser ブラウザ想定、fetchまたはBase64からのデコード。

参考 https://github.com/rollup/plugins/blob/master/packages/wasm/src/helper.ts

Nuxt3での設定

nuxt.config.tsには、rollupのplugin-wasmのオプションを指定することができます。nuxt3の公式の例ではboolean型のtrueを指定しています。trueはwasmロードを有効化するだけでrollupには無害な値となっています。

export default defineNuxtConfig({
  nitro: {
    experimental: {
      wasm: true, // RollupWasmOptionsの指定が可能
    },
  },
});

実際に指定できるオプションには以下があります。

sync

同期的にロードするwasmファイル
デフォルト:空の配列

maxFileSize

インライン生成の最大サイズ
デフォルト:14 * 1024

publicPath

maxFileSizeを超えた場合にwasmをコピーするファイル場所
デフォルト:空文字

targetEnv

wasm実行環境のターゲット
デフォルト:auto

fileName

ファイル名のフォーマット 
デフォルト:[hash][extname]

参考 https://github.com/rollup/plugins/tree/master/packages/wasm/#readme

Nuxt3でWASMを実行してみる

簡単な四則演算を実行するだけのプログラムを実行してみます。

wasmのビルド

まずは、Cのコードです。EMSCRIPTEN_KEEYALIVEマクロを指定するとwasmのexport対象と認識されます。

#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
int add(int a, int b)
{
  return a + b;
}

EMSCRIPTEN_KEEPALIVE
int sub(int a, int b)
{
  return a - b;
}

EMSCRIPTEN_KEEPALIVE
int mult(int a, int b)
{
  return a * b;
}

EMSCRIPTEN_KEEPALIVE
int div(int a, int b)
{
  return a / b;
}

今回はemccコマンドでStandaloneモードでビルドします。

emcc -O3 calc.c -o calc.wasm --no-entry

--no-entryがないと以下のようなエラーになります。メッセージを読むと、Standaloneモードのwasmでは--no-entryを使用するようにとあります。

error: undefined symbol: main/__main_argc_argv (referenced by top-level compiled C/C++ code)
warning: Link with -sLLD_REPORT_UNDEFINED to get more information on undefined symbols
warning: To disable errors for undefined symbols use -sERROR_ON_UNDEFINED_SYMBOLS=0
warning: _main may need to be added to EXPORTED_FUNCTIONS if it arrives from a system library
warning: To build in STANDALONE_WASM mode without a main(), use emcc --no-entry
Error: Aborting compilation due to previous errors

生成されたwasmフィアるをテキスト表現形式で表示してみます。emscriptenの補助関数はなくStandaloneモードになっているようです。サイズは292byteです。

$ wasm2wat calc.wasm 
(module
  (type (;0;) (func (param i32 i32) (result i32)))
  (type (;1;) (func (result i32)))
  (type (;2;) (func))
  (type (;3;) (func (param i32)))
  (type (;4;) (func (param i32) (result i32)))
  (func (;0;) (type 2)
    nop)
  (func (;1;) (type 0) (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add)
  (func (;2;) (type 0) (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.sub)
  (func (;3;) (type 0) (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.mul)
  (func (;4;) (type 0) (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.div_s)
  (func (;5;) (type 1) (result i32)
    global.get 0)
  (func (;6;) (type 3) (param i32)
    local.get 0
    global.set 0)
  (func (;7;) (type 4) (param i32) (result i32)
    global.get 0
    local.get 0
    i32.sub
    i32.const -16
    i32.and
    local.tee 0
    global.set 0
    local.get 0)
  (func (;8;) (type 1) (result i32)
    i32.const 1024)
  (table (;0;) 2 2 funcref)
  (memory (;0;) 256 256)
  (global (;0;) (mut i32) (i32.const 5243920))
  (export "memory" (memory 0))
  (export "add" (func 1))
  (export "sub" (func 2))
  (export "mult" (func 3))
  (export "div" (func 4))
  (export "__indirect_function_table" (table 0))
  (export "_initialize" (func 0))
  (export "__errno_location" (func 8))
  (export "stackSave" (func 5))
  (export "stackRestore" (func 6))
  (export "stackAlloc" (func 7))
  (elem (;0;) (i32.const 1) func 0))

nuxt

続いてnuxt側のコードです。

configは以下のようにします。

export default defineNuxtConfig({
  nitro: {
    experimental: {
      wasm: true, // 機能をonにする
    },
  },
  modules: ["@nuxt/ui"],
});

クライアントは計算に使用する2値をクエリで渡してfetch関数でapiを呼びます。サーバapiではクライアントから受け取った値を元に計算を行ない結果をjsonで返します。以下のnuxtのサンプルプログラムと同様な形です。

参考:https://github.com/nuxt/framework/tree/main/examples/experimental/wasm

server/api/calc.ts

importでwasmファイルを呼んでいます。この結果はrollupによりtransformされます。

import { defineLazyEventHandler } from "h3";

export default defineLazyEventHandler(async () => {
  const {
    exports: { add, sub, mul, div },
  } = await loadWasmInstance(
    // @ts-ignore
    () => import("~/server/wasm/calc.wasm"),
  );

  return (event) => {
    const { a, b } = getQuery(event);
    return { add: add(a, b), sub: sub(a, b), mul: mul(a, b), div: div(a, b) };
  };
});

async function loadWasmInstance(importFn: any, imports = {}) {
  const init = await importFn().then((m: any) => m.default || m);
  const { instance } = await init(imports);
  return instance;
}

.nuxt/dev/index.mjsを見ると以下のようなコンパイル後のコードを確認できます。wasmプログラムはbase64でエンコードされているようです。

function _loadWasmModule (sync, filepath, src, imports) {
  function _instantiateOrCompile(source, imports, stream) {
    var instantiateFunc = stream ? WebAssembly.instantiateStreaming : WebAssembly.instantiate;
    var compileFunc = stream ? WebAssembly.compileStreaming : WebAssembly.compile;

    if (imports) {
      return instantiateFunc(source, imports)
    } else {
      return compileFunc(source)
    }
  }

  
var buf = null;
var isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;

if (filepath && isNode) {
  
var fs = require("fs");
var path = require("path");

return new Promise((resolve, reject) => {
  fs.readFile(path.resolve(__dirname, filepath), (error, buffer) => {
    if (error != null) {
      reject(error);
    } else {
      resolve(_instantiateOrCompile(buffer, imports, false));
    }
  });
});

} else if (filepath) {
  
return _instantiateOrCompile(fetch(filepath), imports, true);

}

if (isNode) {
  
buf = Buffer.from(src, 'base64');

} else {
  
var raw = globalThis.atob(src);
var rawLength = raw.length;
buf = new Uint8Array(new ArrayBuffer(rawLength));
for(var i = 0; i < rawLength; i++) {
   buf[i] = raw.charCodeAt(i);
}

}


  if(sync) {
    var mod = new WebAssembly.Module(buf);
    return imports ? new WebAssembly.Instance(mod, imports) : mod
  } else {
    return _instantiateOrCompile(buf, imports, false)
  }
}

function calc(imports){return _loadWasmModule(0, null, 'AGFzbQEAAAABFwVgAn9/AX9gAAF/YAAAYAF/AGABfwF/AwoJAgAAAAABAwQBBAUBcAECAgUGAQGAAoACBgkBfwFBkIjAAgsHhwELBm1lbW9yeQIAA2FkZAABA3N1YgACA211bAADA2RpdgAEGV9faW5kaXJlY3RfZnVuY3Rpb25fdGFibGUBAAtfaW5pdGlhbGl6ZQAAEF9fZXJybm9fbG9jYXRpb24ACAlzdGFja1NhdmUABQxzdGFja1Jlc3RvcmUABgpzdGFja0FsbG9jAAcJBwEAQQELAQAKSAkDAAELBwAgACABagsHACAAIAFrCwcAIAAgAWwLBwAgACABbQsEACMACwYAIAAkAAsQACMAIABrQXBxIgAkACAACwUAQYAICw==', imports)}

const calc$1 = /*#__PURE__*/Object.freeze({
  __proto__: null,
  default: calc
});

app.vue

app.vueでは、fetchでapiを呼び計算結果を受け取り表示します。

<script setup>
const a = ref(200);
const b = ref(100);

const { data } = await useAsyncData("calc", () =>
  $fetch("/api/calc", { params: { a: a.value, b: b.value } }),
);
</script>

<template>
  <NuxtExampleLayout example="experimental/wasm">
    <p>
      <code>a = 200</code>
    </p>
    <p>
      <code>b = 100</code>
    </p>
    <p>
      Computation performed server-side with WASM :
      <br />
      <code>{{ a }} + {{ b }} = {{ data.add }}</code
      ><br />
      <code>{{ a }} - {{ b }} = {{ data.sub }}</code
      ><br />
      <code>{{ a }} * {{ b }} = {{ data.mul }}</code
      ><br />
      <code>{{ a }} / {{ b }} = {{ data.div }}</code>
    </p>
  </NuxtExampleLayout>
</template>

実際にページにアクセスしてみると、aとbの2値を用いた各計算結果を取得できるていることを確認できます。wasm機能は本執筆時点での公式ドキュメントを見るとexperimental機能となっていますが問題なく動作しているようです。

次はもう少し複雑な計算も試してみたいと思います。

参考リンク

コメント

タイトルとURLをコピーしました