Open52

C / C++ 言語のプログラムを WebAssembly を使って Web ページ上で動かす

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

このスクラップについて

Web ページ上でロボット制御フレームワークの ARCS6 を使用してロボットアームのシミュレーションを実行するシステムの開発を検討している。

AWS RoboMaker のようにサーバーでシミュレーションを実行しても悪くはないが可能であればブラウザで完結できれば素晴らしい。

ARCS6 は C++ で書かれているので C/C++ 言語で書かれた制御プログラムを WebAssembly を使って Web ページ上で動かせるかどうかを試してみる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

shell_minimal.html がない

MDN のドキュメントのカスタム HTML テンプレートを使うセクションで shell_minimal.html を emsdk のリポジトリから探すとの説明があるがざっと見た感じでは見つからなさそう。

一旦 Emscripten のチュートリアルに切り替えて終わったら MDN に戻ってこようかな。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

インストールがなかなか終わらないので Docker を使う

コマンド
docker run --rm -v $(pwd):/src -u $(id -u):$(id -g) \
  emscripten/emsdk emcc hello.c -o hello.html

こちらはこちらで Docker イメージのダウンロードがなかなか終わらない。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Wasm の出力は Node でも動かせる

コマンド
node a.out.js

これは面白い、インストールか Docker イメージのダウンロードが終わったら試してみたい。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Docker イメージのダウンロードの方が先に終わった

コマンド
docker run --rm -v $(pwd):/src -u $(id -u):$(id -g) \
  emscripten/emsdk emcc hello.c -o hello.html
実行結果
cache:INFO: generating system asset: symbol_lists/11506546530a3da28a96474fd7960ff397e27fd9.txt... (this will be cached in "/emsdk/upstream/emscripten/cache/symbol_lists/11506546530a3da28a96474fd7960ff397e27fd9.txt" for subsequent builds)
cache:INFO:  - ok

作成されたファイルは下記の 3 つ。

  • hello.html
  • hello.js
  • hello.wasm

まずは hello.html をブラウザで開いてみる。

普通に開くとずっと Preparing の状態になる。

デベロッパーツールでコンソール出力を確認したところ XHR が失敗しているようだ。

コンソール出力
Access to XMLHttpRequest at 'file:///Users/susukida/workspace/wasm/hello-wasm/hello.wasm' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, isolated-app, chrome-extension, chrome, https, chrome-untrusted.

ローカルで Web サーバーを起動する。

コマンド
python3 -m http.server

http://localhost:8000/hello.html にアクセスする。

今度はうまくいった!

Node.js でも実行できるということなので試してみる。

コマンド
node hello.js
実行結果
Hello World

素晴らしい。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

やっと Homebrew のインストールが終わった

