Brainf*ckという言語はご存知でしょうか。これはいわゆる難解プログラミング言語 (Esolang) の一種で、たった8種類という少なくシンプルな命令セット1でチューリング完全になっていることが知られています。1文字1命令で、手続き型で比較的分かりやすいという特徴から、難解プログラミング言語の代表格ともいえる言語です。
Brainfckは30,000バイトのメモリと1つのポインタを持ち、+
, -
, <
, >
, [
, ]
, .
, ,
の8命令からなる言語です。例えば、以下のプログラムは"Hello, world!"を出力するBrainfckプログラムです。
+++++++++[
>++++++++>+++++++++++
>++++++++++++>+++++<<<<-
]>.>++.>..+++.
>-.------------.<
++++++++.--------.
+++.------.<-.>>+.
Brainf*ckは極めて簡単な言語仕様ながら、奥の深いプログラミングが可能なたいへん興味深く面白い言語です。
さて、その一方で、Brainfckのこのような特徴が、**Brainfck系言語**という存在を生みました。これは私がそう呼んでいるだけで一般的な呼び方なのかは分かりませんが、Brainfck系言語というのは、Brainfckの8種類の命令を別の文字列に変えることで作られる新規性も面白さも全く無いプログラミング言語です。Brainfckの言語仕様が簡単なのでこのようなプログラミング言語の実装はあまり難しくありませんが、「プログラミング言語を作った」というと実際はなんも凄くないのに何となく凄そうな響きがすることや、Brainfckプログラムの性質上同じ文言が大量に並ぶという小学生くらいなら喜びそうな見た目の面白さがあることから、そのような言語がねとらぼで取り上げられてみたり、果てはエンジニアが集まるはずのここQiitaですらBrainf*ck系言語を作る人やそれをもてはやす人がいる状態です。ここで紹介した3つの事例から分かる通り、ファン活動としてこのような言語が作られることが多いようです。
また、そのような人を支援するライブラリもいくつか存在するようです。Qiitaで人気なのはr-fxxkで、独自のBrainf*ck系言語のインタプリタを簡単に作れるようになっています。
現在のところ、Brainfck系言語を作成するには、自分でインタプリタを作成するか、あるいは最低でもr-fxxkなどのライブラリを使用できる素養が必要であり、これが本来1ミリも無いはずのBrainfck系言語の価値をわずかながらに高める要因となっています。
前置きが長くなりましたが、この記事ではBrainfck系言語をさらに簡単に作れる方法を紹介し、Brainfck系言語の価値をさらにゼロに近づけることを目論んでいます。なお、記事名に【個人開発】とあることから分かる通り、今回紹介するのは筆者が制作したちょっとしたWebサービスです。では、さっそく今回作ったサービスを宣伝します。
このサービスは、命令の文字列を8種類決めて名前と説明文を入力するだけで簡単にBrainf*ck系言語の公式ページを作ることができるものです。もはや自分で1行たりともプログラムを書く必要はありません。このサービスで作られた言語は、言語の説明文に加えてブラウザ上で動作するWebインタプリタと、ダウンロード可能なコマンドライン向けインタプリタ(node.js製)が備えられています。また、サンプルプログラムが4種類ついており、その場で小学生が喜びそうなプログラムの見た目を楽しむことができます。
以上でお伝えしたかった内容は終わりですが、Qiitaの記事の質がどうのと騒がれる昨今に記事の内容が宣伝だけというのは申し訳ないので、ここからはあのサービスの技術的な側面に触れようと思います。
Brainf*ckインタプリタ
まあ、サーバー側は普通にnode.jsでクライアント側は普通にReactですが、注目すべき点があるとすればBrainf*ckインタプリタがWebAssemblyで動いているという点かと思います。これはRustで書いたインタプリタがWebAssemblyにコンパイルされています。このインタプリタは以下の独立したリポジトリに置いてあり、一応npmにも公開してあります。
ちょっとずるいですが、Brainfck系言語のソースが入力として与えられると、まずそれを普通のBrainfckに変換してからこのbf-wasm
に渡すようになっています。
RustからWebAssemblyへのコンパイル
RustはWebAssemblyを出力できる有力な言語の一つですが、実際どうやってWebAssemblyにコンパイルするのかは諸説あるかと思います。ここでは素直にcargo
を用いてコンパイルするようになっています。実際、package.jsonを見ると以下のコマンドでコンパイルしていることが分かります。
cargo build --target wasm32-unknown-unknown --release
bf-wasm
にはRustで書かれたコア部分とJavaScriptで書かれたラッパー部分があり、それらをwebpackで1つにまとめる構成をとっています。webpack.config.jsを見るとrust-native-wasm-loaderを使おうとした形跡が見えたりしますが、確か結局うまくいきませんでした。その代わりに、生成されたwasm
ファイルをJavaScriptから直にimportして、wasm-loaderに処理してもらっています。
ただ、webpackにより生成されたJavaScriptファイルを見るとwasmファイルのバイナリデータがJavaScriptコードに埋め込まれているため何だかファイルサイズの面で無駄があるように思えます。別のファイルにするとかそういう最適化は今後の課題でしょうか。
Rust側とJavaScript側の連携
WebAssemblyはモジュールという形で読みこまれ、JavaScriptからはモジュールからエクスポートされた関数を呼び出すことができます。また、逆にJavaScriptから提供された関数をWebAssembly側から呼び出すことができます。今回は簡単のために、WebAssembly側から関数をエクスポートしてJavaScript側から呼び出す形のみ使うことにします。
lib.rsを見るとpub fn
で関数がエクスポートされています。このようにエクスポートされた関数はJavaScript側から呼び出すことができます。ただし、#[no_mangle]
が必要な点に注意してください。
また、データをJavaScriptとRust間でどう受け渡すのかにも工夫が必要です。というのも、WebAssemblyのデータ型は整数とか浮動小数点数とかだけで、文字列のような複雑なデータを関数の引数などで受け渡すことができません。その代わり、同じメモリをJavaScript側とWebAssembly側の両方から読み書きできることを利用して、データをポインタで受け渡しします。このとき、メモリレイアウトはWebAssembly(というかRust)側によって制御されていますから、データを置くためのメモリ領域はRust側のコードで確保することになります。実際、lib.rsはallocとfreeという2つの関数をエクスポートしており、これらを用いてJavaScript側からメモリの確保や解放を行うようになっています。free
のコードを見るとunsafe
などと書いており見るからに危ないですが、まあそういうものでしょう。
例えば、インタプリタに入力としてソースファイルを渡すには、まずalloc
でソース用のメモリ領域を確保し、JavaScript側からその領域に書き込むという流れになります。Brainf*ckプログラムの出力についても、それ用の領域にRust側から書き込んでおいて、あとでJavaScript側からその領域を読むことになります。
また、もう1つ厄介な点として、Brainf*ckプログラムは入力を受け付ける可能性がある点が上げられます。Rust(WebAssembly)側から直にユーザーの入力を待ったりすることはできませんから、入力待ちの際にはJavaScript側に制御を返す必要があります。このことから、Rust側がエクスポートしているrun
関数はプログラムを全部実行するのではなく、次の入力待ちが発生するまで(またはプログラムが終了するまで)実行するようになっています。JavaScript側はユーザーから入力を得たらそれを入力バッファに書き込み、再びrun
を呼び出します。
以上のことが分かればソースコードを読めばだいたい何をやっているのか分かるのではないかと思います。
まとめ
この記事ではまずBrainfckとBrainfck系言語について解説し、サービスを宣伝しました。このサービスを用いることで余計な手間を書けずにBrainfck系言語などという何の価値もないおもちゃを作ることができます。皆さんもこのサービスを使ってどんどんBrainfck系言語を生みだすことでBrainf*ck系言語の価値をゼロにしましょう。
ぶっちゃけ、サービスを宣伝したかっただけと言われると否定できませんが、実装に興味がある人向けに、JavaScriptとWebAssemblyの提携についても少し解説しました。
-
命令の少なさで言えばもっと少ない言語はありますが。Lazy Kとか。 ↩