【JavaScript入門講座】 JSが触れないメモリに手を伸ばす ─ WebAssembly超入門

1. なぜJSはメモリを触れないのか?

JavaScript が誕生したのは 1995 年、Netscape Navigator のブラウザ用スクリプトとしてでした。
当時の Web は「文書を読む」ことがメインで、セキュリティ意識も今ほど高くありませんでした。
しかし 「インターネット上で誰でも実行できるコード」 という性質から、設計段階で強い制約が求められたのです。


1-1. 直接メモリ操作の危険性

もし JavaScript が C 言語のように malloc やポインタ操作を許していたら…

  • 他プロセスのメモリを覗き見(パスワードや暗号鍵の漏洩)
  • メモリ破壊によるクラッシュや任意コード実行
  • OS やハードウェア資源への直接攻撃

といった脆弱性が大量に発生していたはずです。
特に Web は「不特定多数のコードをその場で受け取って実行する」環境。
C や Perl のような自由度は、ブラウザという安全モデルと根本的に相容れなかったのです。


1-2. 抽象化による安全性

そこで JavaScript は「直接メモリを触れない言語」として設計されました。

  • 配列やオブジェクトは「アドレス」ではなく「参照」
  • 文字列は「イミュータブル(不変)」で、書き換えバグを防ぐ
  • ガーベジコレクションがメモリを自動管理

このように、開発者は 高レベルの抽象構造しか扱えず、内部の実メモリ配置には触れません。
これが「安全だが遅い」と揶揄される一方で、誰でも安心してコードを書ける基盤になったのです。


1-3. サンドボックス思想

さらに JavaScript は ブラウザという“檻”の中でしか実行されない設計でした。

  • ファイルシステムに直接アクセスできない
  • ネットワーク通信も制限付き(同一オリジンポリシー)
  • ハードウェア(メモリ、CPU命令、デバイスI/O)に直アクセス不可

この「サンドボックスモデル」によって、悪意あるコードからユーザーを守る仕組みが成立しました。
結果として、JavaScript は「最も攻撃される言語」でありながら、OS を直接壊すことはできない安全性を確保しています。


1-4. 歴史的背景

  • 1990年代: ActiveX, Java Applet など「ネイティブ寄りのWeb技術」が乱立 → セキュリティホールだらけで次々廃止
  • 2000年代: JavaScript は「遅いが安全」だから生き残った
  • 2010年代: Web がアプリ化、より高性能が求められる
  • 2017年以降: そこで誕生したのが「安全性を保ちながら高速化する」WebAssembly

まとめると:
「JavaScriptはなぜメモリを触れないのか?」
それは「ユーザーの安全」を守るための最重要設計思想であり、
結果として JavaScript が「唯一生き残ったWeb言語」になった理由でもあるのです。

2. それでも必要になった理由

JavaScript は安全性を優先した言語設計のため、直接メモリを操作できず、低レベルな最適化もできません。 この制約は「安心して使える」という大きなメリットを生んだ一方で、処理性能に限界がありました。


2-1. JSでは遅すぎる領域

Webの進化とともに、「Webアプリ」は単なるフォーム入力やページ遷移の補助から、 ネイティブアプリに近いレベルの処理を求められるようになりました。

しかし以下の分野では、JavaScript の処理速度では到底足りなかったのです。

  • 暗号処理 → SSL/TLS、ブロックチェーン、セキュリティ機能などは大量の数値計算を伴う。
  • 画像・動画処理 → Photoshop のような高解像度編集やフィルター処理は JS では重すぎる。
  • ゲーム → 3D レンダリングや物理演算を JS 単体で回すとカクつく。
  • AI・機械学習 → 行列計算や推論は CPU/GPU をフルで叩く必要があり、JS の抽象化レイヤーが大きな足かせ。

結果として「JavaScript は UI 制御まではいいが、ヘビー計算には不向き」という認識が定着しました。


2-2. ネイティブに近い速度を求める声