コマンド
emcc -v
実行結果
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.32-git
clang version 17.0.0 (https://github.com/llvm/llvm-project.git df82394e7a2d06506718cafa347bf7827c79fc4f)
Target: wasm32-unknown-emscripten
Thread model: posix
InstalledDir: /usr/local/Cellar/emscripten/3.1.32/libexec/llvm/bin
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Canvas に描画

Emscripten のチュートリアルにもっと面白いサンプルがあったので試してみる。

https://emscripten.org/docs/getting_started/Tutorial.html#generating-html

コマンド
touch hello_world_sdl.cpp
hello_world_sdl.cpp
#include <stdio.h>
#include <SDL/SDL.h>

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

int main(int argc, char** argv) {
  printf("hello, world!\n");

  SDL_Init(SDL_INIT_VIDEO);
  SDL_Surface *screen = SDL_SetVideoMode(256, 256, 32, SDL_SWSURFACE);

#ifdef TEST_SDL_LOCK_OPTS
  EM_ASM("SDL.defaults.copyOnLock = false; SDL.defaults.discardOnLock = true; SDL.defaults.opaqueFrontBuffer = false;");
#endif

  if (SDL_MUSTLOCK(screen)) SDL_LockSurface(screen);
  for (int i = 0; i < 256; i++) {
    for (int j = 0; j < 256; j++) {
#ifdef TEST_SDL_LOCK_OPTS
      // Alpha behaves like in the browser, so write proper opaque pixels.
      int alpha = 255;
#else
      // To emulate native behavior with blitting to screen, alpha component is ignored. Test that it is so by outputting
      // data (and testing that it does get discarded)
      int alpha = (i+j) % 255;
#endif
      *((Uint32*)screen->pixels + i * 256 + j) = SDL_MapRGBA(screen->format, i, j, 255-i, alpha);
    }
  }
  if (SDL_MUSTLOCK(screen)) SDL_UnlockSurface(screen);
  SDL_Flip(screen); 

  printf("you should see a smoothly-colored square - no sharp lines but the square borders!\n");
  printf("and here is some text that should be HTML-friendly: amp: |&| double-quote: |\"| quote: |'| less-than, greater-than, html-like tags: |<cheez></cheez>|\nanother line.\n");

  SDL_Quit();

  return 0;
}

下記を追記する。

makefile
hello_world_sdl.html: hello_world_sdl.c
	emcc $^ -o $@
コマンド
make hello_world_sdl.html

ブラウザで http://localhost:8000/hello_world_sdl.html にアクセスする。

カラフルな画像が表示された。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

まだちょっとしか触っていないけど WebAssembly すごいな。

これで C 言語プログラムを Web 上に移植できたら素晴らしいと思うけど、そんなに簡単にはできなさそう。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

次はファイルの読み書き

https://emscripten.org/docs/getting_started/Tutorial.html#using-files

当り前だけどブラウザで実行時はローカルのファイルシステムにはアクセスできない。

ビルド時に下記のようにプリロードする。

コマンド
./emcc test/hello_world_file.cpp -o hello.html --preload-file test/hello_world_file.txt

イメージ的には埋め込んでいる(embed)している感じだが違うのかな?

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

まずは gcc でビルドして実行してみる

コマンド
touch hello_world_file.c
touch hello_world_file.txt
hello_world_file.c
#include <stdio.h>

int main() {
  FILE *file = fopen("hello_world_file.txt", "rb");

  if (!file) {
    printf("cannot open file\n");
    return 1;
  }

  while (!feof(file)) {
    char c = fgetc(file);
    if (c != EOF) {
      putchar(c);
    }
  }

  fclose(file);
  return 0;
}
hello_world_file.txt
==
This data has been read from a file.
The file is readable as if it were at the same location in the filesystem, including directories, as in the local filesystem where you compiled the source.
==

makefile に下記を追記する。

makefile
hello_world_file: hello_world_file.c
	gcc $^ -o $@
コマンド
make hello_world_file
./hello_world_file
実行結果
==
This data has been read from a file.
The file is readable as if it were at the same location in the filesystem, including directories, as in the local filesystem where you compiled the source.
==

いいね。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

次に emcc でビルドして実行してみる

makefile に下記を追記する。

makefile
hello_world_file.html: hello_world_file.c hello_world_file.txt
	emcc $< -o $@ --preload-file hello_world_file.txt

http://localhost:8000/hello_world_file.html にアクセスする。

いい感じだけど最後の == が表示されていない。

テキストファイルはちゃんと改行しないとダメなのかな?

どうやらそのようだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

コードの最適化

下記のように -O オプションを使用することでコードの最適化が行われる。

コマンド
emcc -O1 test/hello_world.cpp

実際にやってみる。

コマンド
emcc -O1 hello.c -o hello_o1.js
emcc -O2 hello.c -o hello_o2.js

コンパイルに時間がかかる、初回だけ?

比較してみる。

オプション .js のファイルサイズ .wasm のファイルサイズ
-O0 63,146 12,177
-O1 38,383 2,425
-O2 13,333 2,030

確かにファイルサイズが最適化すると小さくなることがわかる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

テストケースの実行

まずは Emscripten の GitHub レポジトリをクローンする。

コマンド
git clone https://github.com/emscripten-core/emscripten
cd emscripten
test/runner test_loop

下記のエラーメッセージが表示される。

エラーメッセージ
Traceback (most recent call last):
  File "test/runner.py", line 37, in <module>
    import jsrun
  File "/Users/susukida/workspace/wasm/hello-wasm/emscripten/test/jsrun.py", line 6, in <module>
    import common
  File "/Users/susukida/workspace/wasm/hello-wasm/emscripten/test/common.py", line 31, in <module>
    import clang_native
  File "/Users/susukida/workspace/wasm/hello-wasm/emscripten/test/clang_native.py", line 10, in <module>
    from tools.shared import PIPE, run_process, CLANG_CC, CLANG_CXX
ModuleNotFoundError: No module named 'tools.shared'

tools.shared がインポートできないということだが、tools.shared とは emscripten のソースコードに含まれる tools/shared.py のことだろうか?

あるように思えるのだが Python がよくわからない。

ビルドが必要なのかな?

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

今更だけど emsdk を使って Emscripten をインストールする

コマンド
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest

こちらもエラーメッセージが表示される。

メッセージ
Resolving SDK alias 'latest' to '3.1.32'
Resolving SDK version '3.1.32' to 'sdk-releases-29ad1037cd6b99e5d8a1bd75bc188c1e9a6fda8d-64bit'
Installing SDK 'sdk-releases-29ad1037cd6b99e5d8a1bd75bc188c1e9a6fda8d-64bit'..
Installing tool 'node-14.18.2-64bit'..
Error: Downloading URL 'https://storage.googleapis.com/webassembly/emscripten-releases-builds/deps/node-v14.18.2-darwin-x64.tar.gz': <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1091)>
Warning: Possibly SSL/TLS issue. Update or install Python SSL root certificates (2048-bit or greater) supplied in Python folder or https://pypi.org/project/certifi/ and try again.
error: installation failed!

