自作言語をWebAssemblyに対応する試み


自作言語のすすめ

ある程度プログラミングに慣れてくると、「自分で設計したオリジナル言語」を作りたくなりますよね?...ね?

自分で設計した言語を使って、ウェブのフロントエンドアプリ開発に利用できたら楽しそうです。実用性は無いに等しく、ましてやお仕事での開発で利用できるはずもありませんが、技術的な興味と自己満足で、自作言語でWebAssemblyのアプリを作ってみようという試みです。

orelang

皆さんはorelangについて覚えているでしょうか?
覚えてるも何も、知らねぇよそんなもん初めて聞いた、というかもしれません。元ネタは3年前に一部界隈で流行った、とても低機能な自作言語の記事です。

▼プログラミング言語を作る。1時間で。
https://qiita.com/shuetsu@github/items/ac21e597265d6bb906dc

制御構造は条件つきループのuntilのみ。演算子は比較の=と加算の+のみという、とてもシンプルな設計で、JSONで記述します。

サンプルプログラムは次のようなものです。なんとなく雰囲気は掴めるでしょうか。

["step",
  ["set", "sum", 0 ],
  ["set", "i", 1 ],
  ["while", ["<=", ["get", "i"], 10],
    ["step",
      ["set", "sum", ["+", ["get", "sum"], ["get", "i"]]],
      ["set", "i", ["+", ["get", "i"], 1]]]],
  ["print", ["get", "sum"]]]

当時作成したときの都合により、untilの代わりにwhile、演算子は=ではなく<=で実装していますが、大筋では変わりません。最初にsum0i1を代入し、i10になるまでインクリメントしながらループします。ループがまわる度にsumiを足していきます。ループを抜けたときsumの値が1から10の総和である55になっていれば正解です。最後のprint関数は、整数値を1個表示することができます(それしかできません)

LLVM

LLVMはC系言語で利用されているコンパイラ基盤です。clangコンパイラの最も重要な部分で使われているのでお馴染みです。

CやC++など、clangコンパイラが対応している言語であればWebAssemblyのバイトコードが生成できるのなら、LLVMのAPIで生成した LLVM IR からのWebAssembly化もできるはずだよね。だったら、自作言語のWebAssembly対応も可能なのでは、というのがこの記事の趣旨です。

clangをインストールする

Ubuntu 18上での開発を想定しています。clang-6.0を使用します。

sudo apt install clang

忘れずにバージョンを確認しておきます。

soramimi@ubuntu18x64:~$ clang -v
clang version 6.0.0-1ubuntu2 (tags/RELEASE_600/final)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
...

InstalledDirの行に注意してください。上記の例ではシステムにインストールされたUbuntu標準のclangが実行されました。

もし、WebAssembly(Emscripten SDK)の開発環境だと次のようになると思います。

soramimi@ubuntu18x64:~$ source ~/emsdk/emsdk_env.sh
...
soramimi@ubuntu18x64:~$ clang -v
clang version 6.0.1  (emscripten 1.38.30 : 1.38.30)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /home/soramimi/emsdk/clang/e1.38.30_64bit
...

ローカル開発環境とWebAssemby開発環境を行ったり来たりしていると、時々どちらのclangが実行されているか混乱することがありますので注意が必要です。異変に気づいたらどちらのclangが使われているか確認しましょう。

which clang

ローカル開発環境でclangを使いたいときは、一度ターミナルを閉じて開き直します。

やってみる

お馴染みのプログラムで試してみます。

hello.c
#include <stdio.h>
int main()
{
    printf("Hello, world\n");
    return 0;
}

普通に実行するなら次の通りです。

$ clang hello.c
$ ./a.out
Hello, world
$

次に LLVM IR を経由してコンパイルしてみます。

$ clang -c -S -emit-llvm hello.c -o hello.ll

.llという拡張子のファイルがLLVM IRです。このファイルの中を覗いてみるとアセンブリ言語っぽいですが、簡単に言うと、コンパイル中の抽象構文木を元に生成された、特定のCPUに依存しない仮想のアセンブリ言語と思っておけば良いです。

これを実行ファイルにするのはC/C++などと同じ要領でできます。

$ clang hello.ll
$ ./a.out
Hello, world
$

LLVM IRをWebAssembly化する

ここからはWebAssembly開発環境を利用しますので、Emscripten SDKのための環境変数を設定します。環境構築がまだの方は先日の記事を参考にしてください。

source ~/emsdk/emsdk_env.sh

次のようにしてコンパイルします。

emcc hello.ll -s WASM=1 -o hello.html

これによって生成されたhello.htmlhello.jshello.wasmをウェブサーバに置いて、ウェブブラウザを開発者モードにし、hello.htmlにアクセスすると、コンソールにHello, worldが表示されます。

LLVM IRを生成する

LLVM IRを生成するのは、字句解析後に抽象構文木を構築するのとだいたい同じです。これをLLVMのAPI関数を利用して行うのですが、これがなかなか面倒です。

次のような、

; ModuleID = 'mymodule'
source_filename = "mymodule"

define i32 @main() {
entry:
  ret i32 0
}

何もしないで0を返すだけのmain関数を出力してみましょう。

