WebAssembly 興味あるけど何?調べても、どうやって動いてるのかさっぱりわからないんだけど…という方多いのではないでしょうか!自分もそうでした 😂

この記事では、 WebAssembly の目指しているゴールをその具体例とともに示し、みなさんの今後の深堀りを行うためのインデックスとしてお役にたてれば幸いです!なお、この記事は以下のような読者を想定しています。

  • WebAssembly がどういうものか知りたい人
  • 自分が作った WebAssembly がブラウザで実行されるまでの仕組みを知りたい人

以下に書いてあることのほとんどは MDN に書いてあることをベースに、自分なりに丁寧に書き下したものになります。

https://developer.mozilla.org/ja/docs/WebAssembly/Guides/Concepts

非常によくまとまっているので、この記事と併せて読んでいただけると、より理解が深まると思います 💪

この技術が登場したことの背景を理解することは WebAssembly を理解するもっとも近い道筋です。ここでは必要最低限の背景の説明に留めております。

ブラウザに足りなかったもの

ブラウザは HTML、CSS、JavaScript を解釈してそれをユーザーに見せるためのプログラムです。サイト配信者はその 3 つの言語を組み合わせ、ユーザーに配信することでさまざまなコンテンツを届けてきました。

多くのユースケースではこの 3 種類の言語の組み合わせで十分でしたが、ゲームや機械学習ではどうでしょうか?物理演算や機械学習モデルの計算など、メモリを大量かつ高速に扱うケースは JavaScript にとって荷が重いタスクです。一般的にインタプリタ言語がコンパイル言語と比べてその性能が低いことはソフトウェアエンジニアにはよく知られていることでしょう。

より詳細な歴史的経緯やその変遷は他の文献に委ねますが、端的に言えば WebAssembly 登場以前ではブラウザでネイティブに近い実行速度でプログラムを実行させるための仕組みが必要とされていたことが背景にあります。

WebAssembly の誕生

そういった経緯で誕生した規格が WebAssembly です。WebAssembly の前身に相当するものが C/C++ をソースとしてコンパイルしたものを利用していた経緯もあり、この規格は C/C++ や Rust といったメモリ効率の高い言語をコンパイルすることを念頭に設計されています。

WebAssembly は Web 標準として組み込まれるために以下の 4 つの要項を挙げています。

  1. 高速かつ高効率、そしてポータブルであること
  2. 可読性があり、デバッグ可能であること
  3. 安全であること
  4. ウェブを破壊しないこと

それぞれについて、詳しく見ていきましょう 👀

1. 高速かつ高効率、そしてポータブルであること

誕生背景の直接的な理由はおおよそこれであることが考えられます。では、これを達成するためにはどうすればよいのでしょうか?鍵となるのはバイトコードと仮想スタックマシンです。

バイトコード

バイトコード(英: bytecode / byte code)は、バイト指向の、中間表現のコードすなわち中間コードの総称である。

バイトコードは私たちが直接書くものではありませんが、理解しておくべき大事な要素です。CPU はメモリを逐次与えられた命令に基づいて書き換えていきますが、その命令は多岐に渡ります。Apple M1 の登場初期、多くのプログラムが正しく動作しなかったのは Intel と Apple M シリーズ CPU の持つ命令セットが異なっていたため、Intel CPU 用の命令を持つソフトウェアが Apple M シリーズの CPU にその命令を送っても解釈されなかったことが原因です。

C++ で書いたプログラムをコンパイルするときに、それぞれの CPU に対して異なるバイナリを生成しなければならないのはそういった背景があります。各 CPU に対してコンパイルしたバイナリの処理速度は高速かつ効率的です。しかし、ウェブアプリのように実行環境が多岐にわたる場合、各環境に対応したバイナリを個別に生成するのはあまり現実的でありません。

その課題を解決するのがバイトコードです。バイトコードは CPU のアーキテクチャに依存しない抽象度を保ちつつ、低レベルの命令(機械語)への変換を効率的に行うことができるように設計された中間表現になります。 JVM、.NET や JIT コンパイルされた Python はバイトコードを利用しています。これによりアプリケーションやライブラリ制作者は、ランタイムが環境差異を吸収してくれることから、一度バイトコードを生成すれば、どの環境でも実行できるようになります。

仮想スタックマシン

上のバイトコードを実行するための概念として仮想スタックマシンが登場します。まずは以下の例を見てください。

義務教育で習ってきたよくみる数値計算の記法は演算子を間に配置するものです。


1 + 2 * 3 = 1 + 6 = 7

これを、逆ポーランド記法 を用いて表現すると以下の表現になります。

1 2 3 * +
= 1 6 +     # `*` 演算子はスタックに積み上げられていた 2 と 3 を取り出して 6 を戻している
= 7

ここでは、左から順番に数値を積み上げていき、演算子が出現したらその数値を取り出して計算しています。

スタックマシンはこのように、スタックに値を積み上げることと取り出して演算子を適用することを繰り返すことで計算します。

