GopherLua で設定ファイルを書くためのライブラリを書きました。設定ファイル以外にも使えますけど。

モノとしてはGopherLuaのテーブルをGoの構造体にマップしてくれます。といっても、内部動作的にはHashicorpの mapstructure を使っているだけだったりします。一度GopherLuaのテーブルを map[string]interface{} に変換してあげて、あとはmapstructureにおまかせ。

ただ、一般的なLuaの命名規則とGoの命名規則が違うので名前を変換する関数が指定できます。デフォルトでは snake_caseCamelCase に変換します。

type Role struct {
    Name string
}

type Person struct {
    Name      string
    Age       int
    WorkPlace string
    Role      []*Role
}

L := lua.NewState()
if err := L.DoString(`
person = {
  name = "Michel",
  age  = "31", -- weakly input
  work_place = "San Jose",
  role = {
    {
      name = "Administrator"
    },
    {
      name = "Operator"
    }
  }
}
`); err != nil {
    panic(err)
}
var person Person
if err := gluamapper.Map(L.GetGlobal("person").(*lua.LTable), &person); err != nil {
    panic(err)
}
fmt.Printf("%s %d", person.Name, person.Age)

のように非常に簡単にLuaを設定ファイルとして使うことができます。Luaは可読性が高く、JSONと異なりコメントが書けて、YAMLよりも簡単に値を変数化できるので設定ファイルにすると便利です(なんでも出来てしまう、というのがネックと言えばネックですが)。


先日公開した GopherLua ですが。

GopherLuaはLuaなので、ホスト言語との親和性を重視しております。GolangなのにGoroutine関係のサポートがないとだめでしょ!ということで、とりあえずですが、channelを扱えるようにしました。

これもまたReadmeに書いてありますが、以下のように使います。 selectも使えますし、GolangとLuaの間で縦横無尽にメッセージ通信できます。

func receiver(ch, quit chan lua.LValue) {
    L := lua.NewState()
    defer L.Close()
    L.SetGlobal("ch", lua.LChannel(ch))
    L.SetGlobal("quit", lua.LChannel(quit))
    if err := L.DoString(`
    local exit = false
    while not exit do
      channel.select(
        {"|<-", ch, function(ok, v)
          if not ok then
            print("channel closed")
            exit = true
          else
            print("received:", v)
          end
        end},
        {"|<-", quit, function(ok, v)
            print("quit")
            exit = true
        end}
      )
    end
  `); err != nil {
        panic(err)
    }
}

func sender(ch, quit chan lua.LValue) {
    L := lua.NewState()
    defer L.Close()
    L.SetGlobal("ch", lua.LChannel(ch))
    L.SetGlobal("quit", lua.LChannel(quit))
    if err := L.DoString(`
    ch:send("1")
    ch:send("2")
  `); err != nil {
        panic(err)
    }
    ch <- lua.LString("3")
    quit <- lua.LTrue
}

func main() {
    ch := make(chan lua.LValue)
    quit := make(chan lua.LValue)
    go receiver(ch, quit)
    go sender(ch, quit)
    time.Sleep(3 * time.Second)
}

こんな感じですね。Luaと同じくState自体はスレッドセーフではないので、goroutineごとにStateをもってChannelで通信します。

実装について

迷ったんですが、channelは 基本型 として実装しています。 ユーザーデータ型でもよかったんですが、channelはGolangの特徴の一つですし、基本型にしたほうが親和性がよいので。

あと、内部的にはリフレクションなのでそんなに性能はよくないかもしれません。

よければ使ってみてください

実装してみたものの、正直私は職業プログラマでなく趣味プログラマなのであんまり使うシーンがないです。なんかいい使いどころがあれば使ってみてください。


LuaをGo言語のみで実装した GopherLua を公開しました。

詳しくはGithubのREADMEを見ていただくとして、特徴としては以下になります。

  • Lua5.1ベース

    • 5.1の機能はほぼ実装済み
  • Compiler, VMともに完全にGo言語のみで実装

  • 引数の受け渡し以外でのスタック操作が不要で使いやすいAPI

なぜ作ろうと思ったか

