ブラウザアプリの作り方
NagiScript を WebAssembly にコンパイルして、ブラウザで動く小さなアプリをゼロから作ってみましょう。ロジックは NagiScript で書き、DOM 操作やイベントは JavaScript が担当する役割分担が基本です。
完成イメージ
このチュートリアルで作るのは「カウンターアプリ」です。ボタンを押すと NagiScript で書いた関数が WASM 上で呼ばれ、カウントが増減します。
┌───────────────────────────┐
│ +──────┐ │
│ │ = 5 │ [!] [-1] [+1] │
│ └──────┘ │
│ count in NagiScript │
└───────────────────────────┘
ステップ 1: ロジックを NagiScript で書く
ブラウザから呼び出す関数は
export "C" でエクスポートします。WASM
の関数は数値や真偽値、ポインタを引数・戻り値に取れるので、まずは単純なカウンター計算から始めます。
// counter.ngs — ブラウザから呼び出すビジネスロジック
export "C" fn increment(n: i32) -> i32 {
return n + 1
}
export "C" fn decrement(n: i32) -> i32 {
return n - 1
}
export "C" fn is_even(n: i32) -> i32 {
if n % 2 == 0 { return 1 }
return 0
}
export "C" fn label_for(n: i32) -> i32 {
// 0 なら "zero"、10 以上なら "big"、それ以外は "normal"
if n == 0 { return 0 }
if n >= 10 { return 1 }
return 2
}
ステップ 2: WASM にコンパイル
nagiscript wasm counter.ngs -o counter.wasm
# counter.wasm が生成される
ステップ 3: HTML と JavaScript で組み込む
生成した counter.wasm を
fetch で読み込み、WebAssembly.instantiate
で初期化します。ヘルパー関数を使って DOM に組み込みます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Counter</title>
</head>
<body>
<h1>NagiScript Counter</h1>
<p id="count">0</p>
<button id="minus">-1</button>
<button id="plus">+1</button>
<p id="label"></p>
<script>
let count = 0;
let counter = null; // WASM exports
async function loadWasm() {
const bytes = await fetch('counter.wasm').then(r => r.arrayBuffer());
const { instance } = await WebAssembly.instantiate(bytes, {
env: { memory: new WebAssembly.Memory({ initial: 256 }) }
});
counter = instance.exports;
// 初期表示
render();
}
// NagiScript の label_for() は i32 を返すので JS 側で文字列化する
const LABELS = ['zero', 'big', 'normal'];
function render() {
document.getElementById('count').textContent = count;
const label = LABELS[counter.label_for(count)] ?? '?';
document.getElementById('label').textContent =
'is_even = ' + counter.is_even(count) + ' / ' + label;
}
document.getElementById('plus').addEventListener('click', () => {
count = counter.increment(count); // WASM で計算
render();
});
document.getElementById('minus').addEventListener('click', () => {
count = counter.decrement(count);
render();
});
loadWasm();
</script>
</body>
</html>
ブラウザで開けば、「+1」「-1」ボタンでカウントが増減し、NagiScript
側の increment / decrement /
is_even / label_for が WASM
上で実行されているのが分かります。
文字列をやり取りする
NagiScript の string はメモリ上で
{ data, len } のセルとして渡ります。JS 側からは
TextEncoder でバイト列を WASM
メモリに書き込み、ポインタと長さを渡します。
export "C" fn char_count(data: i32, len: i32) -> i32 {
return len
}
let memory;
async function load() {
const bytes = await fetch('greet.wasm').then(r => r.arrayBuffer());
const imports = { env: { memory: new WebAssembly.Memory({ initial: 256 }) } };
const wasm = await WebAssembly.instantiate(bytes, imports);
memory = wasm.instance.exports.memory;
window.ngs = wasm.instance.exports;
}
function writeString(memory, str) {
const bytes = new TextEncoder().encode(str);
const dv = new DataView(memory.buffer);
const HEADER = 16; // {data, len} セル
const ptr = allocate(memory, HEADER + bytes.length);
// 文字本体をセルの直後にコピー
const dst = ptr + HEADER;
new Uint8Array(memory.buffer, dst, bytes.length).set(bytes);
dv.setUint32(ptr, 0, true);
dv.setUint32(ptr + 8, bytes.length, true); // len
return ptr;
}
// 使用例: ngs.char_count(ptr, len) を呼んでから ptr を返す文字列セルを読む
const ptr = writeString(memory, "hello");
console.log(ngs.char_count(ptr, 5)); // => 5
詳細なメモリの扱いは WebAssembly チュートリアル を参照してください。
.ngsx による UI 構文(ロードマップ)
.ngsx(JSX 風 UI 構文)はランタイムに props 基盤が存在するものの、ドキュメント化は保留されています。以下は予定されている構文です。
NagiScript は JSX 風のマークアップ構文
.ngsx を予定しています。React
コンポーネントと統合し、NagiScript だけで UI
まで記述できるようになる予定です。
component Counter(ref count, onPlus, onMinus) {
return (
<div class="counter">
<p>{count}</p>
<button onClick={onMinus}>-1</button>
<button onClick={onPlus}>+1</button>
</div>
)
}
状態管理のパターン
今回のように「計算は NagiScript、状態は
JS」が最もシンプルです。規模が大きくなったら、状態も NagiScript
側に置いて
export "C" で読み書きする設計にできます。
var counter: i32 = 0
export "C" fn get_counter() -> i32 { return counter }
export "C" fn set_counter(v: i32) { counter = v }
export "C" fn inc() -> i32 {
counter = counter + 1
return counter
}
plusBtn.addEventListener('click', () => {
const v = exports.inc(); // 状態は WASM 内で保持
document.getElementById('count').textContent = v;
});
document.getElementById('reset').addEventListener('click', () => {
exports.set_counter(0);
document.getElementById('count').textContent = 0;
});
実践のヒント
- 最小限のエクスポートを心がける — WASM 境界を越える関数は計算集約処理に絞り、DOM は JS に委譲
-
型定義を活用する —
エクスポート関数は小さい関数に分割し、
nagiscript wasm main.ngsの出力から呼び出し方を確認する -
重い処理は Web Worker に移す — WASM
の同期実行はメインスレッドをブロックするため、計算量の高いエクスポートは
Web Worker 側で実行して UI を止めない(
async/std:asyncは現行では未実装) -
数値は i32 / f64 がおすすめ — JS の
Numberと相性が良く、変換コストが小さい
