Emscripten
dlang
SDL2

emscripten+D言語(LDC)+bindbc-sdlを試す

はじめに

クリエイティブ・コモンズ・ライセンス
この 作品 は クリエイティブ・コモンズ 表示 4.0 国際 ライセンスの下に提供されています。

本記事では、D言語で書いたアプリケーションをemscriptenによりブラウザ上で実行する方法について解説します。

今回のソースコード

https://github.com/outlandkarasu-sandbox/emdman

emscriptenとは

emscriptenとは、asm.jsやWebAssemblyのためのコンパイラ・ツールチェインです。つまり、C言語などのソースコードをブラウザで実行可能なasm.jsやWebAssemblyにコンパイルすることができます。

また、libcやSDL2といったC言語のライブラリのポーティングも含んでいます。

ソースコードをWebAssemblyへコンパイルし、さらにブラウザ上で動作するライブラリをリンクすることで、普通のLinux向けに書かれたようなアプリケーションをそのままブラウザで実行できるようビルドすることが可能です。

D言語でemscriptenをどう使うか

さて、emscriptenは基本的にはC/C++向けのツールチェインです。
しかしながら、実態はLLVMの中間コードからasm.js・WebAssemblyへのコンパイルを行なっており、C/C++の部分はフロントエンドにすぎません。
LLVMの中間コード(ビットコード)さえ生成できれば、それをemscriptenにコンパイルさせることが可能です。

D言語については、LLVMをバックエンドに使っているLDCを使用することでソースコードからLLVMビットコードを生成できます。そのビットコードをemscriptenに処理させれば、C/C++と同様にブラウザでの動作が見込めます。

emscripten使用上の注意点

LLVMのビットコードさえあれば、emscriptenによりブラウザ上で実行可能にできる見込みがあります。
しかしながら、以下の点で十分注意を払い、かつ試行錯誤する必要があります。

  • GCやPhobosといったD言語のランタイムライブラリの機能は、emscriptenにポーティングされていないため使用できない。
  • LLVMの中間コードとはいえ、生成される命令をemscriptenが期待するものになるべく合わせる必要がある。
    • emscriptenのポーティングライブラリの関数を使用する。
    • ポインタサイズや構造体のデータレイアウトを合わせる。
  • emscripten特有の不具合を回避する。
    • 特にデバッグ情報の取り扱いで問題が生じやすいもよう。

実装

方針

注意点に述べた各問題について、現在のD言語ではそれぞれ解決策が存在します。

  • GCやPhobosといったD言語のランタイムライブラリの機能を使わない。
    • -betterC@nogcnothrow属性を活用する。
  • emscripten向けのビットコードを出力する。
    • bindbc-sdlの静的バージョンで-betterCのままSDL2の関数を使用する。
  • emscripten特有の不具合を回避する。
    • がんばる。

上記方針でemscriptenでのD言語開発の実現を目指します。

環境構築

emscriptenはLLVMやらclangやら各種ライブラリやらがたくさん集まってできているので、環境構築が難しそうです。

そこで今回はDockerを使用して済ませることにしました。

https://hub.docker.com/r/trzeci/emscripten/

また、LDCや他の使用ツールもDockerfileに記載し、Dockerビルド時にインストールされるようにします。

docker/Dockerfile
FROM trzeci/emscripten-slim:sdk-tag-1.38.21-64bit

# D言語等のインストール
# ログイン時に有効になるよう.bashrcにも追記
RUN apt-get -y update && \
    apt-get -y install vim sudo curl && \
    sudo -u emscripten /bin/sh -c "curl -fsS https://dlang.org/install.sh | bash -s ldc-1.12.0" && \
    (echo 'source $(~/dlang/install.sh ldc -a)' >> /home/emscripten/.bashrc)

# dub設定(後述)
ADD settings.json /var/lib/dub/settings.json

こういった大きいツールを使うときもDockerは割と楽ですね。

コーディング

コーディングは-betterCSDL2のコードを書くつもりで進めます。
メインループ部分のみ、ブラウザ上で動作することからemscripten独自の関数を使用しています。

画像を表示する最小限のデモは下記の通りです。

source/app.d
// SDL2・SDL_imageのimport。どちらもemscriptenで使える。
import bindbc.sdl;
import bindbc.sdl.image;
import core.stdc.stdio : printf; // printfもemscripten上で使える。

// メインループ用関数の宣言。
alias em_arg_callback_func = extern(C) void function(void*) @nogc nothrow;
extern(C) void emscripten_set_main_loop_arg(em_arg_callback_func func, void *arg, int fps, int simulate_infinite_loop) @nogc nothrow;
extern(C) void emscripten_cancel_main_loop() @nogc nothrow;

/// ログ出力
void logError(size_t line = __LINE__)() @nogc nothrow {
    printf("%d:%s\n", line, SDL_GetError());
}

/// メインループに渡す引数
struct MainLoopArguments {
    SDL_Renderer* renderer;
    SDL_Texture* texture;
}