もともとC言語でものを作るときにはLuaを設定ファイルの代わりとして取り入れていました。Goではあまり拡張言語実装がないので、jsonだったりiniだったりを設定ファイルとして使っていましたが、やっぱり微妙にめんどくさい。変数くらい欲しいなあ・・・とか。

結局、固い言語だけでモノを作るのは難しく、やわらかさが必要になるポイントがあります。そういうところはまず、「設定ファイル」として外だしされます。そしてその設定ファイルがどんどん肥大化したり複雑化したりして・・・(XML地獄とか)。Cに対するLua、PythonやRuby、Javaに対するGroovyなど「固い言語」+「拡張言語」は自分的に一番しっくりくる構成です。それをGoで実現するために実装してみました。

もうひとつは単純にLuaに興味があったこと。正確にはLuaというよりレジスタ型VMに。スタック型VMは実装したことがあるのですが、レジスタ型で実装してみたかったのです。

最後に、Go言語で処理系を実装してみたかったこと。実行ファイルが吐けるし、速度はそれなりに速いし、Go自体がGCを持っているのでかなり処理系作るのが楽だと思われるのでどんなものかな、と。

簡単な使い方

こうして

import (
    "github.com/yuin/gopher-lua"
)

こんな感じですね。

L := lua.NewState()
defer L.Close()
if err := L.DoString(`print("hello")`); err != nil {
    panic(err)
}

Go関数をLuaで呼ぶ場合は以下のような感じ。LuaとGoの間の引数と戻り値の受け渡しのみにスタックを使います。

func Double(L lua.LState) int {
    lv := L.ToInt(1)             // get argument
    L.Push(lua.LNumber(lv * 2))  // push result
    return 1                     // number of results
}

func main() {
    L := lua.NewState()
    defer L.Close()
    L.SetGlobal("double", L.NewFunction(Double)) // Original lua_setglobal uses stack... 
}

あとは README をみていただければ大体分かるかと思います。

実装について

全般の話

まず、そんなにLua自体のコードは読んでません(おい)。Luaは1パスでコード生成までできる文法で本家はそういう実装ですが、GopherLuaでは

  1. トークナイズ(Lexer, 手書き)
  2. パース(go-yacc)
  3. コード生成
  4. 最適化

まったく違うパス構成なので実装は完全に独自実装です。またyaccなのでユーザが文法を簡単に変えられます。

現状最適化はほとんどやっていません。複数JMPをまとめるくらい。

あと、やっぱ三項演算子欲しい・・・。

データモデルの話

こういう言語を実装する場合はいわゆる共用体のような、1つの型で複数の型を判別できるものが必要となります。Cでは共用体やポインタの下位ビットを使いますが、Goでは以下の選択肢があります。

  • reflect.Value
  • interface
  • unsafe.Pointer

GopherLuaでは interface を使っています。 interface

  • Go側のAPIを考えたとき一番分かりやすい

という利点がある一方

  • ネイティブ型をラップした interface の場合、 interface への変換が発生して速度低下を招く

という欠点があります。そこで簡単なベンチマークをしたのですが

  • 単純な例(フィボナッチ計算)では確かに reflect.Value などが速い。
  • 一方、複雑な例になればそれほど差が無いように見えた

ため interface を採用しました。ただ、プロファイルをとるとかなりの部分が interface への変換に取られているので、ここが(Goが進化して)速くなればGopherLuaも速くなると思います。

速度の話

それほどパフォーマンスチューニングはしていませんが、フィボナッチではperlと同じくらいの性能は出ているようです。メモリ確保はそもそも気をつけて減らしているので後は

  • 関数をベタ書きする

くらいかなあ。Goは短い関数をinline化してくれますが、そのinline化はそんなに賢くないのでやっぱりダメですね。ためしにVMの関数コール部分などをベタ書きすると1.2倍くらいの速度になりました。今のところそこまで速度を求めていないので元に戻しましたけど。

やっぱマクロ欲しいなあ・・・

とりあえず、使えるはずです

Lua5.1のテストは主要なものは通っているので使えるものになっているはずです。設定ファイルのかわりに使うもよし、プラグインの仕組みに使うのもよし。今後はGopherLuaのTableをGoのstructにマッピングするような( jsonxml パッケージみたいな)ライブラリとか作りたいなと思っていたりします。