pip install certifi もやってみたけどダメだった。

関連しそうなイシューは下記の通り。

https://github.com/emscripten-core/emscripten/issues/9036

Hillsie さんによると open /Applications/Python\ 3.7/Install\ Certificates.command を実行することで解決できるらしい。

https://github.com/emscripten-core/emscripten/issues/9036#issuecomment-532092743

上記のエラーが出なくなった!ありがとう Hillsie さん。

無事に SDK のインストールが終わったようだ。

コンソール出力もそれほど長くないので全文を下記に示しておく。

コンソール出力
Resolving SDK alias 'latest' to '3.1.32'
Resolving SDK version '3.1.32' to 'sdk-releases-29ad1037cd6b99e5d8a1bd75bc188c1e9a6fda8d-64bit'
Installing SDK 'sdk-releases-29ad1037cd6b99e5d8a1bd75bc188c1e9a6fda8d-64bit'..
Installing tool 'node-14.18.2-64bit'..
Downloading: /Users/susukida/workspace/wasm/hello-wasm/emsdk/zips/node-v14.18.2-darwin-x64.tar.gz from https://storage.googleapis.com/webassembly/emscripten-releases-builds/deps/node-v14.18.2-darwin-x64.tar.gz, 32076686 Bytes
Unpacking '/Users/susukida/workspace/wasm/hello-wasm/emsdk/zips/node-v14.18.2-darwin-x64.tar.gz' to '/Users/susukida/workspace/wasm/hello-wasm/emsdk/node/14.18.2_64bit'
Done installing tool 'node-14.18.2-64bit'.
Installing tool 'python-3.9.2-64bit'..
Downloading: /Users/susukida/workspace/wasm/hello-wasm/emsdk/zips/python-3.9.2-3-macos-x86_64.tar.gz from https://storage.googleapis.com/webassembly/emscripten-releases-builds/deps/python-3.9.2-3-macos-x86_64.tar.gz, 31899321 Bytes
Unpacking '/Users/susukida/workspace/wasm/hello-wasm/emsdk/zips/python-3.9.2-3-macos-x86_64.tar.gz' to '/Users/susukida/workspace/wasm/hello-wasm/emsdk/python/3.9.2_64bit'
Done installing tool 'python-3.9.2-64bit'.
Installing tool 'releases-29ad1037cd6b99e5d8a1bd75bc188c1e9a6fda8d-64bit'..
Downloading: /Users/susukida/workspace/wasm/hello-wasm/emsdk/zips/29ad1037cd6b99e5d8a1bd75bc188c1e9a6fda8d-wasm-binaries.tbz2 from https://storage.googleapis.com/webassembly/emscripten-releases-builds/mac/29ad1037cd6b99e5d8a1bd75bc188c1e9a6fda8d/wasm-binaries.tbz2, 348020053 Bytes
Unpacking '/Users/susukida/workspace/wasm/hello-wasm/emsdk/zips/29ad1037cd6b99e5d8a1bd75bc188c1e9a6fda8d-wasm-binaries.tbz2' to '/Users/susukida/workspace/wasm/hello-wasm/emsdk/upstream'
Done installing tool 'releases-29ad1037cd6b99e5d8a1bd75bc188c1e9a6fda8d-64bit'.
Done installing SDK 'sdk-releases-29ad1037cd6b99e5d8a1bd75bc188c1e9a6fda8d-64bit'.
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

SDK の有効化

コマンド
./emsdk activate latest
コンソール出力
Resolving SDK alias 'latest' to '3.1.32'
Resolving SDK version '3.1.32' to 'sdk-releases-29ad1037cd6b99e5d8a1bd75bc188c1e9a6fda8d-64bit'
Setting the following tools as active:
   node-14.18.2-64bit
   python-3.9.2-64bit
   releases-29ad1037cd6b99e5d8a1bd75bc188c1e9a6fda8d-64bit