#include <llvm/IR/LLVMContext.h>
#include <llvm/IR/Module.h>
#include <llvm/IR/Function.h>
#include <llvm/IR/BasicBlock.h>
#include <llvm/IR/Instruction.h>
#include <llvm/IR/Instructions.h>
#include <llvm/IR/Constant.h>
#include <llvm/IR/Constants.h>
#include <llvm/Support/raw_os_ostream.h>

using namespace llvm;

int main()
{
    LLVMContext llvmcx;
    Module *module;
    Function *current_function;
    BasicBlock *current_block;

    module = new Module("mymodule", llvmcx);
    DataLayout dl(module);

    // main関数を作成(戻り値int、引数なし)
    current_function = Function::Create(FunctionType::get(Type::getInt32Ty(llvmcx), false), GlobalVariable::ExternalLinkage, "main", module);

    // ブロックを作成
    current_block = BasicBlock::Create(llvmcx, "entry", current_function);

    // 関数の内容を構築


    // 普通はここでブロックの中に様々な命令を構築する


    // return 0
    ReturnInst::Create(llvmcx, ConstantInt::get(Type::getInt32Ty(llvmcx), 0), current_block);

    // LLVM IR を出力
    std::string ll;
    raw_string_ostream o(ll);
    module->print(o, nullptr);
    o.flush();

    puts(ll.c_str());
    return 0;
}

コンテキストを作って、モジュールを作って、ファンクションを作って、ベーシックブロックを作って、return命令を作って、文字列で出力するだけなのですが、たったそれだけのLLVM IRを生成するのに、この分量のコードが必要になります。

orelangからLLVM IRを生成する

自作言語からLLVM IRを生成すればいいのですが、それがとても骨の折れる作業です。

...という記事を、3年前に書きました。

▼Orelang(俺言語) の LLVM IR コンパイラを作ってみた
https://qiita.com/soramimi_jp/items/b7a0a9de381f3c320fe6

JSONテキストをパースして、LLVMのAPIを用いて構文木を構築し、LLVM IRを出力します。あとは、emsdkのclangにかけてコンパイルし、ウェブサーバにアップすれば完成です。

orelangコンパイラのソースコードは筆者のリポジトリにあります。

コンパイラと言っても、ソースコード埋め込みのJSONからLLVM IRを生成するだけなので、コンパイラを名乗るのはおこがましい気がしますが、コンパイラのフロントエンドということで許してください。

実行手順を示します。

soramimi@ubuntu18x64:~$ git clone https://github.com/soramimi/orec.git
Cloning into 'orec'...
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 32 (delta 0), reused 3 (delta 0), pack-reused 27
Unpacking objects: 100% (32/32), done.
soramimi@ubuntu18x64:~$ source ~/emsdk/emsdk_env.sh 
Adding directories to PATH:
PATH += /home/soramimi/emsdk
PATH += /home/soramimi/emsdk/clang/e1.38.30_64bit
PATH += /home/soramimi/emsdk/node/8.9.1_64bit/bin
PATH += /home/soramimi/emsdk/emscripten/1.38.30

Setting environment variables:
EMSDK = /home/soramimi/emsdk
EM_CONFIG = /home/soramimi/emsdk/.emscripten
EM_CACHE = /home/soramimi/emsdk/.emscripten_cache
LLVM_ROOT = /home/soramimi/emsdk/clang/e1.38.30_64bit
EMSCRIPTEN_NATIVE_OPTIMIZER = /home/soramimi/emsdk/clang/e1.38.30_64bit/optimizer
BINARYEN_ROOT = /home/soramimi/emsdk/clang/e1.38.30_64bit/binaryen
EMSDK_NODE = /home/soramimi/emsdk/node/8.9.1_64bit/bin/node
EMSCRIPTEN = /home/soramimi/emsdk/emscripten/1.38.30

soramimi@ubuntu18x64:~$ cd orec/
soramimi@ubuntu18x64:~/orec$ make
g++ -std=c++11 -I/usr/lib/llvm-6.0/include   -c -o main.o main.cpp
g++ -std=c++11 -I/usr/lib/llvm-6.0/include   -c -o json.o json.cpp
g++ main.o json.o -o orec -L/usr/lib/llvm-6.0/lib `/usr/bin/llvm-config-6.0 --libs`
soramimi@ubuntu18x64:~/orec$ ./orec >test.ll
soramimi@ubuntu18x64:~/orec$ emcc test.ll -s WASM=1 -o test.html
soramimi@ubuntu18x64:~/orec$ cp test.html /var/www/html/wasmtest/index.html
soramimi@ubuntu18x64:~/orec$ cp test.js /var/www/html/wasmtest/
soramimi@ubuntu18x64:~/orec$ cp test.wasm /var/www/html/wasmtest/

HTMLファイルを生成するために emcc test.ll -s WASM=1 -o test.html というコマンドを実行してから、test.html を index.html としてコピーしました。それ以降は emcc test.ll -o test.js を実行すれば、.js.wasmだけ生成されるようになります。これら3つのファイルをウェブサーバに配置し、ウェブブラウザからアクセスすれば、orelangの実行結果が表示されます。