WebAssembly モジュールの作成と操作

この記事は WebAssembly と何が速くしたのかのシリーズの3部です。もしまだ前の記事を読んでいない場合、最初から読むことをお勧めします。WebAssembly は、Web ページ上で JavaScript 以外のプログラミング言語を実行する方法です。これまで Web ページの様々な部分と対話するためにブラウザでコードを実行したいとき、唯一のオプションは JavaScript でした。WebAssembly が高速かどうかの話題の時は比較の対象は JavaScript になります。実際は、WebAssembly を使用しているか JavaScript を使用しているかのどちらか一方になるわけではありません。

実際、開発者は同じアプリケーションで WebAssembly と JavaScript の両方を使用することになります。WebAssembly を自分で作成しないなくても、それを利用可能です。

WebAssembly のモジュールは JavaScript から使用できるように関数を定義することができます。したがって、npm today から lodashの ようなモジュールをダウンロードし、その API の一部である関数を呼び出すように、将来、WebAssembly モジュールをダウンロードすることができるようになるでしょう。

では WebAssembly のモジュールをどのように作成し、JavaScript からどのように呼び出せるのかを見てみましょう。

WebAssembly はどこに適合するのか?

Assembly についての記事の中で、コンパイラが高水準のプログラミング言語をどのようにして機械語に翻訳するのかを話しました。

Diagram showing an intermediate representation between high level languages and assembly languages, with arrows going from high level programming languages to intermediate representation, and then from intermediate representation to assembly language

WebAssembly はこの図のどこに収まるのですか?

ターゲットアセンブリ言語の単なる別のものだと考えるかもしれません。これらの言語 (x86、ARM) のそれぞれが特定のマシンアーキテクチャに対応している点を除いて、それは事実です。

Web 上でユーザーのマシン上で実行されるコードを提供しているときは、コードが実行されるターゲットアーキテクチャはわかりません。

したがって、WebAssembly は他のアセンブリとは少し異なります。実際の物理マシンではなく、仮想マシンの機械語になっています。

このため、WebAssembly の命令は仮想命令と呼ばれることもあります。それらは、JavaScript のソースコードよりもはるかに機械語に直接マッピングされています。ごく一般的なハードウェアで効率的に行うことができることの一種です しかし、特定のハードウェアに対する特定の機械語への直接のマッピングではありません。

Same diagram as above with WebAssembly inserted between the intermediate representation and assembly

ブラウザが WebAssembly をダウンロードします。次に、WebAssembly からターゲットマシンのアセンブリコードまでの短い期間に使用します。

.wasm 形式へコンパイルする

WebAssembly を現在最もサポートしているコンパイラツールチェーンは LLVM です。LLVM に接続できる様々なフロントエンドとバックエンドがあります。

注記: ほとんどの WebAssembly のモジュール開発者は、C や Rust といった言語でコードを作成し、WebAssembly にコンパイルしますが、WebAssembly のモジュールを作成するには他の方法もあります。例えばTypeScript を使用して WebAssembly のモジュールを構築するのに役立つ実験的なツールや、WebAssemblyのテキスト表現を直接コーディングする方法もあります。

C から WebAssembly に変換したいとしましょう。clangフロントエンドを使用して、C から LLVM IR (中間コード) に変換することができます。LLVM は LLVM IR を理解します。そのため、LLVM はいくつかの最適化を実行できます。

LLVM IR (中間コード) から WebAssembly に変換するには、バックエンドが必要です。LLVM プロジェクトには現在進行中のものがあります。バックエンドはその大部分であり、すぐに終了するものです。しかし現在はこれを動作させるのはトリッキーかもしれません。

Emscripten と呼ばれる別のツールがありますが、これは現時点では少し使いやすくなっています。それは別のターゲット (asm.js と呼ばれる) にコンパイルし、それを WebAssembly に変換することで WebAssembly を生成できる独自のバックエンドを持っています。これは内部で LLVM を使用するので、Emscripten から 2 つのバックエンドを切り替えることができます。

Diagram of the compiler toolchain