Next steps:
- To conveniently access emsdk tools from the command line,
  consider adding the following directories to your PATH:
    /Users/susukida/workspace/wasm/hello-wasm/emsdk
    /Users/susukida/workspace/wasm/hello-wasm/emsdk/node/14.18.2_64bit/bin
    /Users/susukida/workspace/wasm/hello-wasm/emsdk/upstream/emscripten
- This can be done for the current shell by running:
    source "/Users/susukida/workspace/wasm/hello-wasm/emsdk/emsdk_env.sh"
- Configure emsdk in your shell startup scripts by running:
    echo 'source "/Users/susukida/workspace/wasm/hello-wasm/emsdk/emsdk_env.sh"' >> $HOME/.zprofile
コマンド
source ./emsdk_env.sh
コンソール出力
Setting up EMSDK environment (suppress these messages with EMSDK_QUIET=1)
Adding directories to PATH:
PATH += /Users/susukida/workspace/wasm/hello-wasm/emsdk
PATH += /Users/susukida/workspace/wasm/hello-wasm/emsdk/upstream/emscripten
PATH += /Users/susukida/workspace/wasm/hello-wasm/emsdk/node/14.18.2_64bit/bin

Setting environment variables:
PATH = /Users/susukida/workspace/wasm/hello-wasm/emsdk:/Users/susukida/workspace/wasm/hello-wasm/emsdk/upstream/emscripten:/Users/susukida/workspace/wasm/hello-wasm/emsdk/node/14.18.2_64bit/bin:/Users/susukida/Downloads/google-cloud-sdk/bin:/Users/susukida/.local/share/solana/install/active_release/bin:/Users/susukida/.nvm/versions/node/v16.17.0/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/go/bin:/usr/local/share/dotnet:/opt/X11/bin:~/.dotnet/tools:/Library/Apple/usr/bin:/Library/Frameworks/Mono.framework/Versions/Current/Commands:/Users/susukida/.rover/bin:/Users/susukida/.cargo/bin:/usr/local/sbin:/Users/susukida/bin:/Users/susukida/development/flutter/bin
EMSDK = /Users/susukida/workspace/wasm/hello-wasm/emsdk
EMSDK_NODE = /Users/susukida/workspace/wasm/hello-wasm/emsdk/node/14.18.2_64bit/bin/node
EMSDK_PYTHON = /Users/susukida/workspace/wasm/hello-wasm/emsdk/python/3.9.2_64bit/bin/python3
SSL_CERT_FILE = /Users/susukida/workspace/wasm/hello-wasm/emsdk/python/3.9.2_64bit/lib/python3.9/site-packages/certifi/cacert.pem

which emcc を実行すると /Users/susukida/workspace/wasm/hello-wasm/emsdk/upstream/emscripten/emcc になっているのでしっかり変わっている。

ついでに Node.js なども変わっている。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

この状態でテストをしてみる

コマンド
cd ../emscripten
test/runner
コンソール出力
runner: warning: config file not found: /Users/susukida/workspace/wasm/hello-wasm/emscripten/.emscripten.  You can create one by hand or run `emcc --generate-config`
runner: error: BINARYEN_ROOT not set in config (/Users/susukida/workspace/wasm/hello-wasm/emscripten/.emscripten), and `wasm-opt` not found in PATH

出力が変化した。

emcc --generate-config を実行すれば良いのかな?

コンソール出力
emcc: error: config file already exists: `/Users/susukida/workspace/wasm/hello-wasm/emsdk/.emscripten`

既にあると言われてしまう、emsdk のディレクトリで実行してみたらどうなるだろう。

コマンド
cd ../emsdk
../emscripten/test/runner
コンソール出力
emcc: error: config file already exists: `/Users/susukida/workspace/wasm/hello-wasm/emsdk/.emscripten`

ダメでした。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

そうだ emsdk の emscripten でテスト実行したらどうだろう

コマンド
cd upstream/emscripten
test/runner
コンソール出力
posixtestsuite not found (run git submodule update --init?)
Test suites:
[]

何だか大丈夫そうな感じがする。

コマンド
test/runner test_loop
コンソール出力
Test suites:
['test_core']
Running test_core: (1 tests)
(checking sanity from test runner)
shared:INFO: (Emscripten: Running sanity checks)
test_loop (test_core.core0) ... Creating new test output directory
cache:INFO: generating system asset: symbol_lists/ff82d52061535c7ef60c6d95bb5924759542de06.txt... (this will be cached in "/Users/susukida/workspace/wasm/hello-wasm/emsdk/upstream/emscripten/cache/symbol_lists/ff82d52061535c7ef60c6d95bb5924759542de06.txt" for subsequent builds)
cache:INFO:  - ok
ok

----------------------------------------------------------------------
Ran 1 test in 2.158s

