WebAssembly, Emscriptenを少しだけ触ってみた話


はじめに

2019年12月、WebAssemblyがweb標準になったというニュースをみました。
https://www.w3.org/2019/12/pressrelease-wasm-rec.html.en

それなら調べてみようと思い、やっと最近ちょっとだけ触ってみたというお話です。

あとQiitaの投稿の練習と、知見者にコメントでいろいろと教えていただきたいので書いてみました。

WebAssemblyってなに?

アセンブラのようなバイナリ形式の低レベル言語です。
Web標準になったことにより、HTML, CSS, JavaScriptに続いて正式にブラウザで動く4つめの言語になりました。
すでに主要4ブラウザ(Chrome, Firefox, Edge, Safari)では動きます。

なにがすごいのか

様々な言語に対応

「アセンブラ風のバイナリ形式」のため、そのまま実装するエンジニアはおそらくほとんどいません。
このWebAssemblyはC/C++, Go, TypeScriptなど様々な言語からコンパイルして生成することができます。
生成されたWebAssemblyはブラウザやnode.js上で動く、
つまりHTML/CSS, JS以外の言語で書かれた資産がブラウザで実行できることになります。

調べてみると
C++で書かれていたゲームをブラウザで実行したりVimを移植したりする記事もでてきました。すごい。

高速

とても速いらしいです(アバウト)。
詳しくはWebAssemblyはなぜ速いのかをご覧ください。
簡単に言うと
JSはダウンロードされてからブラウザ内部で解析され、その後最適になるようにコンパイルされて実行されますが、
WebAssemblyはすでにコンパイルされた中間言語なため実行までが速く、さらにより機械に適した書かれ方をしているため実行も高速ということらしいです。ガベージコレクションもないためメモリの手動管理が必要ですが、それも高速化の要因みたいです。すごい!

というわけで触ってみた

これだけ面白そうな話がでてくると触ってみないわけにはいかないですね。
今回はEmscriptenを使ったC言語からのコンパイルを試してみました。
なぜ選んだのかは
1年間本番環境で WebAssembly ( by Emscripten )を使ってきた中で生じた問題とその解決策
の記事の

アプリケーションのサポート範囲によってはどうしても WebAssembly ありきで考えられない場合があり、そういった場合は WebAssembly を使わないものにフォールバックする必要があり、その観点では、フォールバック先を用意できる AssemblyScript や Emscripten が良いのではないかと考えています(前者は AssemblyScript がというよりは、コンパイル対象が TypeScript であったりするので、そもそもそのまま純粋な JavaScript ソースを生成する経路がありますし、 Emscripten は WASM=0 オプションをコンパイル時に渡すだけで簡単に asm.js を利用した JavaScript ソースコードを生成することができます )。

に納得してしまったからです。
asm.jsに関してはここでは深く言及しませんが、

asm.jsは、Javascriptの文法を保ったまま整数演算(int系)や単精度浮動小数点演算(float)を書けるようにして、 この文法に対応したスクリプト実行環境でJavascriptの数値演算を速くするための技術です。
引用:Javascriptの数値演算とasm.js

というものです。

C言語からのコンパイルとEmscripten

C言語からコンパイルする方法はいろいろありますが、
今回は最もメジャーだと考えられるEmscriptenを使用します。
Emscriptenはllvmやbinaryenを使用して、webassemblyと、その読み込みや設定のためのJSを生成します。
まずは公式からSDKが配布されているため、公式ページに従ってインストールしてみましょう。Emscriptenをインストールする前に準備をします。
私はMacを使用しているため、以降はMacユーザーを想定した説明になります、すみません。

Emscriptenイントールの前に

公式にもありますが、インストール前に準備があります。
まずはEmscriptenのSDK(emsdk)はpythonを使用しているため、pythonをアップデートしろとのことです。 おそらく2.7.12以上がインストールされていれば問題なさそうです。

次に以下のツールが入っていなければインストールを行います。

  • Xcode Command Line Tools
  • git
  • cmake

cmakeはC系言語のビルドを自動化してくれるツールです。
インストールのための記事は調べてみればすぐに出ると思うので割愛させてください(あとで余裕あればリンクはります)

Emscriptenインストール

あとは公式の手順に沿ってインストールしていきます。
任意のディレクトリで

# emsdkの入手
git clone https://github.com/emscripten-core/emsdk.git

# ディレクトリに移動
cd emsdk

# Emscriptenのインストール
./emsdk install latest

# Emscriptenのアクティベート(バージョンの指定)
./emsdk activate latest

# PATHの設定
source ./emsdk_env.sh

を順番に実行し、インストールを行いましょう。
(設定を行うと一時的にnode.jsがemsdk内蔵のものを参照するようになります。他にnodeを使用する場合は注意。)

Emscriptenを使ってみる

いよいよEmscriptenを使ってみます。まずはコンソールにhello worldを出してみます。
Emscriptenを使用してhtmlも一緒に生成するデモをよくみますが、今回は自分で用意したhtmlで読み込んでみましょう。