Emscriptenには、C/C++ コードベース全体を移植可能にする多くのツールとライブラリが含まれているため、コンパイラよりもソフトウェア開発キット (SDK) のほうが多くなっています。例えば、システム開発者は、読み書きが可能なファイルシステムに慣れているため、Emscripten は IndexedDB を使用してファイルシステムをシミュレートできます。

使用したツールチェーンにかかわらず、最終的にはファイルは .wasm 形式になります。以下で .wasm ファイルの構造について詳しく説明します。まず、JS でどのように使用できるかを見てみましょう。

JavaScript で .wasm モジュールを読み込む

.wasm ファイルは WebAssembly のファイルであり、JavaScript から読み込むことができます。現在のところ、読み込む処理は少し複雑です。

function fetchAndInstantiate(url, importObject) {
  return fetch(url).then(response =>
    response.arrayBuffer()
  ).then(bytes =>
    WebAssembly.instantiate(bytes, importObject)
  ).then(results =>
    results.instance
  );
}

詳細はこのドキュメントを確認してください。

私たちはこのプロセスをより簡単にしようと取り組んでいます。私たちはツールチェーンを改善し、Webpack のようなモジュールバンドルや SystemJS のようなローダーと統合することを期待しています。WebAssembly モジュールの読み込みは、JavaScript の読み込みと同じくらい簡単です。

WebAssembly モジュールと JS モジュールの間には大きな違いがあります。現在 WebAssembly の関数では、数値 (整数または浮動小数点数) のみをパラメータまたは戻り値として使用できます。

Diagram showing a JS function calling a C function and passing in an integer, which returns an integer in response

文字列のように複雑なデータ型の場合、WebAssembly モジュールのメモリを使用する必要があります。

もし JavaScript を使って作業していたのであれば、メモリに直接アクセスすることはあまり馴染みがありません。 C、C ++、Rust のようなパフォーマンスの高い言語は、手作業によるメモリ管理を行う必要があります。WebAssembly モジュールのメモリは、これらの言語で見られるヒープをシミュレートします。

これを使用するには、ArrayBuffer という JavaScript の型を使用します。配列バッファはバイトの配列です。したがって、配列のインデックスはメモリアドレスとして機能します。

JavaScript と WebAssembly の間に文字列を渡す場合は、文字を文字コードに変換します。次にメモリ配列に書き込みます。インデックスは整数なので、WebAssembly 関数にインデックスを渡すことができます。したがって、文字列の最初の文字のインデックスをポインタとして使用できます。

メモリへのポインタを表す整数を持つ C 関数を呼び出す JS 関数と、次にメモリに書き込むC関数を示す図

WebAssembly モジュールを開発している Web 開発者の場合、そのモジュール周辺にラッパーを作成すべきでしょう。そうすることによってモジュールの使用者はメモリ管理について知る必要はありません。

詳細を知りたい場合は、WebAssembly のメモリの動作を確認してください。

.wasm ファイルの構造

高級言語でコードを記述し、WebAssembly にコンパイルする場合、WebAssembly モジュールの構造を知る必要はありませんが、基本を理解するのに役立ちます。

まだ知らない場合はWebAssembly の記事 (シリーズの第3部) を読むことをお勧めします。

WebAssembly に変換する C 関数を次に示します:

int add42(int num) {
  return num + 42;
}

WASM Explorer を使用してこの機能をコンパイルすることができます。

.wasmファイルを開くと (エディタで表示がサポートされている場合) 次のように表示されます。

00 61 73 6D 0D 00 00 00 01 86 80 80 80 00 01 60
01 7F 01 7F 03 82 80 80 80 00 01 00 04 84 80 80
80 00 01 70 00 00 05 83 80 80 80 00 01 00 01 06
81 80 80 80 00 00 07 96 80 80 80 00 02 06 6D 65
6D 6F 72 79 02 00 09 5F 5A 35 61 64 64 34 32 69
00 00 0A 8D 80 80 80 00 01 87 80 80 80 00 00 20
00 41 2A 6A 0B

これは “バイナリ” 表現のモジュールです。バイナリは通常 16 進表記で表示されますが、簡単にバイナリ表記や人間が読める形式に変換できるため、バイナリの周りに引用符を入れました

例えば、num + 42 のようになります。