OK

無事に実行できた。

ところで test_loop とは何なんだろう?

test_core.py によるとどうやら下記のテストコードのことらしい。

https://github.com/emscripten-core/emscripten/blob/main/test/core/test_loop.c

テストの実行結果(標準出力)は out/test/stdout に保存される。

out/test/stdout
*1800*
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

MDN に戻る前に

make の代わりに emmake make を使うことで WebAssembly 用にビルドできるらしい。

せっかくなので ARCS6 の Docker に emsdk を使って emscripten をインストールして ARCS6 をビルドしてみる。

ARCS6 をビルドするための Docker コンテナを作成する方法は下記のスクラップで説明しています。

https://zenn.dev/tatsuyasusukida/scraps/47a61dee5d60ba

コマンド(ホスト側)
git clone https://github.com/emscripten-core/emsdk.git
コマンド(コンテナ側)
cd ~/src/emsdk/
./emsdk install latest

python がないとのエラーが表示される。

コマンド(コンテナ側)
yum install -y python39
python3 --version

また python がないとのエラーが表示される。

コマンド(コンテナ側)
python3 emsdk.py install latest

今度はうまくいったと思ったらエラーが表示された。

エラーメッセージ
Unpacking '/root/src/emsdk/zips/29ad1037cd6b99e5d8a1bd75bc188c1e9a6fda8d-wasm-binaries.tbz2' to '/root/src/emsdk/upstream'
tar (child): lbzip2: Cannot exec: No such file or directory
tar (child): Error is not recoverable: exiting now
tar: Child returned status 2
tar: Error is not recoverable: exiting now
['tar', '-xf', '/root/src/emsdk/zips/29ad1037cd6b99e5d8a1bd75bc188c1e9a6fda8d-wasm-binaries.tbz2', '--strip', '1'] failed with error code 2!
error: installation failed!

下記の記事によると yum install bzip2 が必要になるらしい。

https://qiita.com/bezeklik/items/e2ec12c02b49b0e8b379

https://svennd.be/lbzip2-cannot-exec-no-such-file-or-directory/

再実行するとまたダウンロードする所から始まる...

今度は無事に成功した!

コマンド(コンテナ側)
python3 emsdk.py activate latest
source ./emsdk_env.sh

また python が見つからないとのエラーが表示される。

which コマンドがないからかな? yum install which でインストールする。

コンソール出力
Setting up EMSDK environment (suppress these messages with EMSDK_QUIET=1)
Adding directories to PATH:
PATH += /root/src/emsdk
PATH += /root/src/emsdk/upstream/emscripten
PATH += /root/src/emsdk/node/14.18.2_64bit/bin

Setting environment variables:
PATH = /root/src/emsdk:/root/src/emsdk/upstream/emscripten:/root/src/emsdk/node/14.18.2_64bit/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
EMSDK = /root/src/emsdk
EMSDK_NODE = /root/src/emsdk/node/14.18.2_64bit/bin/node

できた!これで emcc が実行できるようになった。

試しに ARCS6 をビルドしてみる。

コマンド(コンテナ側)
cd ~/src/arcs6/ARCS6/robot/sample/00_空の基本コード/
emmake make

普通に gcc が実行されてしまったので makefile を見てみる。

makefile をみた感じだと addon, lib, src, sys の 4 つのそれぞれで make が行われている模様。

試しに lib のディレクトリに移動して Makefile を少し書き換えて試してみる。

arcs6/ARCS6/lib/Makefile
CC = emcc
LD = lld

この状態で make を実行。

エラーメッセージ
emcc -O2 -pipe -I. -I../src -I../sys -I/usr/src/linux/include -Wall -Weffc++ -std=c++17 -ftree-vectorize -march=native -fPIE -DARCS_IN -c CuiPlot.cc
clang++: warning: argument unused during compilation: '-march=native' [-Wunused-command-line-argument]
In file included from CuiPlot.cc:14:
In file included from ./CuiPlot.hh:22:
./FrameGraphics.hh:27:10: fatal error: 'linux/fb.h' file not found
#include <linux/fb.h>
         ^~~~~~~~~~~~
1 error generated.
emcc: error: '/root/src/emsdk/upstream/bin/clang++ -target wasm32-unknown-emscripten -fignore-exceptions -fvisibility=default -mllvm -combiner-global-alias-analysis=false -mllvm -enable-emscripten-sjlj -mllvm -disable-lsr -DEMSCRIPTEN --sysroot=/root/src/emsdk/upstream/emscripten/cache/sysroot -Xclang -iwithsysroot/include/fakesdl -Xclang -iwithsysroot/include/compat -O2 -pipe -I. -I../src -I../sys -I/usr/src/linux/include -Wall -Weffc++ -std=c++17 -ftree-vectorize -march=native -fPIE -DARCS_IN -c CuiPlot.cc -o CuiPlot.o' failed (returned 1)
make: *** [Makefile:40: CuiPlot.o] Error 1

