先日公開した 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 パッケージみたいな)ライブラリとか作りたいなと思っていたりします。


コマンドラインランチャー iceberg のv0.9.6 をリリースしました。 GitHubの リリース一覧 よりダウンロードできます。

v0.9.5からのバージョンアップ方法は同梱ドキュメントの更新履歴を参照してください。

更新点は以下です。

  • FIXED : 一部のパスでアイコンが正しく読み込まれない問題を修正
  • FIXED : 一部のパスを読み込んだ際にクラッシュする問題を修正
  • CHANGED : 使用するコンパイラをMinGW-W64 4.9.1にアップグレード
  • IMPROVED : 単一キーをホットキーに割り当てられるようになった
  • NEW : パス補完とオプション補完でオートコンプリートを有効にする path_autocomplete と option_autocomplete を追加

ようやく重い腰をあげてgccのバージョンアップをしました…。icebergは今までmingw gcc4.5系を使っていて、これWindowsのくせにLP64なんですね。まぁそのおかげで*nix生まれのライブラリでも割と普通に使えていました。

で4.9にあげたのでWindowsらしくLLP64になったわけで、fltkなんかは平気で longvoid* にキャストしてるのでワーニング出まくりでした…。そんなクリティカルな場所はなかったので簡単なパッチ当てて終了。

鬼車も今まで使ってたバージョンは long のポインタキャストがあって落ちるので最新に。

なんやかんやあったけど今のところきちんと動いている気がします。なにか問題ありましたらGithubの Issues までどうぞ。

あと、どうやらicebergはWindows8で動かないっぽいです(Windows7互換モードにすると動く)。 8の環境持ってないのでなんで動かないのかわかりません。コンパイラ変えただけで動くようになってたり…しないかなあ。というわけでWindows8使ってる方、もし互換モードなしで動いたらコメントください。