/// メイン関数。@nogc nothrow で言語機能を抑制
extern(C) int main(int argc, const char** argv) @nogc nothrow {
    // SDL初期化
    if(SDL_Init(SDL_INIT_VIDEO) != 0) {
        logError();
        return -1;
    }
    scope(exit) SDL_Quit();

    // SDL_image初期化(PNG使用)
    if(IMG_Init(IMG_INIT_PNG) != IMG_INIT_PNG) {
        logError();
        return -1;
    }
    scope(exit) IMG_Quit();

    // ウィンドウとそのレンダラーを生成する。
    SDL_Window* window;
    SDL_Renderer* renderer;
    if(SDL_CreateWindowAndRenderer(640, 480, SDL_WINDOW_SHOWN, &window, &renderer) != 0) {
        logError();
        return -1;
    }
    scope(exit) {
        SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    }

    // 画像ファイルのロード
    auto dman = IMG_Load("images/dman.png");
    if(!dman) {
        logError();
        return -1;
    }
    scope(exit) SDL_FreeSurface(dman);

    // 画像からテクスチャーを生成
    auto texture = SDL_CreateTextureFromSurface(renderer, dman);
    if(!texture) {
        logError();
        return -1;
    }
    scope(exit) SDL_DestroyTexture(texture);

    // 描画用のメインループ開始
    auto arguments = MainLoopArguments(renderer, texture);
    emscripten_set_main_loop_arg(&mainLoop, &arguments, 60, 1);
    return 0;
}

// メインループ
extern(C) void mainLoop(void* p) @nogc nothrow {
    // 引数の取得
    auto arguments = cast(MainLoopArguments*) p;
    auto renderer = arguments.renderer;
    auto texture = arguments.texture;

    // 背景消去
    SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0x00);
    SDL_RenderClear(renderer);

    // テクスチャ描画
    SDL_RenderCopy(renderer, texture, null, null);
    SDL_RenderPresent(renderer);

    // ループ1回分終了
    emscripten_cancel_main_loop();
}

ビルド

今回難しいのはビルドです。

dub.json

試行錯誤の結果、dub.jsonは下記の通りになりました。これ1つでemscriptenによるビルドまで行えます。

dub.json
{
    "name": "emdman",
    "authors": [
        "outland.karasu@gmail.com"
    ],
    "description": "A minimal emscripten D man demo.",
    "copyright": "Copyright © 2018, outland.karasu@gmail.com",
    "license": "BSL-1.0",
    "dflags-ldc": ["--output-bc", "-betterC"], // ビットコード出力設定
    "targetName": "app.bc",
    "dependencies": {
        "bindbc-sdl": "~>0.4.1"
    },
    "subConfigurations": {
        "bindbc-sdl": "staticBC" // 静的リンク・betterCモード指定
    },
    "versions": ["BindSDL_Image"], // SDL_image使用

        // ビットコードへのビルド後にemscriptenのコンパイラを動かし、実行用HTML等を生成する。
        // 最適化なし・WebAssemblyあり・SDL/SDL_image(png)使用
        // Web環境限定・画像ファイルは埋め込み・実行用HTML生成
    "postBuildCommands": ["emcc -v -O0 -s WASM=1 -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS='[\"png\"]' -s ENVIRONMENT=web --embed-file images -o dist/index.html app.bc"]
}

32ビットコード(x86)生成にする

x64のままビルドを行なったところ、一応コンパイルは通ったのですがdata layoutが異なるという警告が出ました。

warning: Linking two modules of different data layouts: '/tmp/emscripten_temp_WwvmL5_archive_contents/mulsc3_20989819.c.o' is 'e-p:32:32-i64:64-v128:32:128-n32-S128' whereas '/src/app.bc' is 'e-m:e-i64:64-f80:128-n8:16:32:64-S128'

warning: Linking two modules of different target triples: /tmp/emscripten_temp_WwvmL5_archive_contents/mulsc3_20989819.c.o' is 'asmjs-unknown-emscripten' whereas '/src/app.bc' is 'x86_64-unknown-linux-gnu'

emscriptenは基本的に32ビットコードを生成するらしくさすがにポインタサイズが違うのはやばい……と思って、Dockerfileにあるように/var/lib/dub/settings.jsonを追加しました。

settings.json
{
        "defaultArchitecture": "x86", // 32ビットコード指定
        "defaultCompiler": "ldc" // デフォルトでLDCを使用する
}

dubのsettings.jsonについては下記issueでドキュメント記載予定とのことです。

https://github.com/dlang/dub/issues/1463

デバッグ情報を消す

普通にldc(およびdub)でビルドを行うと、下記のようなエラーがemscriptenで発生しました。

shared:ERROR: Failed to run llvm optimizations:

これはどうやらLLVMの最適化器のエラーのようです。
以下のような不具合を見ると、デバッグ情報周辺のバグのようです。

https://github.com/kripken/emscripten/issues/4078

上記は、この記事の環境では--build=releaseを指定してデバッグ情報を出力しないことで回避できました。

結果

ここまでの内容の試行錯誤を繰り返した末に、ついにブラウザでのデモの実行に成功しました。

ブラウザに描画されたD言語くんはこのような見た目でした。

Screen Shot 2018-12-07 at 1.15.53.png

http://outlandish-watch-web.s3-website-ap-northeast-1.amazonaws.com/

調査の結果、この"D言語くん"をブラウザに描画することに成功しました。結果として、D言語のコードであってもブラウザできいてますか実行できることが示されました。
やはり普通の環境と比べて不安定で、SDL_LowerBlitを使用すると描画が行えなかったりしましたが、これはD言語くんです。

D言語くんはいます。います本来サーバーサイド言語として使用されるD言語がクライアントサイドでも使用できるのは興味深く、ビジネスロジック等のD言語くんはそこにいます。[削除済]また、使用できるライブラリも物理エンジンのBulletやSDL_mixer・oggなどが揃っています。います。PCとブラウザで同様に実行できるゲームなどに活躍しそうです。
D言語くんでした




よろしくおねがいします








memo.png

追記

The Art of MachineryというD言語などを扱った技術ブログに本記事の英訳が掲載されました。

https://theartofmachinery.com/2018/12/20/emscripten_d.html

翻訳してくださったSimonさんありがとうございます!