LoginSignup
15
7

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-31

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


WebAssemblyとは

WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine.
(公式より引用)

  • ブラウザ上で動くバイナリコードのフォーマット (名前の通り、ウェブ用のアセンブリ言語)
  • C/C++、Rustなどからコンパイル可能
  • 略称はWASM(ワズム)
  • jsのコードからWASMを呼び出せる。逆もできる。
  • Chrome, FIrefox, Safari, Edgeなどで使用可能
  • つまり、上記言語で書いたコードをブラウザ上で動かすことができるようになる
  • 速くて実行環境を選ばない
  • 主な用途は画像処理やゲームなど
  • ファイルやネットワーク、メモリへのアクセスは仕様策定中らしい(要出典)


おしながき(やったこと)

  • HelloWorldを動かしてみる
  • メモリリークするコードを動かすとどうなる?
  • メモリを使いまくるとどうなる?
  • ランタイムエラーになるコードを動かすとどうなる?


HelloWorldを動かしてみる

参考: https://laboradian.com/tried-webassembly/
(MDNと同じ手順だが、補足説明付きでより分かりやすい)

 
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を作る

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を開く

1.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)
};


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

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

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


メモリを使いまくるとどうなる?

前項でメモリリークを確認できなかったのはリーク幅が小さくて分かりにくかっただけなのでは? と思いリーク幅をひろげてみました。
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)


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

最後に、ランタイムエラーになるコードを動かしてみました。
結果は、当たり前ですが、ブラウザ上でもちゃんとランタイムエラーになりました。
ただし、ランタイムエラーとなった行以降のコードも実行されました。
(普通に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

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

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






最後に

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

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

15
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
7