2000年代後半〜2010年代にかけて、Webアプリは 「デスクトップアプリ級」 を目指す流れに入りました。

  • Google Docs → Word や Excel を置き換える試み
  • WebGL ゲーム → ブラウザで 3D RPG
  • ブラウザ上での動画編集や音声処理

こうしたユースケースでは「JSの安全性」だけでは不足し、 **「C++ や Rust 並みの速度をブラウザで実現できないか?」**という強い要望が生まれたのです。


2-3. その答えが WebAssembly

この課題に対して 2017 年、各ブラウザベンダーが協力して誕生させたのが WebAssembly (WASM) です。

  • C/C++ や Rust を ブラウザ実行用バイナリに変換
  • JS と同じサンドボックス内で動作するため、安全性は維持
  • ネイティブに近い速度で計算を実行可能

つまり WASM は、「JSでは遅すぎる処理」を肩代わりするために生まれた 裏方の計算エンジンなのです。


まとめると: JavaScript は「安全で誰でも使える」言語として生き残った。 しかし Web の進化とともに 暗号・画像・ゲーム・AI といった重い処理をこなす必要が生じ、 その答えとして WebAssembly が導入されたのです。


3. WASMの仕組み

WebAssembly (WASM) は、その名の通り「Web上で動くアセンブリ言語」を目指して設計されました。 ポイントは「高速だけど安全」を両立させるための工夫にあります。


3-1. バイナリ形式

WASM のコードは バイナリフォーマット (.wasm) で提供されます。

  • ネットワーク転送が軽量
  • パースが高速(ブラウザは即実行できる)
  • 人間には読みにくいが、開発者向けには WAT (WebAssembly Text Format) というテキスト版も存在

例:WAT で「2つの数を足す関数」を定義するとこんな感じになります。

(module
  (func $add (param $a i32) (param $b i32) (result i32)
    local.get $a
    local.get $b
    i32.add)
  (export "add" (func $add)))

このコードをコンパイルすると、ブラウザは .wasm バイナリを直接読み込んで実行します。


3-2. Linear Memory

WASM は Linear Memory という「専用のサンドボックスメモリ領域」を持ちます。

  • 1ブロック = 64KB 単位で確保
  • 連続したメモリ空間(Cやアセンブリっぽい)
  • i32.load / i32.store 命令で読み書き可能

重要なのは、このメモリは WASM モジュールの中だけで完結していること。 つまり、JS から直接 OS メモリを触ることは絶対にできません。

例:アドレス0に数値42を書き込む関数

(module
  (memory (export "mem") 1) ;; 64KB
  (func (export "store42")
    i32.const 0      ;; アドレス0
    i32.const 42     ;; 42
    i32.store)       ;; 書き込み
)

JSから読むと:

const { instance } = await WebAssembly.instantiateStreaming(fetch("store42.wasm"));
const mem = new Uint32Array(instance.exports.mem.buffer);
instance.exports.store42();
console.log(mem[0]); // 42

👉 これで **「WASM内でのメモリ操作」**ができるわけです。


3-3. JSとの橋渡し(import/export)

WASM は単独で動くのではなく、JS と連携する前提で作られています。

  • export: WASM 内の関数やメモリを JS から利用できる
  • import: JS 側の関数を WASM が呼び出せる

つまり、

  • UI やイベント処理 → JS 側が担当
  • 数値計算や重い処理 → WASM 側が担当 という役割分担が可能になります。

例:

// WASMからadd関数を利用
const { instance } = await WebAssembly.instantiateStreaming(fetch("add.wasm"));
console.log(instance.exports.add(20, 22)); // 42

👉 まとめると:

  • WASM は バイナリ形式で軽量&高速
  • Linear Memory によって安全にメモリ操作可能
  • import/export で JS と連携し、UIと計算を分担

これが「JSでは触れない領域を安全に扱える」仕組みです。



