最近Gradleの便利さに感動しました。設定ファイルをスクリプト言語で記述するのは大変やりやすいものです。
C/C++のようなコンパイル言語からLuaのようなスクリプト言語を利用するのはよくある手段ですが、Goでもスクリプトとやりとりする美味い方法がないか調べてみました。
望む機能
-
go build
したアプリケーションから、スクリプトを呼び出せる- スクリプトファイルを書き換えた場合、goを再コンパイルしなくても更新内容が反映される
- goアプリケーション内で利用している構造体のプロパティーに、スクリプトからアクセスできる
- スクリプトから、goアプリケーション内のメソッドを呼び出せる
- 実行効率? 知らんなあ
試す言語
Goで実装されたJavaScriptエンジンottoを用いると、とても簡単に上の機能を得ることが出来ます。
その他にはJavaScript(v8 engine)、Python、Lua、@mattn 氏制作のGo製スクリプトAnkoなどの選択肢がありそうです。
実装
main.go
package main
import (
"fmt"
"github.com/robertkrimen/otto"
)
// スクリプトに渡すデータ・メソッドの定義
type Property struct {
num1 int
Num2 int
Name string
Greet func() string
Sum func(call otto.FunctionCall) otto.Value
}
func (p *Property) Greet2() string {
return "Hello, " + p.Name
}
func main() {
prop := Property{
num1: 12345,
Num2: 45678,
Name: "John Doe",
Greet: func() string {
return "Hello"
},
Sum: func(call otto.FunctionCall) otto.Value {
x, _ := call.Argument(0).ToInteger()
y, _ := call.Argument(1).ToInteger()
result, _ := otto.ToValue(x + y)
return result
},
}
fmt.Println("--- Go : スクリプト実行前 ---")
fmt.Println(prop)
fmt.Println("-----------------------------")
vm := otto.New()
script, err := vm.Compile("script.js", nil)
if err != nil {
panic(err)
}
fmt.Println("---- JS : スクリプト実行 ----")
vm.Set("prop", &prop) // ... 変数propをスクリプト内でも使えるように設定
_, vm_err := vm.Run(script)
fmt.Println("---- JS : エラー出力 --------")
if vm_err != nil {
fmt.Println(vm_err)
}
fmt.Println("-----------------------------")
fmt.Println("--- Go : スクリプト実行後 ---")
fmt.Println(prop)
fmt.Println(prop.Name)
fmt.Println("-----------------------------")
}
script.js
// 設定したプロパティの一覧を表示
for(var p in prop){
console.log(p + " : " + prop[p]);
}
// プロパティに設定したメソッドの実行
console.log(prop.Greet());
console.log(prop.Greet2());
console.log(prop.Sum(3, 10));
// プロパティの変更
prop.Name = "ナナシノゴンベイ";
console.log(prop.Greet2());
// もとのプロパティにはないので、変更してもGoにわたらない
prop.Name2 = "ナナシノゴンベイ";
$ go build main.go
$./main
--- Go : スクリプト実行前 ---
{12345 45678 John Doe 0x401890 0x4018b0}
-----------------------------
---- JS : スクリプト実行 ----
Num2 : 45678
Name : John Doe
Greet : function () { [native code] }
Sum : function () { [native code] }
Greet2 : function () { [native code] }
Hello
Hello, John Doe
13
Hello, ナナシノゴンベイ
---- JS : エラー出力 --------
-----------------------------
--- Go : スクリプト実行後 ---
{12345 45678 ナナシノゴンベイ 0x401890 0x4018b0}
ナナシノゴンベイ
-----------------------------
スクリプトが実行され、スクリプト内で変更したプロパティがGoプログラムの中でも反映されています。
スクリプトの内容だけ変えて、再度実行したらどうなるでしょうか。
$ vim script.js
:s/ナナシノゴンベイ/名無しの権兵衛/g
$./main
--- Go : スクリプト実行前 ---
{12345 45678 John Doe 0x401890 0x4018b0}
-----------------------------
---- JS : スクリプト実行 ----
Num2 : 45678
Name : John Doe
Greet : function () { [native code] }
Sum : function () { [native code] }
Greet2 : function () { [native code] }
Hello
Hello, John Doe
13
Hello, 名無しの権兵衛
---- JS : エラー出力 --------
-----------------------------
--- Go : スクリプト実行後 ---
{12345 45678 名無しの権兵衛 0x401890 0x4018b0}
名無しの権兵衛
-----------------------------
prop.Nameを「名無しの権兵衛」に置き換えてくれています!
補足
-
Property
構造体のnum1
は隠蔽されているのでスクリプトからは参照できません - Goで変数propをスクリプト内でも使えるように設定している部分は、ポインタ渡しをしないとスクリプト内で値の変更が出来ません。参照・メソッドの実行はできるので、スクリプト内からGoへの副作用を防ぎたいときはあえて値渡しするという手もあるかも。
- 上の例では、引数がないメソッド(Greet, Greet2)を作っていますが、これらメソッドを引数付きで呼び出すとGoのpanicが発生します。Sumメソッドのように
otto.FunctionCall
を引数に取るほうが良いです。 - int型の変数(Num2)に「あいうえお」をスクリプトで代入すると結果は0になります。文字列「111」のように数値変換可能ならばGo側では数値として代入することが出来ます。
- Goで
import _ "github.com/robertkrimen/otto/underscore"
することで、スクリプトでunderscore.jsが利用できます
今回はGoの設定をスクリプトでしたいという動機がありましたが、Goでゲームやその他のアプリケーションを作る際に拡張部分にottoを使うのもありな感じですね。
otto便利!