予想以上にビルドできたけど linux/fb.h がインストールできなくて止まってしまった。

確かに /usr/src/linux/include はない。

gcc ではコンパイルが成功するのに不思議。

試しに -I /usr/src/kernels/4.18.0-425.10.1.el8_7.x86_64/include を追加したらどうなるだろう。

エラーメッセージ
emcc -O2 -pipe -I. -I../src -I../sys -I/usr/src/linux/include -I/usr/src/kernels/4.18.0-425.10.1.el8_7.x86_64/include -Wall -Weffc++ -std=c++17 -ftree-vectorize -march=native -fPIE -DARCS_IN -c CuiPlot.cc
clang++: warning: argument unused during compilation: '-march=native' [-Wunused-command-line-argument]
In file included from CuiPlot.cc:14:
In file included from ./CuiPlot.hh:22:
In file included from ./FrameGraphics.hh:27:
In file included from /usr/src/kernels/4.18.0-425.10.1.el8_7.x86_64/include/linux/fb.h:5:
In file included from /usr/src/kernels/4.18.0-425.10.1.el8_7.x86_64/include/linux/kgdb.h:16:
/usr/src/kernels/4.18.0-425.10.1.el8_7.x86_64/include/linux/linkage.h:8:10: fatal error: 'asm/linkage.h' file not found
#include <asm/linkage.h>
         ^~~~~~~~~~~~~~~
1 error generated.
emcc: error: '/root/src/emsdk/upstream/bin/clang++ -target wasm32-unknown-emscripten -fignore-exceptions -fvisibility=default -mllvm -combiner-global-alias-analysis=false -mllvm -enable-emscripten-sjlj -mllvm -disable-lsr -DEMSCRIPTEN --sysroot=/root/src/emsdk/upstream/emscripten/cache/sysroot -Xclang -iwithsysroot/include/fakesdl -Xclang -iwithsysroot/include/compat -O2 -pipe -I. -I../src -I../sys -I/usr/src/linux/include -I/usr/src/kernels/4.18.0-425.10.1.el8_7.x86_64/include -Wall -Weffc++ -std=c++17 -ftree-vectorize -march=native -fPIE -DARCS_IN -c CuiPlot.cc -o CuiPlot.o' failed (returned 1)
make: *** [Makefile:40: CuiPlot.o] Error 1

今度は別のエラーで失敗する。

せっかくなので src と sys でも同様にビルドしようと思ったがこれらは単体ではビルドできないみたい。

予想はしていたが一発でビルドが通るなんてことは無いが学び甲斐があって楽しい。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

MDN に戻ってカスタム HTML テンプレートを使用する

https://developer.mozilla.org/ja/docs/WebAssembly/C_to_wasm#カスタム_html_テンプレートを使う

コマンド
touch hello2.c
hello2.c
#include <stdio.h>

int main() {
    printf("Hello World\n");
    return 0;
}
コマンド
emcc -o hello2.html hello2.c --shell-file emscripten/src/shell_minimal.html

emscripten は emsdk には含まれないみたいなので emscripten のリポジトリをチェックアウトする必要がある。

最適化すると HTML から改行がなくなるので無効にしている。

コマンド
python3 -m http.server

ブラウザで http://localhost:8000/hello2.html にアクセスする。

シンプルなページが表示された。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

C の関数を JavaScript から呼び出す

コマンド
touch hello3.c
hello3.c
#include <stdio.h>
#include <emscripten/emscripten.h>

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

#ifdef __cplusplus
#define EXTERN extern "C"
#else
#define EXTERN
#endif

EXTERN EMSCRIPTEN_KEEPALIVE void myFunction(int argc, char ** argv) {
    printf("MyFunction Called\n");
}
コマンド
emcc -o hello3.html hello3.c \
  --shell-file emscripten/src/shell_minimal.html \
  -s NO_EXIT_RUNTIME=1 \
  -s "EXPORTED_RUNTIME_METHODS=['ccall']"

デベロッパーツールを開いて下記のコードを評価する。

Module.ccall("myFunction", null, null, null)

無事に MyFunction を呼ぶことができた。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

新しいワークスペースを作成する

ワークスペースが散らかってきたので。

コマンド
mkdir hello-libwebp
cd hello-libwebp
git clone https://github.com/webmproject/libwebp
touch webp.c
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