WebAssembly ではこのスタックマシンがバイトコードを実行するための、型、命令セット、メモリモデルを定義しています。そして、この仕様を実装し実際に動作させるためのソフトウェアのことを Embedder と呼んでいます。

2. 可読性があり、デバッグ可能であること

バイトコードは人間が理解するには難しく、コードがどのように実行されているのか理解しづらいです。そのような状況はネイティブアプリならともかく、ブラウザで動作するプログラムとしては好ましくないです。JavaScript と同様に開発者ツールでブレークポイントを貼って動作が理解できるとよさそうです。

バイナリ形式とテキスト形式

それを実現するための仕様として、WebAssembly には バイナリ形式 (WebAssembly Binary Format) と テキスト形式 (WebAssembly Text Format) の 2 つの表現方式が定義されています。バイナリ形式は、#バイトコード の WebAssembly における表現方式になります。特徴的なのはテキスト形式でこれは バイナリ形式と等価でありつつも人間が読めるような表現であること です。

たとえば、渡された 2 つの引数を足し合わせた数を返す add 関数を作ってみましょう。

add.wat

(module
  ;; 関数を定義する
  (func $add (param i32 i32) (result i32)
    ;;  ^関数名      ^引数      ^戻り値
    local.get 0
    local.get 1
    i32.add
    ;; スタックに積みあげられた2つの引数を取り出して足し合わせている
    return
  )
  ;; 関数を公開する
  (export "add" (func $add))
)

バイナリ形式 (wasm) とテキスト形式 (wat) はオンラインで手軽に変換するための デモサイト が用意されています。あるいはローカルで試したい場合には wasm-tools を使うとよいでしょう。

shell

$ wasm-tools parse add.wat -o add.wasm
$ ls -l
total 16
-rw-r--r--@ 1 shusann  staff   48 Apr  5 17:03 add.wasm
-rw-r--r--  1 shusann  staff  257 Apr  5 17:02 add.wat

生成された add.wasm はバイナリ形式 (wasm) でテキスト形式 (wat) よりファイルサイズが小さくなっていることがわかります。そして、おもしろいのはここからテキスト形式に戻せることです

shell

