正月なので #発火村 に参加してきた
国境の長いトンネルを抜けると、そこは雪国だった。
正月発火村 #発火村 - Togetter
群馬の秘境、水上でハッカソンに参加してきた。
ハッカソンではClion.JSというECMA-335*1の仮想マシンのJavaScript実装を作りたかったのだけど、作業量的に一日二日でできるものでは到底なかったでので「実装を始めた」という程度でした。Clion.JSの開発は続けるつもりです。
さて、ハッカソン前半はずっとmonoを参考にしながらCLI executableのローダを書いていたのだけど、複雑すぎてまるで形になりませんでした*2。そこで急遽VMだけを先に作ることにし、最終的に発表したものが以下のスクリプトです。これにCLI executableを別途disassembleしたものを与えると実行します。
CLI VMは仕様を読むとスタックマシンのようですね。なのでスタックを用意して、disasmしたものからとってきた命令列を実行するだけです。もちろん完全な実装ではなく、文字列の出力と加減算くらいしかできませんが、100行程度のものなのでスタックマシンのVMの簡単なサンプルとしては丁度いいでしょう。
clion-vem.js:
#!/usr/bin/env node "use strict"; var p = console.log; var fs = require('fs'); var name = process.argv[2] || '/dev/stdin'; // loader var buffer = fs.readFileSync(name).toString().split(/\n+/); var i; for(i = 0; i < buffer.length; i++) { if(buffer[i].match(/\.entrypoint$/)) { break; } } var ops = []; for(; i < buffer.length; i++) { var matched = buffer[i].match(/IL_(....):\s+(\w+)\s*(.*)$/); if(matched) { var label = matched[1]; var name = matched[2]; var args = matched[3]; ops.push({ label: label, name: name, args: args }); } else if(buffer[i].match(/^\s*\}/)) { break; } } // virtual machine (function() { var stack = []; var registory = []; var method = { 'System.Console::WriteLine': function(arg) { console.log("%s", arg); }, 'string::Concat': function(a, b) { return String(a) + String(b); }, }; var i, op; for(i = 0; i < ops.length; i++) { op = ops[i]; switch(op.name) { case "box": // noop break; case "call": var m = op.args.match(/(\w+(?:\.\w+)*::\w+)\((.*)\)/); var name = m[1]; var argc = m[2].split(/,/).length; var args = stack.splice( stack.length - argc ); stack.push( method[name].apply(this, args) ); break; case "add": var right = stack.pop(); var left = stack.pop(); stack.push( (+left) + (+right) ); break; case "sub": var right = stack.pop(); var left = stack.pop(); stack.push( (+left) - (+right) ); break; case "ldc": var m = op.args.match(/\w+\s*$/); var v = parseInt(m); stack.push(v); break; case "ldstr": stack.push( eval(op.args) ); break; case "stloc": var idx = op.args.match(/\d+/)[0]; registory[idx] = stack.pop(); break; case "ldloc": var idx = op.args.match(/\d+/)[0]; stack.push(registory[idx]); break; case "ret": return; default: throw Error("Not yet implemented: " + op.name); } } })();
以下のようなC#コードが実行できます。
class HelloWorld { static void Main() { var x = "Hello, "; var y = "world!"; System.Console.WriteLine(x + y); } }
実行するにはmonoとnodeが必要で、逆にこれらがあればプラットフォームに関係なく実行できます。
$ mcs hello.cs # -> hello.exeができる
$ monodis hello.exe | ./clion-vem.js
Hello, world!
monodisするといろいろ出てきますが、見ているのは以下のところだけ。
$ monodis hello.exe // (snip) IL_0000: ldstr "Hello, " IL_0005: stloc.0 IL_0006: ldstr "world!" IL_000b: stloc.1 IL_000c: ldloc.0 IL_000d: ldloc.1 IL_000e: call string string::Concat(string, string) IL_0013: call void class [mscorlib]System.Console::WriteLine(string) IL_0018: ret // (snip)
インストラクションの意味は以下のとおり。だいたい名前から推測できます。
- ldstr 文字列定数をスタックにpush
- stloc.x スタックから値をpopしてローカル変数xに保存
- ldloc.x ローカル変数xから値を得てスタックにpush
- call 関数の呼び出し
- ret 関数からリターン
意味がわかればあとはコードに落としこむだけです。