C言語で書いたHelloWorld(+α)をJavaScriptから動かしてみた【WebAssembly】

ブラウザ上でC言語のコードを実行するって、一体全体どういうことだろう?
前職でCを扱っていたこともあって、WebAssemblyは気になる技術のひとつでした。
この記事では、HelloWorldに加えて、ちょっとした実験の結果をまとめてみました。

<br>
## 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などで使用可能
* つまり、上記言語で書いたコードをブラウザ上で動かすことができるようになる
* 速くて実行環境を選ばない
* [主な用途](https://webassembly.org/docs/use-cases/)は画像処理やゲームなど
* ファイルやネットワーク、メモリへのアクセスは仕様策定中らしい(要出典)

<br>
## おしながき(やったこと)
* HelloWorldを動かしてみる
* メモリリークするコードを動かすとどうなる?
* メモリを使いまくるとどうなる?
* ランタイムエラーになるコードを動かすとどうなる?

<br>
## HelloWorldを動かしてみる
参考: https://laboradian.com/tried-webassembly/
([MDN](https://developer.mozilla.org/ja/docs/WebAssembly/C_to_wasm)と同じ手順だが、補足説明付きでより分かりやすい)

　
**[Emscripten](https://emscripten.org/docs/getting_started/downloads.html)(コンパイラ)をインストール**

```
# 今回インストールしたバージョン
$ 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を作る**

```hello.c
#include <stdio.h>

int main(int argc, char *argv[]) {
  printf("Hello World\n");
  return 0;
}
```

　↓

**コンパイル**

```bash
# コンパイル前
$ 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サーバを立てる**

```bash
# 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を開く**

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162371/d0da3f5a-17a3-e67a-d24a-98fbaabe1400.png)
※ ロゴとかターミナル風の画面は、コンパイラが生成したものです
　(もちろん、オプション次第でwasm + jsだけの出力もできます)


　
なお、HelloWorldをコンパイルした時に自動生成されたコードはこんな感じでした。
(しっかり解析できていない＆大量にあるため、雰囲気をつかみやすいところだけ抜粋しました)

```hello.html
<!-- ロゴとかターミナル風な画面とか余計なものは割愛 -->
<script async type="text/javascript" src="hello.js"></script>
```

```hello.js
// 実際のコードは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)
};
```


<br>
## メモリリークするコードを動かすとどうなる?

**1sごとに100KBメモリリークするコードを用意**

```memoryLeak.c
#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でパフォーマンスを計測**

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162371/b9a5cb3a-d619-cc5e-a5c3-1713fe791ae2.png)

JS Heapが増えておらず、フロント側でのメモリリークは検知されませんでした。
...どういうことだろう?
([proposalの一覧](https://github.com/WebAssembly/proposals)にGCがあるから、GCが働いたというわけではなさそう)


<br>
## メモリを使いまくるとどうなる?

前項でメモリリークを確認できなかったのはリーク幅が小さくて分かりにくかっただけなのでは? と思いリーク幅をひろげてみました。
100ms毎に1MB、計100MBリークするコードに書き換え、同じように検証したところ以下のエラーに遭遇。

ブラウザ、WASM、OS、どのレイヤの制限かは不明ですが、使えるメモリには限りがあるみたいです。
(当然っちゃ当然ですが)

```console.log
 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)
```

<br>
## ランタイムエラーで落ちるコードを動かすとどうなる?

最後に、ランタイムエラーになるコードを動かしてみました。
結果は、当たり前ですが、ブラウザ上でもちゃんとランタイムエラーになりました。
ただし、ランタイムエラーとなった行以降のコードも実行されました。
(普通にCとして実行した場合はエラー箇所で落ちるため、微妙に違う。結構怖い挙動。)

　

```segfault.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
```

　
ブラウザから実行するとエラー箇所の後も処理が継続
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162371/3facf8f2-b54e-251f-ba18-d52a38550561.png)


エラー時のコンソールログ

```console.log
 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
```


<br>
<br>
<br>
## 最後に
標準入力、ファイルI/O、スレッド、セマフォ、共有メモリ、シグナル、Cからjsの関数を呼ぶ、複数の関数を定義する、ファイル分割、再帰呼び出し、速度比較...などなど。
他にも気になる部分はありますが、ひとまずはここで実験を終えます。

ゆくゆくはブラウザ上で全てが完結するようになりそうで、夢がありますね。