以下のように簡単なディレクトリを作りましょう。


test
├── hello.c
└── index.html

それぞれの中身はこんな感じ。すごくシンプル。
hello.jsはこれから生成します。

hello.c
#include <stdio.h>

int main() {
  printf("Hello World\n");
}
index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>emscripten test</title>
  </head>
  <body>
    <script src="./hello.js" ></script>
  </body>
</html>

Emscriptenでのコンパイル

testディレクトリ配下で以下のコマンドを実行します

emcc hello.c -o hello.js

-oオプションは生成ファイルの指定です。
これを行うと以下のようになったはずです。

.
├── hello.c
├── hello.js
├── hello.wasm
└── index.html

このhello.wasmがWebAssemblyで, hello.jsがその読み込みやメモリ設定などをしてくれています。
この状態でindex.htmlをサーバで読み込んでみましょう。(ローカルで開くとwasmが読み込めないので注意。)
consoleにhello worldが表示されていれば成功です!

もし

wasm streaming compile failed: TypeError: Failed to execute 'compile' on 'WebAssembly': Incorrect response MIME type. Expected 'application/wasm'.

のような警告が出る場合はサーバにwasmの設定がされていないため
その設定をする必要があります。
使っているサーバのMIME TYPE設定ファイルにwasmを書き加えましょう。

Moduleオブジェクト

Emscriptenを使用して生成したjsを読み込むとグローバルにModuleオブジェクトが生成され、この中にいろんなものが入るみたいです。
ここはまだ調べきれてないので今後の課題ですね。

JSからC言語の関数を呼び出す

例えばボタンを押したなどのアクションがあった際に引数を渡してWebAssemblyで何かしらの計算させたかったとします。
今回は簡単な足し算をしてみましょう。

C言語の設定

hello.cを次のように変更します。

hello.c
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#else
#define EMSCRIPTEN_KEEPALIVE
#endif

#include <stdio.h>

int EMSCRIPTEN_KEEPALIVE add(int a, int b) {
  return a + b;
}

int main() {
  printf("Hello World\n");
}

なにやら見慣れない文字が。
emscripten.h はemsciptenにあるヘッダーです。
Emscriptenはコンパイル時にdead code elimination (Tree Shaking的なやつ)を行っており、
main関数から辿れない関数は消去されてしまいます。
そのため関数名の前にEMSCRIPTEN_KEEPALIVEをつけてコンパイル時に書き出すように教える必要があります。
ifdefでヘッダ読み込みを分岐させているのは、Emscripten以外でこのコードを使用した場合でもエラーが出ないようにするためです。

コンパイル

それでは再度コンパイルを行いましょう。
先ほどとは違い、次は以下のコマンドを実行してください。

emcc hello.c -o hello.js -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']"

これによってccallから関数を呼び出すことができます。

JSからの呼び出し

htmlを以下のように変更します。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>emscripten test</title>
  </head>
  <body>
    <button onclick="add(1,2)">1 + 2 = ?</button> 
    <script src="./hello.js" ></script>
    <script>
      const add = (a, b) => {
        const result = Module.ccall('add','number',['number'], [a, b]); 
        console.log(result);
      }
    </script>
  </body>
</html>

これでボタンを押した際にjsで定義したadd関数が呼ばれるようになりました。
add関数の中をみていきましょう。

const result = Module.ccall('add','number',['number', 'number'], [a, b]); 

グローバルに存在するModuleオブジェクト中のccallを使用することでWebAssemblyの関数を呼び出すことができます。
これは4つの引数を持ち、前から順番に
1. 使用したい関数名
2. 使用したことによって返ってくる型(number or string)
3. C言語の関数に渡す引数の型の配列(number or string)
4. C言語の関数に渡す引数の配列

です。

サーバで開きボタンを押したらconsoleに3と表示されれば成功!

調べてみて・触ってみて思ったことを少しだけ

いろんな記事をみていると
Docker創業者が

「もしもWebAssemblyとWASIが2008年に存在していたら、Dockerを開発する必要はなかった」

と言ったというものも出てきました。
WebAssemblyとRustが作るサーバーレスの未来

私は調べてみた結果この技術にかなり期待しています。
例えば画像処理などのフロントエンド以外の分野で長けたエンジニアと協力し、より高度なウェブアプリケーションを実現したりもできるはずです。
それと同時に私たちフロントエンドエンジニアはHTML/CSS, JSだけでなく
幅広い言語やそれを用いて作られた資源についても知っていく必要があると思っています。
今後フロントエンドエンジニアやソフトウェアエンジニアの境界があまりなくなっていくのかなあとか。。完全に混ぜ合わさることはなさそうですが、行き来は今よりも簡単にできるようになりそうですよね。

まだこれからどんどん発展していくであろう技術なので引き続き調べてみるつもりです