ビルド

コマンド
emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
  -I libwebp \
  webp.c \
  libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c
コンソール出力
emcc: warning: EXTRA_EXPORTED_RUNTIME_METHODS is deprecated, please use EXPORTED_RUNTIME_METHODS instead [-Wdeprecated]
cache:INFO: generating system asset: symbol_lists/9505c82c5b64656d1d38ff02f372d469a24fc536.txt... (this will be cached in "/Users/susukida/workspace/wasm/hello-wasm/emsdk/upstream/emscripten/cache/symbol_lists/9505c82c5b64656d1d38ff02f372d469a24fc536.txt" for subsequent builds)
cache:INFO:  - ok

一発でビルドが通った、すごいな。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

動作確認

コマンド
touch a.out.html
a.out.html
<script src="./a.out.js"></script>
<script>
  Module.onRuntimeInitialized = async () => {
    const api = {
      version: Module.cwrap("version", "number", []),
    };
    document.write(`<h1>${api.version()}</h1>`);
  };
</script>
コマンド
python3 -m http.server

ブラウザで http://localhost:8000/a.out.html にアクセスする。

バージョン番号が表示されている、素晴らしい。

ちなみに 66304 は 16 進数だと 10300 なのでバージョンは 1.3.0 のようだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

エンコードの途中経過

webp.c
#include <stdlib.h>
#include "emscripten.h"
#include "src/webp/encode.h"

EMSCRIPTEN_KEEPALIVE
int version() {
  return WebPGetEncoderVersion();
}

EMSCRIPTEN_KEEPALIVE
uint8_t* create_buffer(int width, int height) {
  return malloc(width * height * 4 * sizeof(uint8_t));
}

EMSCRIPTEN_KEEPALIVE
void destroy_buffer(uint8_t* p) {
  free(p);
}

int result[2];

EMSCRIPTEN_KEEPALIVE
void encode(uint8_t* img_in, int width, int height, float quality) {
  uint8_t* img_out;
  size_t size;

  size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);

  result[0] = (int)img_out;
  result[1] = size;
}

EMSCRIPTEN_KEEPALIVE
void free_result(uint8_t* result) {
  WebPFree(result);
}

EMSCRIPTEN_KEEPALIVE
int get_result_pointer() {
  return result[0];
}

EMSCRIPTEN_KEEPALIVE
int get_result_size() {
  return result[1];
}
a.out.html
<script src="./a.out.js"></script>
<script>
  Module.onRuntimeInitialized = async () => {
    const api = {
      version: Module.cwrap("version", "number", []),
      create_buffer: Module.cwrap("create_buffer", "number", [
        "number",
        "number",
      ]),
      destroy_buffer: Module.cwrap("destroy_buffer", "", ["number"]),
      encode: Module.cwrap("encode", "", [
        "number",
        "number",
        "number",
        "number",
      ]),
      free_result: Module.cwrap("free_result", "", ["number"]),
      get_result_pointer: Module.cwrap("get_result_pointer", "number", []),
      get_result_size: Module.cwrap("get_result_size", "number", []),
    };

    const image = await loadImage("./image.jpg");
    const p = api.create_buffer(image.width, image.height);
    Module.HEAP8.set(image.data, p);

    api.encode(p, image.width, image.height, 100);

    const resultPointer = api.get_result_pointer();
    const resultSize = api.get_result_size();
    const resultView = new Uint8Array(
      Module.HEAP8.buffer,
      resultPointer,
      resultSize
    );

    const result = new Uint8Array(resultView);
    const blob = new Blob([result], { type: "image/webp" });
    const blobURL = URL.createObjectURL(blob);
    const img = document.createElement("img");

    img.src = blobURL;

    document.body.appendChild(img);

    api.free_result(resultPointer);
    api.destroy_buffer(p);
  };

  async function load(src) {
    const imgBlob = await fetch(src).then((resp) => resp.blob());
    const img = await createImageBitmap(imgBlob);
    const canvas = document.createElement("canvas");

    canvas.width = img.width;
    canvas.height = img.height;

    const ctx = canvas.getContext("2d");
    ctx.drawImage(img, 0, 0);
    return ctx.getImageData(0, 0, img.width, img.height);
  }
</script>

画像は https://googlechrome.github.io/samples/webassembly/image.jpg からダウンロードする。

ビルドコマンドは下記の通り。

コマンド
emcc -s WASM=1 -s EXPORTED_RUNTIME_METHODS='["cwrap"]' \
    -s ALLOW_MEMORY_GROWTH=1 \
    -I libwebp \
    webp.c \
    libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c