$ wasm-tools print add.wasm
(module
  (type (;0;) (func (param i32 i32) (result i32)))
  (export "add" (func $add))
  (func $add (;0;) (type 0) (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add
    return
  )
)

つまり、ウェブアプリケーション文脈において配信者は軽量なバイナリ形式にコンパイルしたファイルを配布し、必要に応じてブラウザでそれをテキスト形式に戻すことで可読性とデバッガビリティを担保しているというわけです。配信された wasm ファイルは実際に開発者ツールでデバッグできます。

デバッグ

たとえば、 Google Earth は 3D のレンダリングを WebAssembly で行っています。ネットワークタブを実際に見ると、ダウンロードされていることが確認できます。

wasm ファイルを開くとテキスト形式に変換されます。さらに、ブレークポイントを貼るとその時点におけるローカル変数やコールスタックなどを確認できます。

3. 安全であること

多くのプログラムの脆弱性はメモリへの不正なアクセスが理由となっていることが多いことは有名な話だと思います。WebAssembly Embedder の仕様として各インスタンスが与えられたメモリ以外へのアクセスを認めず、与えたメモリ領域を超えたアクセスが試みられた場合プログラム自体を終了 (trap) することを仕様としていることで安全性を担保しています。

4. ウェブを破壊しないこと

WebAssembly は既存のエコシステムを置換するものではなく、今まで足りなかった部分を補うものとして作られました。これは Google Earth の例 でも確認できるようにウェブアプリケーションを構成するコンポーネントの 1 つとして組み込まれ、JavaScript と相互に利用できるように設計されています。

ブラウザは WebAssembly が想定する Embedder としては第 1 村人です。その仕様は WebAssembly JavaScript Interface で定義されています。WebAssembly と JavaScript を相互に呼び出すことは、 importexport の 2 つコンポーネントを使って実現します。以下の例では、 demo.watEmbedder であるブラウザから import1import2 を受け取り、それらを呼び出しています。また、ブラウザからは WebAssembly から export された f 関数を呼び出しています。

demo.wat

(module
    ;; 後述の import Object で注入される関数をここで import する
    (import "js" "import1" (func $i1))
    (import "js" "import2" (func $i2))
    ;; main 関数は import1 関数を呼び出す
    (func $main (call $i1))
    (start $main)
    ;; import2 関数を呼び出すものを f として export する
    (func (export "f") (call $i2))
)

browser の js


var importObj = {js: {
    import1: () => console.log("hello,"),
    import2: () => console.log("world!")
}};

fetch('demo.wasm').then(response =>
    response.arrayBuffer()
).then(buffer =>
    
    WebAssembly.instantiate(buffer, importObj)
).then(({module, instance}) =>
    
    instance.exports.f()
);

このようにして、 WebAssembly は既存のウェブを破壊することなく、うまく統合されるようにデザインされています。

インターフェイスに違和感を覚えた?

一方、wasm のファイルを fetch してインスタンス化する処理を書くのは手続き的で面倒に感じられます。WebAssembly はコアコンセプトに ES Module を意識した モジュール を定義していて、理想的には import を使ってモジュールをインポートできるとよさそうです。

ES Module としてモジュールをインポートするための仕様は目下標準化に向けて進行中で ES Module Integration Proposal for WebAssembly でトラッキングされています。JS Embedding Interface を実装している deno はこれにいちはやく対応していて、ブラウザでも将来的に以下のようによりエルゴノミックに WebAssembly を利用できるようになるでしょう。

Deno 2.1

import { add } from "./add.wasm";

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


さて、ここまでで WebAssembly がどういうもので何ができるのかわかったと思います。それを踏まえて、今回は Go で WebAssembly バイナリを作ってブラウザで動かしてみましょう。Go 1.24 では WebAssembly 用の go:wasmexport ディレクティブが追加されました。

https://tip.golang.org/doc/go1.24#wasm

せっかくなのでそのディレクティブを使用して、 #バイナリ形式とテキスト形式 のセクションで作成した add 関数と同じものを Go で実装してみます。以下に紹介するコードはデモレポジトリ (shusann01116/go-wasm) で公開しているので併せて参照してください。

まずは、テキスト形式をおさらいします。

add.wat

(module
  ;; 関数を定義する
  (func $add (param i32 i32) (result i32)
    ;;  ^関数名      ^引数      ^戻り値
    local.get 0
    local.get 1
    i32.add
    ;; スタックに積みあげられた2つの引数を取り出して足し合わせている
    return
  )
  ;; 関数を公開する
  (export "add" (func $add))
)

32 ビット整数 2 つを受け取り、1 つを返す add 関数を定義しています。また、 export コンポーネントを使って関数を公開しています。これを Go で表現すると以下のようになります。

add.go

package main

func main() {}


func add(a, b int32) int32 {
  return a + b
}

Go のコードを WebAssembly にコンパイルするには GOOS=jsGOARCH=wasm を指定し、 go build を実行します。

shell

$ GOOS=js GOARCH=wasm go build -o add.wasm

Go は WebAssembly の実行に専用のグルーコードを js で提供しており、それをあらかじめ読み込んでおく必要があります。

$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .

JS 内では、 wasm_exec.js を読み込んでから、 add.wasm をダウンロードしてインスタンス化します。

https://github.com/shusann01116/go-wasm/blob/main/index.html#L11-L18

インスタンス化されたインスタンスは exports を通じてアクセスできます。

https://github.com/shusann01116/go-wasm/blob/main/index.html#L22-L27

今回の例でいうと、 add 関数を result.instance.exports.add として呼び出すことができます。

そして、ブラウザ上で実際に数字を入れてボタンを押下すると WebAssembly で計算された数値結果が表示されていることが確認できます 🎉

add.wasm のファイルサイズ

Go は GC を採用しているため、そのランタイムも WebAssembly にコンパイルする必要があり、ファイルサイズが大きくなってしまいます。これはロード時間がユーザー体験に与える影響度の大きいウェブにおいて理想的ではない状態で、バイナリサイズを小さくするための工夫が必要です。そのため、組み込み系で動作する TinyGo や GC を採用していない Rust や C など言語が比較的有利であり、この文脈でそれら言語がよく登場する理由のうちの 1 つです。

たとえば Go でコンパイルした add.wasm のファイルサイズは 1.5 MB ですが、これを TinyGo でコンパイルした場合は 100 KB 程度に縮小できます。

shell

$ tinygo build -o add-tiny.wasm -target wasm add.go
$ ls -lh add*.wasm
-rw-r--r--@ 1 shusann  staff   106K Apr  5 20:22 add-tiny.wasm
-rwxr-xr-x@ 1 shusann  staff   1.5M Apr  5 19:19 add.wasm

今回は WebAssembly が目指しているゴールがどのような形で実装されているかをなぞり、その上で実際に Go から WebAssembly バイナリを生成してブラウザで動作する一連の流れを追ってきました。当初のゴールであった WebAssembly の概要の理解が叶ったならば幸いですし、さらなる深堀りのための足がかりになれば嬉しいです 😊

本記事では WebAssembly をもっとアツくしているブラウザ外の Embedding API である WebAssembly System Interface (WASI) について触れることができませんでした。本当はこれについても触れたかったのですが、記事が長くなりすぎるので別の機会に触れることとします 😢

まだまだ発展途上な WebAssembly ですが、すでに多くのエコシステムに影響を与えていてこれからの発展が非常に楽しみです!

それではよき WebAssembly ライフを! 🏖

https://developer.mozilla.org/ja/docs/WebAssembly/Guides/Concepts

https://webassembly.org/

フラッグシティパートナーズ海外不動産投資セミナー 【DMM FX】入金

Source link