ブラウザ上でC言語のコードを実行するって、一体全体どういうことだろう?
前職でCを扱っていたこともあって、WebAssemblyは気になる技術のひとつでした。
この記事では、HelloWorldに加えて、ちょっとした実験の結果をまとめてみました。
## WebAssemblyとは *WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine.* ([公式](https://webassembly.org/)より引用)
- ブラウザ上で動くバイナリコードのフォーマット
(名前の通り、ウェブ用のアセンブリ言語) - C/C++、Rustなどからコンパイル可能
- 略称はWASM(ワズム)
- jsのコードからWASMを呼び出せる。逆もできる。
- Chrome, FIrefox, Safari, Edgeなどで使用可能
- つまり、上記言語で書いたコードをブラウザ上で動かすことができるようになる
- 速くて実行環境を選ばない
- 主な用途は画像処理やゲームなど
- ファイルやネットワーク、メモリへのアクセスは仕様策定中らしい(要出典)
## おしながき(やったこと) * HelloWorldを動かしてみる * メモリリークするコードを動かすとどうなる? * メモリを使いまくるとどうなる? * ランタイムエラーになるコードを動かすとどうなる?
## HelloWorldを動かしてみる 参考: https://laboradian.com/tried-webassembly/ ([MDN](https://developer.mozilla.org/ja/docs/WebAssembly/C_to_wasm)と同じ手順だが、補足説明付きでより分かりやすい)
Emscripten(コンパイラ)をインストール
# 今回インストールしたバージョン
$ emcc -v
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 1.39.16
clang version 11.0.0 (/b/s/w/ir/cache/git/chromium.googlesource.com-external-github.com-llvm-llvm--project 3774bcf9f84520a8c35bf765d9a528040d68a14b)
Target: x86_64-apple-darwin19.5.0
Thread model: posix
shared:INFO: (Emscripten: Running sanity checks)
↓
hello.cを作る
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("Hello World\n");
return 0;
}
↓
コンパイル
# コンパイル前
$ ls
hello.c
# コンパイル
$ emcc hello.c -s WASM=1 -o hello.html
# wasm, js, htmlが生成された
$ ls -l
total 496
-rw-r--r-- 1 arene staff 95 5 19 21:55 hello.c
-rw-r--r-- 1 arene staff 102675 5 21 21:50 hello.html
-rw-r--r-- 1 arene staff 115917 5 21 21:50 hello.js # 2600行(!)
-rw-r--r-- 1 arene staff 21727 5 21 21:50 hello.wasm
コンパイル後に生成されたjsファイルはなんと2600行。
大変な技術だということが伺えます。
なお、当たり前ですがCとしてコンパイルできないコードは、WASMにもコンパイルできませんでした。
↓
chrome://flags/ で Experimental WebAssemblyを有効にする
↓
ローカルPCにWebサーバを立てる
# Emscriptenは簡易的なWebサーバまで用意してくれている
$ emrun --no_browser --port 8080 .
Web server root directory: /Users/arene/temporary/wasm
Now listening at http://0.0.0.0:8080/
↓
ブラウザでhello.htmlを開く
※ ロゴとかターミナル風の画面は、コンパイラが生成したものです
(もちろん、オプション次第でwasm + jsだけの出力もできます)
なお、HelloWorldをコンパイルした時に自動生成されたコードはこんな感じでした。
(しっかり解析できていない&大量にあるため、雰囲気をつかみやすいところだけ抜粋しました)
<!-- ロゴとかターミナル風な画面とか余計なものは割愛 -->
<script async type="text/javascript" src="hello.js"></script>
// 実際のコードは2600行あります
// 概要を掴む意味で、それっぽく抜き出しました
// 関数の定義順も、頭から読めるように変えています(本当は`run();`がファイル末尾にある)
var wasmBinaryFile = 'hello.wasm';
if (!isDataURI(wasmBinaryFile)) {
wasmBinaryFile = locateFile(wasmBinaryFile);
}
run(); // ファイル最下にあったこいつが全ての起点
function run(args) {
// めっちゃいろいろ省略
callMain(args)
}
function callMain(args) {
// めっちゃいろいろ省略
var entryFunction = Module['_main']; // Module['xxx']が大量にあって、main関数含めいろんなものが突っ込まれている
var ret = entryFunction(argc, argv);
exit(ret, true);
}
// こんな感じでCの関数をModule["asm"]に紐づけている
var asm = createWasm();
Module["asm"] = asm;
var _main = Module["_main"] = function() {
assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)');
assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)');
return Module["asm"]["main"].apply(null, arguments)
};
var _malloc = Module["_malloc"] = function() {
assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)');
assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)');
return Module["asm"]["malloc"].apply(null, arguments)
};
var _free = Module["_free"] = function() {
assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. waMBit for main() to be called)');
assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)');
return Module["asm"]["free"].apply(null, arguments)
};
## メモリリークするコードを動かすとどうなる?
1sごとに100KBメモリリークするコードを用意
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void memoryLeak(void) {
int shouldLoop = 1;
int loopCount = 0;
while(shouldLoop) {
loopCount++;
if (loopCount > 10) {
shouldLoop = 0;
}
char* p = malloc(1024 * 100); // 動的にメモリを確保。解放しないため、ループ毎にメモリリークする。
printf("memory leak: leak 100KB!\n");
usleep(1000 * 1000); // 1000msec
}
return;
}
int main(int argc, char *argv[]) {
printf("memory leak: start\n");
memoryLeak();
printf("memory leak: end\n");
return 0;
}
↓
コンパイル後ブラウザで実行し、devtoolでパフォーマンスを計測
JS Heapが増えておらず、フロント側でのメモリリークは検知されませんでした。
...どういうことだろう?
(proposalの一覧にGCがあるから、GCが働いたというわけではなさそう)
## メモリを使いまくるとどうなる?
前項でメモリリークを確認できなかったのはリーク幅が小さくて分かりにくかっただけなのでは? と思いリーク幅をひろげてみました。
100ms毎に1MB、計100MBリークするコードに書き換え、同じように検証したところ以下のエラーに遭遇。
ブラウザ、WASM、OS、どのレイヤの制限かは不明ですが、使えるメモリには限りがあるみたいです。
(当然っちゃ当然ですが)
Cannot enlarge memory arrays to size 17534976 bytes (OOM). Either (1) compile with -s INITIAL_MEMORY=X with X higher than the current value 16777216, (2) compile with -s ALLOW_MEMORY_GROWTH=1 which allows increasing the size at runtime, or (3) if you want malloc to return NULL (0) instead of this abort, compile with -s ABORTING_MALLOC=0
memoryLeak.html:1246 Cannot enlarge memory arrays to size 17534976 bytes (OOM). Either (1) compile with -s INITIAL_MEMORY=X with X higher than the current value 16777216, (2) compile with -s ALLOW_MEMORY_GROWTH=1 which allows increasing the size at runtime, or (3) if you want malloc to return NULL (0) instead of this abort, compile with -s ABORTING_MALLOC=0
memoryLeak.html:1246 exception thrown: RuntimeError: abort(Cannot enlarge memory arrays to size 17534976 bytes (OOM). Either (1) compile with -s INITIAL_MEMORY=X with X higher than the current value 16777216, (2) compile with -s ALLOW_MEMORY_GROWTH=1 which allows increasing the size at runtime, or (3) if you want malloc to return NULL (0) instead of this abort, compile with -s ABORTING_MALLOC=0 ) at Error
at jsStackTrace (http://0.0.0.0:8080/memoryLeak.js:1978:17)
at stackTrace (http://0.0.0.0:8080/memoryLeak.js:1995:16)
at abort (http://0.0.0.0:8080/memoryLeak.js:1735:44)
at abortOnCannotGrowMemory (http://0.0.0.0:8080/memoryLeak.js:2018:7)
at _emscripten_resize_heap (http://0.0.0.0:8080/memoryLeak.js:2021:7)
at wasm-function[13]:0x237a
at wasm-function[11]:0xd36
at wasm-function[8]:0x337
at wasm-function[9]:0x407
at Module._main (http://0.0.0.0:8080/memoryLeak.js:2212:32),RuntimeError: abort(Cannot enlarge memory arrays to size 17534976 bytes (OOM). Either (1) compile with -s INITIAL_MEMORY=X with X higher than the current value 16777216, (2) compile with -s ALLOW_MEMORY_GROWTH=1 which allows increasing the size at runtime, or (3) if you want malloc to return NULL (0) instead of this abort, compile with -s ABORTING_MALLOC=0 ) at Error
at jsStackTrace (http://0.0.0.0:8080/memoryLeak.js:1978:17)
at stackTrace (http://0.0.0.0:8080/memoryLeak.js:1995:16)
at abort (http://0.0.0.0:8080/memoryLeak.js:1735:44)
at abortOnCannotGrowMemory (http://0.0.0.0:8080/memoryLeak.js:2018:7)
at _emscripten_resize_heap (http://0.0.0.0:8080/memoryLeak.js:2021:7)
at wasm-function[13]:0x237a
at wasm-function[11]:0xd36
at wasm-function[8]:0x337
at wasm-function[9]:0x407
at Module._main (http://0.0.0.0:8080/memoryLeak.js:2212:32)
at abort (http://0.0.0.0:8080/memoryLeak.js:1741:9)
at abortOnCannotGrowMemory (http://0.0.0.0:8080/memoryLeak.js:2018:7)
at _emscripten_resize_heap (http://0.0.0.0:8080/memoryLeak.js:2021:7)
at wasm-function[13]:0x237a
at wasm-function[11]:0xd36
at wasm-function[8]:0x337
at wasm-function[9]:0x407
at Module._main (http://0.0.0.0:8080/memoryLeak.js:2212:32)
at callMain (http://0.0.0.0:8080/memoryLeak.js:2500:15)
at doRun (http://0.0.0.0:8080/memoryLeak.js:2562:23)
memoryLeak.js:1741 Uncaught RuntimeError: abort(Cannot enlarge memory arrays to size 17534976 bytes (OOM). Either (1) compile with -s INITIAL_MEMORY=X with X higher than the current value 16777216, (2) compile with -s ALLOW_MEMORY_GROWTH=1 which allows increasing the size at runtime, or (3) if you want malloc to return NULL (0) instead of this abort, compile with -s ABORTING_MALLOC=0 ) at Error
at jsStackTrace (http://0.0.0.0:8080/memoryLeak.js:1978:17)
at stackTrace (http://0.0.0.0:8080/memoryLeak.js:1995:16)
at abort (http://0.0.0.0:8080/memoryLeak.js:1735:44)
at abortOnCannotGrowMemory (http://0.0.0.0:8080/memoryLeak.js:2018:7)
at _emscripten_resize_heap (http://0.0.0.0:8080/memoryLeak.js:2021:7)
at wasm-function[13]:0x237a
at wasm-function[11]:0xd36
at wasm-function[8]:0x337
at wasm-function[9]:0x407
at Module._main (http://0.0.0.0:8080/memoryLeak.js:2212:32)
at abort (http://0.0.0.0:8080/memoryLeak.js:1741:9)
at abortOnCannotGrowMemory (http://0.0.0.0:8080/memoryLeak.js:2018:7)
at _emscripten_resize_heap (http://0.0.0.0:8080/memoryLeak.js:2021:7)
at wasm-function[13]:0x237a
at wasm-function[11]:0xd36
at wasm-function[8]:0x337
at wasm-function[9]:0x407
at Module._main (http://0.0.0.0:8080/memoryLeak.js:2212:32)
at callMain (http://0.0.0.0:8080/memoryLeak.js:2500:15)
at doRun (http://0.0.0.0:8080/memoryLeak.js:2562:23)
## ランタイムエラーで落ちるコードを動かすとどうなる?
最後に、ランタイムエラーになるコードを動かしてみました。
結果は、当たり前ですが、ブラウザ上でもちゃんとランタイムエラーになりました。
ただし、ランタイムエラーとなった行以降のコードも実行されました。
(普通にCとして実行した場合はエラー箇所で落ちるため、微妙に違う。結構怖い挙動。)
#include <stdio.h>
int main(void) {
int i;
char str[4];
char* p;
printf("Before Segmentation Fault\n");
for(i = 0; i < 16; i++) {
str[i] = 'a';
*p = str[i];
}
printf("After Segmentation Fault\n");
return 0;
}
# 普通にC言語としてコンパイル&実行するとエラー箇所で落ちる
$ gcc segfault.c
$ ./a.out
Before Segmentation Fault
[1] 22777 segmentation fault ./a.out
エラー時のコンソールログ
segfault.html:1246 Runtime error: The application has corrupted its heap memory area (address zero)!
segfault.js:1741 Uncaught RuntimeError: abort(Runtime error: The application has corrupted its heap memory area (address zero)!) at Error
at jsStackTrace (http://0.0.0.0:8080/segfault.js:1978:17)
at stackTrace (http://0.0.0.0:8080/segfault.js:1995:16)
at abort (http://0.0.0.0:8080/segfault.js:1735:44)
at checkStackCookie (http://0.0.0.0:8080/segfault.js:1446:46)
at postRun (http://0.0.0.0:8080/segfault.js:1538:3)
at doRun (http://0.0.0.0:8080/segfault.js:2545:5)
at http://0.0.0.0:8080/segfault.js:2554:7
at abort (http://0.0.0.0:8080/segfault.js:1741:9)
at checkStackCookie (http://0.0.0.0:8080/segfault.js:1446:46)
at postRun (http://0.0.0.0:8080/segfault.js:1538:3)
at doRun (http://0.0.0.0:8080/segfault.js:2545:5)
at http://0.0.0.0:8080/segfault.js:2554:7
## 最後に 標準入力、ファイルI/O、スレッド、セマフォ、共有メモリ、シグナル、Cからjsの関数を呼ぶ、複数の関数を定義する、ファイル分割、再帰呼び出し、速度比較...などなど。 他にも気になる部分はありますが、ひとまずはここで実験を終えます。
ゆくゆくはブラウザ上で全てが完結するようになりそうで、夢がありますね。