コンパイルは大丈夫そうだがリンクが失敗する。

エラーメッセージ
wasm-ld: error: /var/folders/_m/_rcrfjyd4_5gtg9v65gyld7w0000gn/T/emscripten_temp_8qxz9egh/picture_csp_enc_88.o: undefined symbol: SharpYuvInit
wasm-ld: error: /var/folders/_m/_rcrfjyd4_5gtg9v65gyld7w0000gn/T/emscripten_temp_8qxz9egh/picture_csp_enc_88.o: undefined symbol: SharpYuvGetConversionMatrix
wasm-ld: error: /var/folders/_m/_rcrfjyd4_5gtg9v65gyld7w0000gn/T/emscripten_temp_8qxz9egh/picture_csp_enc_88.o: undefined symbol: SharpYuvConvert
emcc: error: '/usr/local/Cellar/emscripten/3.1.32/libexec/llvm/bin/wasm-ld @/var/folders/_m/_rcrfjyd4_5gtg9v65gyld7w0000gn/T/emscripten_fa4xeyct.rsp.utf-8' failed (returned 1)

SharpYuvInit などが無いようだ。

下記のようにしたらとりあえずビルドは成功した。

コマンド
emcc -s WASM=1 -s EXPORTED_RUNTIME_METHODS='["cwrap"]' \
    -s ALLOW_MEMORY_GROWTH=1 \
    -I libwebp \
    webp.c \
    libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c \
    libwebp/sharpyuv/*.c

python3 -m http.server で開き http://localhost:8000/a.out.html にアクセスすると素敵なお兄さんたちの画像が表示された。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

気合いでインスタンス化した

コマンド
touch load-and-running.html
load-and-running.html
<script>
  async function main() {
    const results = await WebAssembly.instantiateStreaming(
      fetch("a.out.wasm"),
      {
        env: {
          __assert_fail: () => {},
          emscripten_memcpy_big: () => {},
          emscripten_resize_heap: () => {},
        },
        wasi_snapshot_preview1: {
          fd_close: () => {},
          fd_write: () => {},
          fd_seek: () => {},
        },
      }
    );

    console.log(results);
  }

  main().catch((err) => console.error(err));
</script>

そろそろ WebAssembly の理解が必要だ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

実行は簡単

load-and-running.html
<script>
  async function main() {
    const { instance } = await WebAssembly.instantiateStreaming(
      fetch("a.out.wasm"),
      {
        env: {
          __assert_fail: () => {},
          emscripten_memcpy_big: () => {},
          emscripten_resize_heap: () => {},
        },
        wasi_snapshot_preview1: {
          fd_close: () => {},
          fd_write: () => {},
          fd_seek: () => {},
        },
      }
    );

    document.write(`<h1>${instance.exports.version()}</h1>`);
  }

  main().catch((err) => console.error(err));
</script>

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

コーディング

src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    pub fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
    alert(&format!("Hello, {}!", name));
}

Cargo.toml に lib のセクションを追加する。

Cargo.toml
[lib]
crate-type = ["cdylib"]

これが何を意味するかについては下記のリンクが参考になるらしい。

https://doc.rust-lang.org/reference/linkage.html

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

実行

コマンド
touch index.html
index.html
<script type="module">
  import init, { greet } from "./pkg/hello_wasm.js";

  init().then(() => {
    greet("WebAssembly");
  });
</script>
コマンド
python3 -m http.server

http://localhost:8000 にアクセスして WebAssembly とアラートが表示されることを確認する。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

npm で利用する

https://developer.mozilla.org/ja/docs/WebAssembly/Rust_to_wasm#npm_でパッケージが利用できるようにする

コマンド
wasm-pack build --target bundler
cd pkg
npm link
cd ..
mkdir site
cd site
npm init -y
npm install --save-dev webpack webpack-cli webpack-dev-server
npm link hello-wasm
touch webpack.config.js index.js index.html

npm link って何だろうと思って検索したら下記の記事が見つかった。

https://qiita.com/k_kind/items/b3d04bd5a47ee26b6e2d

webpack.config.js
const path = require("path");
module.exports = {
  entry: "./index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "index.js",
  },
  mode: "development",
  experiments: {
    asyncWebAssembly: true,
  },
  devServer: {
    static: ".",
  },
};
index.js
import("./node_modules/hello-wasm/hello_wasm.js").then((js) => {
  js.greet("WebAssembly with npm");
});
index.html
<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>hello-wasm example</title>
  </head>
  <body>
    <script src="./index.js"></script>
  </body>
</html>
コマンド
npx webpack-dev-server

http://localhost:8080 にアクセスしてアラートが表示されることを確認する。