Table showing hexadecimal representation of 3 instructions (20 00 41 2A 6A), their binary representation, and then the text representation (get_local 0, i32.const 42, i32.add)

コードの仕組み:スタックマシン

あなたが疑問に思っている場合、ここではそれらの指示で何をするのでしょうか。

Diagram showing that get_local 0 gets value of first param and pushes it on the stack, i32.const 42 pushes a constant value on the stack, and i32.add adds the top two values from the stack and pushes the result

add 操作でその値がどこから来るのかはわかりませんでした。これは WebAssembly がスタックマシンと呼ばれるものの例であるためです。これは操作が必要とするすべての値が、操作が実行される前にスタックにキューイングされていることを意味します。

add のような操作は、必要な値の数を知っています。add は 2 つ必要なため、スタックの先頭から 2 つの値をとります。これは命令がソースレジスタまたはデスティネーションレジスタを指定する必要がないため、add 命令が短く (1Byte) できることを意味します。これにより、.wasm ファイルのサイズが縮小され、ダウンロードに要する時間が短縮されます。

WebAssembly はスタックマシンの観点から指定されていますが、実際のマシンでは動作しません。ブラウザが実行中のマシンのマシンコードにブラウザが WebAssembly を変換すると、レジスタが使用されます。 WebAssembly コードではレジスタが指定されていないため、ブラウザにはそのマシンに最適なレジスタ割り当てを柔軟に使用できます。

モジュールのセクション

add42 関数自体に加えて、.wasm ファイルには他の部分もあります。これらはセクションと呼ばれます。セクションのいくつかは任意のモジュールに必要であり、いくつかはオプションです。

必須:

  1. Type. モジュールおよびインポートされた関数で定義された関数の関数シグネチャが含まれます。
  2. Function. モジュールで定義された各関数へのインデックスを与えます。
  3. Code. モジュール内の各関数の本体。

オプション:

  1. Export. 関数、メモリ、テーブル、グローバルを他の WebAssembly モジュールや JavaScript で使用できるようにします。これにより、別々にコンパイルされたモジュールを動的にリンクすることができます。これは WebAssembly の .dll バージョンです。
  2. Import. 他の WebAssembly モジュールまたは JavaScript からインポートする関数、メモリ、テーブル、およびグローバルを指定します。
  3. Start. WebAssembly モジュールがロードされたときに自動的に実行される関数 (基本的にメイン関数のような)。
  4. Global. モジュールのグローバル変数を宣言します。
  5. Memory. メモリをモジュールでどのように使うか定義します。
  6. Table. JavaScript オブジェクトなど WebAssembly モジュールの外部にある値にマップすることができます。これは間接的な関数呼び出しを許可する場合に特に便利です。
  7. Data. インポートした、またはローカルのメモリを初期化します。
  8. Element. インポートした、またはローカルのテーブルを初期化します。

セクションの詳細については、これらのセクションが詳しく説明しています。

次の記事

WebAssembly モジュールを使用する方法を勉強したので、WebAssembly を速くするには?を見てみましょう。

Lin Clark に関して

Lin は Mozilla Developer Relations チームのエンジニアです。 彼女は JavaScript、WebAssembly、Rust、Servo を使っています。また、コードの漫画を描きます。

Lin Clark によるその他の記事はこちら…

1 件のコメント

  1. AnonymousCoward :

    × ごく一般的なハードウェアで効率的に行うことができることの一種です
    intersectionが抜けている。
    → ごく一般的なハードウェアで効率的に行うことができることの共通部分です

    × バックエンドはその大部分であり、すぐに終了するものです。
    違う。
    → このバックエンドはすぐそこまで来ており、もうすぐ完成します。

    × バイナリは通常 16 進表記で表示されますが、簡単にバイナリ表記や人間が読める形式に変換できるため、バイナリの周りに引用符を入れました
    引用符を入れた理由はそこじゃない。
    → 通常 16 進表記で表示されるのでバイナリの周りに引用符を入れましたが、簡単にバイナリ表記や人間が読める形式に変換できます。

    × 実際のマシンでは動作しません。
    違う。
    → これは実際のマシンで動作する仕組みではありません。

    × これは WebAssembly の .dll バージョンです。
    逆。
    → これは.dllのWebAssemblyバージョンです。