Webブラウザ上でネイティブコードを実行する仕組みにWebAssembly(wasm)という仕組みがあります。高度な画像処理などの計算をネイティブで高速に実行することでアプリケーションのパフォーマンスが向上しユーザー体験の向上が期待できます。
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 |
インライン生成の最大サイズ |
| publicPath |
maxFileSizeを超えた場合にwasmをコピーするファイル場所 |
| targetEnv |
wasm実行環境のターゲット |
| fileName |
ファイル名のフォーマット |
参考 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_UNDEFINEDto get more information on undefined symbols warning: To disable errors for undefined symbols use-sERROR_ON_UNDEFINED_SYMBOLS=0warning: _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機能となっていますが問題なく動作しているようです。
次はもう少し複雑な計算も試してみたいと思います。

参考リンク
- https://nuxt.com/docs/examples/experimental/wasm#wasm
- https://github.com/unjs/nitro/pull/450
- https://www.npmjs.com/package/@rollup/plugin-wasm
- https://rollupjs.org/plugin-development/
- https://developer.mozilla.org/ja/docs/WebAssembly/Using_the_JavaScript_API
- https://unicorn.limited/jp/rd/webui/20200916-rollup.html
- https://webassembly.org/
- ハンズオンWebAssembly ―EmscriptenとC++を使って学ぶWebAssemblyアプリケーションの開発方法 [PR]


コメント