ブラウザでC++をコンパイルして実行する

2022年12月5日

ブラウザにソースコードを貼り付けて、各種言語をオンラインで実行する環境は多くあります。古くはJavaScriptのJSFiddleとかですが、最近だと(言語名)-playgroundみたいな名前で、公式に運用されてるケースも多いです。

今回のターゲット言語であるC++もご多分に漏れず、いくつかの環境があります。

上の2つの環境はC++のソースファイル編集がブラウザ上で出来るものの、コンパイルと実行はサーバー上で実施されます。今回はコンパイルと実行もブラウザ上で実施してみようという試みです。

目的

ブラウザでC++をコンパイルして実行する(サーバーではなく)

背景

サーバーでコンパイル・実行する環境を構築するとなると、貧弱な設備では深刻なパフォーマンス低下が起こり、また複雑なセキュリティ配慮も必要になってしまいます。これらの問題を解決するべく、wasmを使ってクライアント側(ブラウザ)でコンパイルと実行ができないか?と試してみました。

必要なモノ

ブラウザで実行するためにはプログラムがwasmになってないといけなく、それらを適切に呼べるJavaScriptがないといけません。C++でそれらを実現するソフトとしては、emscriptenが有名です。このソフトの説明はhttps://ja.wikipedia.org/wiki/Emscriptenに譲りますが、このソフトは通常LinuxやMacやWindowsで実行するタイプの(クロス)コンパイラです。つまり、通常の使い方ではブラウザ上でコンパイルすることが出来ません。しかしこのソフト(コンパイラ)をwasmに出来れば、コンパイラをブラウザ上で実行できる=ブラウザ上でコンパイルできるようになります。

