dlang
WebAssembly
D言語Day 14

D言語で始めるWebAssembly

はじめに

これは D言語 Advent Calendar 2018 の 14 日目の記事であると同時に
WebAssembly Advent Calendar 2018 の 14 日目の記事でもあります。

さて、Web界隈では最近WebAssemblyという新技術が少しブームです。
今までWebブラウザ上で実行できるプログラミング言語はJavaScriptのみでしたが、いつの間にかWebAssemblyというバイナリフォーマットを実行できるようになりました。

現時点での各ブラウザのサポート状況は以下の通りです。

https://caniuse.com/#search=WebAssembly
SnapCrab_NoName_2018-12-9_2-53-5_No-00.png

メジャーなブラウザではほぼサポートしていると言っても良いですね。IE11…知らない子ですね…?

WebAssemblyはC/C++をはじめ、Go,Rust,etc. そして我らがD言語からも生成できるようです。
最近のD言語はなかなか活発でワクワクするような出来事が増えてきていますね。
本記事はまず基本的な情報として、D言語からWebAssemblyを出力して、Webブラウザで実行するところまで解説したいと思います。

WebAssemblyを出力する方法

実はD言語でWebAssemblyを吐き出すには2種類の方法があります。

  • emscriptenのbinaryenを使う方法
  • LLVMのWebAssemblyバックエンドを使う方法

前者は以下の記事を参照してください。

emscripten+D言語(LDC)+bindbc-sdlを試す
https://qiita.com/outlandkarasu@github/items/15e0f4b6d1b2a0eab846

今回は後者の方法について解説します。

LDCを導入する

LDCはLLVMベースのD言語コンパイラです。LLVMのWebAssemblyバックエンドを活用します。

WebAssembly出力はLDC 1.11.0 からサポートされました。
それ以降のバージョンなら何でもいいと思います(適当)

wasmtest.d
extern(C): // シンボル命名をC言語風にする

double add(double a, double b) { return a + b; }

// 必要なエントリポイント
void _start() {}

なんだこれは…。まるで組み込みのプログラムじゃないか。
実はまだD言語のWASMにはまだDruntimeやPhobosはおろか、libcすら無いのです。
強く生きていきましょう。

コンパイル
ldc2 -mtriple=wasm32-unknown-unknown-wasm -betterC wasmtest.d

-betterCオプションを付けることを忘れずに。
wasmだけではWebページにならないので、htmlにJavaScriptを書いてwasmをロードして実行するようにします。

index.html
<html>
  <head>
    <script>

const request = new XMLHttpRequest();
request.open('GET', 'wasmtest.wasm');
request.responseType = 'arraybuffer';
request.onload = () => {
  // ロード完了!
  console.log('response received');
  const bytes = request.response;
  const importObject = {};
  // Wasmをコンパイルして使えるようにするよ!
  WebAssembly.instantiate(bytes, importObject).then(result => {
    console.log('instantiated');
    const { exports } = result.instance;
    // Dで実装されたadd()関数を呼び出すよ!
    const r = exports.add(42, -2.5);
    console.log('r = ' + r);
  });
};
request.send();
console.log('request sent');

    </script>
  </head>
  <body>
    Test page
  </body>
</html>

XMLHttpRequestを使用しているので、ローカルファイルシステムでhtmlを開いても上手くいかないです。
適当にHTTPサーバーを建てましょう。

httpサーバー起動
python -m http.server 8000

実行するとデバッグコンソール以下のログが出力されます。

SnapCrab_NoName_2018-12-11_8-28-31_No-00.png

D言語くんを登場させたい

image.png

唐突ですがWebAssemblyで荒ぶるD言語くんを作りました。

D-man in the WebAssembly
http://nsplus.dojin.com/ueshita/WasmDman/

ソースコード

dman.d
extern(C):

// JS functions
void getScreenSize(out int width, out int height);
void getTextAreaSize(out int width, out int height);
void setLocate(int x, int y);
void setText(immutable(char)* str);

const uint frameRate = 30;
const uint speed = 3;

__gshared uint frameCount = 0;
__gshared uint animFrame = 0;
__gshared int x = 0, y = 0;
__gshared int vx = 1, vy = 1;

