2
Help us understand the problem. What are the problem?

posted at

updated at

Organization

自作言語を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の実行結果が表示されます。

image.png

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
2
Help us understand the problem. What are the problem?