したがって必要なものはwasmにコンパイルされたemscriptenになります。ようはemscripten自体をemscriptenでコンパイルしたものです。コンパイラのソースをそのコンパイラ自身でコンパイルできるものをself hostingと言います。emscriptenの開発にもそのようなissueがあり(https://github.com/emscripten-core/emscripten/issues/6432)、そこには大きな問題もあったようです。そして、このissueの最後に出てくるEmceptionがそれらの問題を解決したと読めます。この記事ではEmceptionを実際にビルドし、付属するデモ(https://jprendes.github.io/emception/)を設置し動かしてみることで、「ブラウザでC++をコンパイルして実行」という目的を実現しようとしています。

Emception

Emceptionとは

jprendesさんが作られたブラウザでC++をコンパイルして実行するプロトタイプ環境のようです。emscriptenの構成要素であるllvmに含まれるclang(C/C++コンパイラ)はバージョン15と新しく、いくつかある似たようなデモ環境と比較してもかなり挑戦的です。ちなみに他のデモ環境は速度は速いもののコンパイラのソースが公開されていないなど、再現性に問題があり、すべてが公開されいるEmceptionとは対照的です。

またcpythonのwasm版とも言えるpyodideも組み合わせており、何でも出来る環境にした欲張りセットのような印象です。デモはしっかりpython経由になっており、当たり前ですが、かなりモッサリ動作します。

ブラウザ上でC++コンパイラやその成果物が動くためには、それらを操作するUIと、結果を保存するファイルシステムが最低限必要になります。EmceptionにもこれらがJavaScriptで書かれています。ビルド後最終的に構築されるルートファイルシステムは287MBで、brotliで圧縮してあるのですが42MBにもなり、現状初期化時のモッサリ感に拍車をかけているようです。

なお、wasiには対応しておらず、wasmtime/wasmerなどのランタイムは未使用です。

ビルドについて

githubのreadmeに書いてあるとおりにビルドすると、現状では4~5点ほど注意点・問題点があります。

1. dockerが必要です

ビルドにはdockerが必要です。ビルドツールチェーンをdockerコンテナ上に構築します。デモの実行には必要ありません。

2. zlibの当該バージョンのソースアーカイブが指定されたURLにない

理由は分かりませんが、どうもセキュリティに問題があったらしく、元のURLからは移動されたようです。emceptionのビルド中、自家製リポジトリの改変pyodideをダウンロードして、さらにemception用パッチ当ててビルドするのですが、その中でダウンロードしてビルドされるzlibのURLに問題があるということです。emceptionに対するパッチ(pyodideのemception用パッチに対するパッチ)だけ書いておきますね。

--- a/patches/pyodide.patch     2022-10-16 22:00:05.379424370 +0900
+++ b/patches/pyodide.patch     2022-10-16 21:56:16.576185694 +0900
@@ -22,6 +22,19 @@ index 7e72eb4..b6c6f24 100644
     # Strip out C++ symbols which all start __Z.
     # There are 4821 of these and they have VERY VERY long names.
     # To show some stats on the symbols you can use the following:
+diff --git a/cpython/Makefile b/cpython/Makefile
+index 2f1d8dd..ee49c4d 100644
+--- a/cpython/Makefile
++++ b/cpython/Makefile
+@@ -15,7 +15,7 @@ SYSCONFIG_NAME=_sysconfigdata__emscripten_
+ ZLIBVERSION = 1.2.11
+ ZLIBTARBALL=$(ROOT)/downloads/zlib-$(ZLIBVERSION).tar.gz
+ ZLIBBUILD=$(ROOT)/build/zlib-$(ZLIBVERSION)
+-ZLIBURL=https://zlib.net/zlib-1.2.11.tar.gz
++ZLIBURL=https://zlib.net/fossils/zlib-1.2.11.tar.gz
+ 
+ SQLITETARBALL=$(ROOT)/downloads/sqlite-autoconf-3270200.tar.gz
+ SQLITEBUILD=$(ROOT)/build/sqlite-autoconf-3270200
 diff --git a/src/js/api.js b/src/js/api.js
 index 50a867f..8b50673 100644
 --- a/src/js/api.js

例えば以下のように使います。上のファイル(emception.buxfix.patch)を置いたディレクトリで、

$ git clone https://github.com/jprendes/emception.git
$ cd emception 
$ git reset --hard 51a4e3d4b6d2739fd170a3a467bf2675b5948808
$ patch -p1 <../emception.buxfix.patch

とすればセキュリティに問題のあるバージョンのzlibが展開されてビルドできます。

3. メモリが16GBくらい必要

ディスク容量も多分15~20GBくらい必要なのですが、llvmのあるファイル(Disasm.cpp)のコンパイルに16GBくらいのメモリが必要です。メモリが足りないとOOM killerからSIGKILLが飛んできて理不尽にビルドがエラーになります。物理メモリが足りなかったらswapを積みましょう。再現性は100%です。

4. llvmの一部コンパイルがセグメンテーション違反(139)で落ちるときがある

再現性はないので、エラーがあっても何度かビルドすればそのうち全部ビルドできます。

5. とりあえずsudoでビルドした

dockerはrootでなくても実行できるようにしているが、いろいろ疑いたくなかったので、とりあえずsudo付きでビルドして、ビルド直後に

$ sudo chown -R $(id -u):$(id -g) .

した。sudoなくても問題はないかもしれない。

その他

ビルドはおよそ以下の手順で行われています。

  1. docker image(emception_build:latest)作成(build-with-docker.sh)
    ubuntu20.04ベースにdocker.io clang-12 ninja-build jq brotliパッケージを入れただけのもの。
    以降の操作はこのイメージのコンテナの中で行われる
  2. native版クロスコンパイラ用バイナリ操作自前コードのビルド(build-tooling.sh)
  3. llvmのビルド(build-llvm.sh)
    本家のとあるバージョンをダウンロード後2行ほどパッチを当ててから、native用のllvm-tblgenとclang-tblgenビルド、wasm用のフルビルド
  4. binaryenのビルド(build-binaryen.sh)
    本家のとあるバージョンをダウンロード後そのままwasm用にフルビルド
  5. pyodideのビルド(build-pyodide.sh)
    本家のとあるバージョンからフォークしたものをダウンロード後、emception用のパッチをさらに当ててからフルビルド
    pyodide内部でもcpython-3.9.5などをダウンロードしてパッチを当ててwasm用にフルビルドしている
  6. emception本体のビルド(build-emception.sh)
    ここまでで作成したwasmとルートファイルシステムに配備するファイルを掻き集めて、一纏めにしている
    ルートファイルシステムの作成とbrotli圧縮も実施
  7. demoのビルド(手打ち)
    普通のwebpackビルド

demoは編集されたソースコードをpyodideのem++.pyからビルドするだけのもののようです。

デモの配備について

./build/demoの内容を静的に配備するだけでOK。

このサイトでは https://elephantcat.work/emception-demo/index.html でしたが、google様に怒られたのでリンクを外しています。サーバー/にnoindexを付けると見ることができます(動くことの確認用です)。

デモの実行について

chromeだとスタックオーバーフローで動作しない。開発ツールを開きながらだと動作する。現時点ではfirefoxならwindows/linux/androidいずれでも動作した。確認に使ったのは先日のバブルソート。

#include <chrono>
#include <iostream>
#include <vector>
using namespace std;
using namespace std::chrono;
template<typename T, typename S> S& operator <<(S& s, const vector<T>& v) {
    s << "[";
    bool is_first = true;
    for (auto e: v) {
        if (is_first) {
            is_first = false;
        } else {
            s << ",";
        }
        s << e;
    }
    s << "]";
    return s;
}
int main() {
    const size_t n = 50000;
    vector<uint32_t> v(n);
    for (size_t i = 0; i < v.size(); ++i) v[i] = v.size() - i - 1;
    cout << decltype(v)(v.begin(), v.begin() + 10) << endl;
    auto start = high_resolution_clock::now();
    for (int i = 0; i < v.size() - 1; ++i) {
        for (int j = i + 1; j < v.size(); ++j) {
            if (v[i] > v[j]) {
                auto tmp = v[i];
                v[i] = v[j];
                v[j] = tmp;
            }
        }
    }
    cout << "Elapsed time: " << duration_cast<milliseconds>(high_resolution_clock::now() - start).count() << "ms" << endl;
    cout << decltype(v)(v.begin(), v.begin() + 10) << endl;
    return 0;
}

結論

emcetionはchromeで動作しないなどの品質的な問題、動作がモッサリするなどの性能上の問題を抱えているが、それらの問題を気にしなければC++コードをブラウザ上でコンパイル実行できた。

考察

今がwasiに沿ったランタイムに依存するアプリへの過渡期と考えると、いじって動かせるemceptionのような環境は渡りに船といったところ。ただしビルドにかかる時間も実行時間も半端ないので、他力本願だけでなく、今後は自分でよりシンプルで軽い環境を作ってみたい。