// Update a frame
void update() {
    int screenWidth, screenHeight;
    getScreenSize(screenWidth, screenHeight);

    int textWidth, textHeight;
    getTextAreaSize(textWidth, textHeight);

    // reflection
    if (x + vx <= 0) vx = 1;
    if (y + vy <= 0) vy = 1;
    if (x + vx + textWidth  >= screenWidth) vx = -1;
    if (y + vy + textHeight >= screenHeight) vy = -1;

    // movement
    x += vx * speed;
    y += vy * speed;
    setLocate(x, y);

    // animation
    frameCount++;
    if (frameCount >= frameRate) {
        frameCount = 0;
        setText(data[animFrame++].ptr);
        animFrame %= data.length;
    }
}

// Wasm entry point
void _start() {}

// Cool D-man's walking
immutable(string)[4] data = [
`  _   _
 (_) (_)
/______ \
\\(O(O \/
 | | | |
 | |_| |
/______/
  <   >
 (_) (_)`,
`  _   _
 (_) (_)
/______ \
\\(O(O \/
 | | | |
 | |_| |
/______/
 (_)  >
     (_)`,
`  _   _
 (_) (_)
/______ \
\\(O(O \/
 | | | |
 | |_| |
/______/
  <   >
 (_) (_)`,
`  _   _
 (_) (_)
/______ \
\\(O(O \/
 | | | |
 | |_| |
/______/
  <  (_)
 (_) `];
index.html
<html>
  <head>
    <title>D-man in the WebAssembly</title>
    <script>

var exports;

// D文字列をJS文字列に変換する
function toJsString(ptr) {
  var buffer = new Uint8Array(exports.memory.buffer);
  var msg = "";
  var index = 0;
  while (1) {
    var code = buffer[ptr + index++];
    if (code == 0) break;
    msg += String.fromCharCode(code);
  }
  return msg;
}
function getScreenSize(width, height) {
  var buffer = new Int32Array(exports.memory.buffer);
  var screen = document.getElementById("screen");
  buffer[width  >> 2] = screen.clientWidth;
  buffer[height >> 2] = screen.clientHeight;
}
function getTextAreaSize(width, height) {
  var buffer = new Int32Array(exports.memory.buffer);
  var textArea = document.getElementById("textArea");
  buffer[width  >> 2] = textArea.clientWidth;
  buffer[height >> 2] = textArea.clientHeight;
}
function setLocate(x, y) {
  var textArea = document.getElementById("textArea");
  textArea.style.left = x;
  textArea.style.top = y;
}
function setText(str) {
  var textArea = document.getElementById("textArea");
  textArea.innerText = toJsString(str);
}
function updateFrame() {
  exports.update();
  requestAnimationFrame(updateFrame);
}

const request = new XMLHttpRequest();
request.open('GET', 'dman.wasm');
request.responseType = 'arraybuffer';
request.onload = () => {
  const bytes = request.response;
  const importObject = {
    env: { // D言語側から呼ばれる関数を定義する
      setText: setText,
      setLocate: setLocate,
      getScreenSize: getScreenSize,
      getTextAreaSize: getTextAreaSize,
      __assert: function(){},
    }
  };
  WebAssembly.instantiate(bytes, importObject).then(result => {
    exports = result.instance.exports;
    updateFrame();
  });
};
request.send();

    </script>
  </head>
  <body id="screen" style="padding: 0; margin: 0;">
    <pre id="textArea" style="padding: 0; margin: 0; font-family: Courier New; position: absolute;"></pre>
  </body>
</html>

ビルド

ldc2 -mtriple=wasm32-unknown-unknown-wasm -betterC -L-allow-undefined dman.d

-L-allow-undefinedは未定義のシンボルをリンク時に許容して、Wasmロード時に解決するようにするリンカオプションです。

まとめ

まだD言語のWebAssemblyは最初の一歩と言う感じです。
そのうちlibcやらSDLが使えるようになってゲームっぽいものが作れるようになるかと。
来年末までにPhobosまで使えるようになってるといいなぁ。

参考URL

Generating WebAssembly with LDC
https://wiki.dlang.org/Generating_WebAssembly_with_LDC