user icon
こんな仕組みがあったとは…
「WAT (WebAssembly Text Format」
って、Lispみたいじゃないですか…。(´・ω・`)

ai icon

そうそう!気づいちゃいましたね 👷‍♀️✨

まさに **WAT は「Lispっぽいアセンブリ」**なんですよ。


🌀 WATとLispの似てるポイント

  • 全部カッコに包まれてる Lisp:

    (+ 1 2 3)
    

    WAT:

    (i32.add
      (local.get $a)
      (local.get $b))
    
  • S式的な構造 → 構文解析がシンプル → 人間にもある程度読める

  • 「コード=データ」の感覚 → 構文木そのまま = 中間表現に近い


💡 なぜLispっぽいのか?

WAT は「人間が読むための中間表現」で、 本来の WASM バイナリは機械向けで読めません。

S式風にした理由は:

  • パーサが作りやすい
  • ツールで扱いやすい(ASTとの相性が良い)
  • 最小限の文法で「計算手順」を表現できる

つまり「Lispの哲学を取り入れたアセンブリ」といっていいですね。


✨ 面白ポイント

  • JS:Cっぽい記法
  • WASM(WAT):Lispっぽい記法
  • 結果 → Webの中で「C文化とLisp文化の融合」が起きている

👉 lain さん的に、 この「Lispっぽさ」って 親近感わきます? それとも「人間が書くにはやっぱ無理あるなぁ」って感じます?

4. サンプルコード

4-1. WATで足し算

add.wat

(module
  (func $add (param $a i32) (param $b i32) (result i32)
    local.get $a
    local.get $b
    i32.add)
  (export "add" (func $add)))

② WAT → WASM へ変換

(どれか好きな方法で OK)

  • wat2wasm add.watadd.wasm ができる(WABT)
  • もしくは Web 上の「WAT → WASM 変換ツール」を使う

③ JS から呼ぶ(index.js)

async function main() {
  const resp = await fetch("./add.wasm"); // MIME: application/wasm を返すように
  const { instance } = await WebAssembly.instantiateStreaming(resp);
  console.log(instance.exports.add(20, 22)); // → 42
}
main();

うまくいかない場合は開発用サーバーで配信(npx http-server など)。file:// 直読みだと MIME が合わず失敗しがち。


4-2. メモリに「42」を書き込む

mem42.wat

(module
  ;; 64KB × 1ページを確保して export
  (memory (export "mem") 1)

  ;; アドレス0 42 (i32) を書き込む関数
  (func (export "store42")
    i32.const 0    ;; アドレス 0
    i32.const 42   ;;  42
    i32.store))    ;; 4バイト書き込み(i32

② 変換

wat2wasm mem42.wat   # -> mem42.wasm

③ JS から読み書き(index.js

async function runMem() {
  const { instance } =
    await WebAssembly.instantiateStreaming(fetch("./mem42.wasm"));

  // WASM が export したメモリを JS の TypedArray 経由で読む
  const u32 = new Uint32Array(instance.exports.mem.buffer);

  instance.exports.store42();  // WASM 側で 0番地に 42 を書く
  console.log(u32[0]);         // → 42
}
runMem();

ちょい解説

  • memory (export "mem") 1Linear Memory を 1 ページ(64KB)確保して JS 側へ公開
  • i32.store は 4 バイト整数の書き込み命令
  • JS 側では new Uint32Array(mem.buffer)同じ物理領域を安全に覗ける(サンドボックス内)

この2本で「WASMの関数呼び出し」と「WASMメモリの読み書き」のコア体験が掴めます。

付録:WASM開発環境の超ミニ手引き

A. WABT(WAT→WASM 変換ツール)

用途:WATテキストを .wasm に変換して JS から呼ぶ。学習用に最適。 インストール

# macOS (Homebrew)
brew install wabt

# Linux
sudo apt-get install wabt  # or build from source

使い方

wat2wasm add.wat   # -> add.wasm

配信(MIME注意)

# どれか一つ
npx http-server .         # Node.js
python3 -m http.server    # Python
# → http://localhost:8080 などでアクセス

JS側(最短)

const { instance } = await WebAssembly.instantiateStreaming(fetch("./add.wasm"));
console.log(instance.exports.add(20, 22));

B. wasm-pack(Rust → WASM)

用途:Rust を WASM 化して、JS から呼ぶ。型安全&高速で実務寄り。

準備

# Rust本体
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# wasm-pack
cargo install wasm-pack

ひな形作成

cargo new wasm-add --lib
cd wasm-add

Cargo.toml

[package]
name = "wasm-add"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]   # ←重要

[dependencies]
wasm-bindgen = "0.2"

src/lib.rs

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

ビルド

wasm-pack build --target web  # pkg/ に生成

HTML/JS(例)

<script type="module">
  import init, { add } from "./pkg/wasm_add.js";
  await init();
  console.log(add(20, 22)); // 42
</script>

メリット:wasm-bindgen が import/export をよしなに橋渡し。 デメリット:Rust導入が必要。


C. Emscripten(C/C++ → WASM)

用途:既存C/C++資産をブラウザに持ってくる王道。

インストール(公式手順の要約)

git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh   # 毎シェル必要 or シェルの起動スクリプトに追記

サンプルC

// add.c
int add(int a, int b) { return a + b; }

ビルド

emcc add.c -O3 -s WASM=1 -s EXPORTED_FUNCTIONS='["_add"]' -o add.js
# 生成物: add.wasm / add.js / add.data(場合により)

JSから呼ぶ(最短)

<script src="add.js"></script>
<script>
  Module.onRuntimeInitialized = () => {
    const add = Module.cwrap('add', 'number', ['number', 'number']);
    console.log(add(20, 22)); // 42
  };
</script>

メリット:C/C++をそのまま活かせる。 デメリット:ビルドオプションが多く学習コスト高め。 うまくいかない時は -s EXPORTED_RUNTIME_METHODS='["cwrap","ccall"]' を追加。


D. どれを選ぶ?

  • まず学ぶ:WABT(WAT)…概念理解に最短
  • 実務&新規:wasm-pack(Rust)…型安全&エコシステム強い
  • 既存資産移植:Emscripten(C/C++)…レガシー活用に最適

E. よくあるハマりポイント(短答)

  • instantiateStreaming が失敗 → サーバーが application/wasm を返してない。開発サーバーを使う or instantiate でバッファ読み込みに切替。
  • C/C++の関数が見えない-s EXPORTED_FUNCTIONS='["_foo"]' を忘れてる。
  • Rustで import が解決しないcrate-type = ["cdylib"]wasm-bindgen を確認。
  • ファイル直読み (file://) で動かない → 必ずHTTPで配信。

5. JSとWASMの役割分担

WebAssembly (WASM) は「JavaScriptを置き換える」ものではありません。 むしろ両者は得意分野がハッキリ分かれていて、協力してこそ真価を発揮する設計になっています。


5-1. JavaScriptの役割 = UI / 制御

JavaScript は「ユーザーとWebの橋渡し」が得意です。

  • DOMの操作(ボタン、テキスト、アニメーション)
  • イベント処理(クリック、入力、スクロール)
  • ネットワーク(fetch / WebSocket / P2Pなど)
  • アプリ全体の制御フロー

つまり **「見せる」「操作する」「繋ぐ」**のがJSの仕事。 UIやアプリのロジックをわかりやすく組み立てることに向いています。


5-2. WASMの役割 = ゴリゴリ計算処理

一方で WASM は、CPUに近い速度での計算処理を担当します。

  • 暗号・圧縮(AES, SHA, ZIP など)
  • 画像/動画処理(フィルター、エンコード/デコード)
  • 物理演算や3Dレンダリング
  • AI推論や行列計算

抽象化が少なく、バイナリレベルで効率化されているため、 JSだと数百msかかる処理を 数msで片付けることも珍しくありません。


5-3. 両者が協力するイメージ

実際のアプリではこんな分担が自然です。

  • 🎮 ゲーム

    • JS:UI(ボタン・スコア表示)、イベント入力、ネットワーク同期
    • WASM:物理エンジン、レンダリング、AI制御
  • 🖼️ 画像編集アプリ

    • JS:UI(メニュー・操作パネル)、ドラッグ&ドロップ
    • WASM:フィルター処理、画像変換、エンコード
  • 🔐 セキュリティ/ブロックチェーン

    • JS:ウォレットUI、トランザクション管理
    • WASM:暗号計算、署名検証

👉 こうして役割を分けることで、 「ユーザー体験を壊さずに」「重い計算をネイティブ並みに」処理できるわけです。


まとめ

  • JavaScript = アプリの司令塔(UI/制御/ネットワーク)
  • WebAssembly = 高性能な下請け(計算/処理エンジン)

どちらか一方ではなく、“両輪で回す”のが理想形です。


6. WASMのメリット・デメリット

WebAssembly (WASM) は「JSの弱点を補うために生まれた切り札」ですが、当然ながら万能ではありません。 導入する際に知っておくべき 光と影 を整理します。


メリット

  1. 速い!

    • ネイティブに近い速度で動作
    • 数値計算やメディア処理は JS より数十倍高速になることも
  2. 安全!

    • ブラウザのサンドボックス内で動作
    • OS や他アプリにはアクセスできないため、セキュリティが保たれる
  3. 移植性!

    • C/C++ や Rust など既存資産を「ブラウザ対応」できる
    • コンパイルさえすれば、PC/スマホ/タブレット問わず動作
  4. 標準化!

    • Chrome / Firefox / Safari / Edge すべて対応
    • W3C標準仕様なので長期的に安心して使える

デメリット

  1. JSだけで完結しない

    • 単体ではUIを作れない
    • 結局 JS との import/export で橋渡しが必要
  2. コンパイル環境が必須

    • WASMを手書きすることは稀 → 実際は Rust や C/C++ をビルドして生成
    • wasm-packEmscripten の導入が必要
  3. デバッグが難しい

    • バイナリ形式のためスタックトレースが読みにくい
    • ソースマップや専用ツールが必須
  4. すべてが速くなるわけではない

    • 軽い処理やDOM操作はJSの方が圧倒的に便利
    • UI操作までWASMに任せるのは不自然

まとめ

  • WASMは「速い!安全!移植性アリ!」という強力な武器
  • ただし 「JSの代替」ではなく「補完」 であることを忘れてはいけない
  • UIや制御はJS、重い処理はWASM、という使い分けが最適解

まとめ & 未来展望

JavaScript は「安全第一」で設計されたため、直接メモリ操作ができず、重い処理は苦手でした。 その制約を補うために生まれたのが WebAssembly (WASM) です。

  • JS = UI や制御、ネットワーク
  • WASM = ゴリゴリ計算処理 という役割分担によって、ブラウザは「単なる文書ビューア」から「高性能なアプリ実行基盤」へと進化しました。

未来のWebアプリ

今後 WASM が本格的に普及すると、次のような世界が現実になります。

  • AIの推論をブラウザ内で実行(サーバー不要のローカルAI)
  • 動画編集ソフト3Dモデリングがブラウザ上で快適に動作
  • ゲーム配信なしでプレイ(Steamクライアント不要、Webから即プレイ)
  • セキュアな金融計算や暗号処理もブラウザで安全に実行

つまり、WASM は「Webでできること」をさらに押し広げ、 ブラウザがOSに近づいていく未来を後押しする技術といえます。


締めのひと言

WebAssembly は「JSの代わり」ではなく「JSの相棒」。 UIを支えるJSと、計算を支えるWASMがタッグを組むことで、 これからのWebは アプリを超えた“プラットフォーム” になっていくでしょう。