Go Advent Calendar 2016 16日目です。去年に引き続き今年も3つカレンダーがあり相変わらずの人気ですね。

さて、Go1.8では待望?のShared Libraryのロードが可能になります。 pluginパッケージ を使います。

Go1.8beta1ではLinuxとMacOSがサポートされていたのですが、MacOSで問題が見つかりbeta2ではLinuxのみで利用可能な機能となります。

Advent Calendar 2の qt-luigiさんのネタ と被ってしまったのですが実戦的に使ってみました、ということで許してください。

プラグインの作成とコンパイル

マニュアルページにあるとおり、以下のようになります。

 1package main
 2
 3// // No C code needed.
 4import "C"
 5
 6import "fmt"
 7
 8var V int
 9
10func F() { fmt.Printf("Hello, number %d\n", V) }

ポイントは

  • Cのコードはないが、 import "C" が必要
  • packagemain

という2点です。コンパイルは

1$ go build -buildmode=plugin -o plugin.so plugin.go

でOK。簡単ですね。これで plugin.so が生成されます。

プラグインのロード

これまたマニュアルページどおりですが

 1p, err := plugin.Open("plugin.so")
 2if err != nil {
 3    panic(err)
 4}
 5v, err := p.Lookup("V")
 6if err != nil {
 7    panic(err)
 8}
 9f, err := p.Lookup("F")
10if err != nil {
11    panic(err)
12}
13*v.(*int) = 7
14f.(func())() // prints "Hello, number 7"

のように非常に直感的に使えます。

GopherLuaで使ってみた

拙作のPure GoによるLua実装 GopherLua ですが(何気にstarいっぱいでうれしいですね)、当然ながらC言語実装のように共有ライブラリをロードできませんでした。

そのため、必要なライブラリはすべて事前に組み込んでおく必要がありました。そこでGo1.8で共有ライブラリロードを実装できるのか、実装できるだろうけどちゃんと動くのか、と思い試してみました。

こちらは feature-exp-go1.8pluginsブランチ で実際に動かせます。プラグイン部分のコミットは 571b031 です。

まずプラグイン側から。Luaのお作法通りです。

 1package main
 2
 3import (
 4    "C"
 5    "github.com/yuin/gopher-lua"
 6)
 7
 8func Add(L *lua.LState) int {
 9    v1 := L.CheckInt(1)
10    v2 := L.CheckInt(2)
11    L.Push(lua.LNumber(v1 + v2))
12    return 1
13}
14
15func LuaOpenPlugin(L *lua.LState) int {
16    L.Push(
17        L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{
18            "add": Add,
19        }))
20    return 1
21}

C実装のLuaでは luaopen_共有ライブラリファイル名 が実行されるのですがそこはGoの命名規則に合わせました。違いはそれくらいですね。

こいつをコンパイルして・・・

1$ cd /home/yuin/tmp/plugin
2$ go build -buildmode=plugin -o plugin.so plugin.go

こうじゃ

1$ glua
2> package.cpath = package.cpath .. ";" .. "/home/yuin/tmp/plugin/?.so"
3> adder = require("plugin")
4> print(adder.add(1, 2))
53

おおおおおおおおおおおおお

普通に動きますね。素晴らしい。ちなみに、「ロードする側」と「ロードされる側(すなわちプラグイン)」のバージョンが違うと以下のようにエラーになります。この判定が結構厳しいので(プラグインが参照していない部分の更新でもダメっぽい)、事前にプラグインをコンパイルしておいて配布、は難しいのではないでしょうか。

1<string>:1: plugin.Open: plugin was built with a different version of package github.com/yuin/gopher-lua

package.loadlib も実装しました。

1$ glua
2> print(package.loadlib("/home/yuin/tmp/plugin/notfound", "foo"))
3nil plugin.Open(/home/yuin/tmp/plugin/notfound): realpath failed    open
4> print(package.loadlib("/home/yuin/tmp/plugin/plugin.so", "foo"))
5nil plugin: symbol foo not found in plugin plugin/unnamed-16c3f13f46f4b66b64ad316d78cd61078d12ac64  init
6> print(package.loadlib("/home/yuin/tmp/plugin/plugin.so", "LuaOpenPlugin"))
7function: 0xc4200c9840
8> 

完璧ですね。

pluginパッケージ、使えそうですが・・・

少なくとも、Linuxでは plugin パッケージは使えそうです。ただし、本体と共有ライブラリのコンパイル時、完全にバージョンを合わせる必要があるところが難しそう。

Goの大きなメリットである単一バイナリ配布や、クロスコンパイルと相性は悪いですがうまく使っていければいいなと思います。


たまには実用的なものをつくろうと思って、Go+Luaで置くだけで動くチャットボットを作ってみました。Slack, IRC, Hipchatをサポートしています。

チャットボットといえばHubotだと思いますが、もっとさくっと動かしたいという方におすすめです。置けばうごきます。

特徴は以下です。

  • Goなので置けば動く
  • それでいてLuaでスクリプトを書ける
  • 最初からマルチスレッド(複数goroutine)を考慮している
  • HTTP(S)サーバ機能があるのでWEBHOOKも一緒に作れる
  • 定期ジョブも流せる
 1function main()
 2  local bot = golbot.newbot("Slack", { token = "xxxxx" })
 3
 4  bot:respond([[\s*(\d+)\s*\+\s*(\d+)\s*]], function(m, e) -- 3
 5    bot:say(e.target, tostring(tonumber(m[2]) + tonumber(m[3])))
 6  end)
 7
 8  bot:serve(function(msg)
 9    if msg.type == "say" then
10      bot:say(msg.channel, msg.message)
11      respond(msg, true)
12    end
13  end)
14end

こんな感じのよくあるAPIです。特徴的なのがworkerの仕組みで

 1function main()
 2  bot:respond([[deploy]], function(m, e)
 3    bot:say(e.target, "accepted")
 4    goworker({target=e.target, type="deploy"})
 5  end)
 6
 7  bot:serve(function(msg)
 8    if msg.type == "say" then
 9      bot:say(msg.target, msg.message)
10    end
11  end)
12end
13
14function worker(msg)
15  if msg.type == "deploy" then
16    do_deploy()
17    notifymain({type="say", target=msg.target, message="your deployment has been completed"})
18  end
19end

このように goworker でLuaからGoroutineをつくって重い処理などをWorkerで実行することができます。Workerからは notifymain でメインGroutineにメッセージをおくることができます。

HTTPサーバ機能では以下のような関数を定義するだけで簡単にWEBHOOKが作れます。

 1function http(r)
 2  if r.method == "POST" and r.URL.path == "/webhook" then
 3    local data = assert(json.decode(r:readbody()))
 4    local message = data.item.message.message
 5    local user = data.item.message.from.name
 6    local room = data.item.room.name
 7
 8    local ret = {
 9      message = "hello! from webhook",
10      message_format = "html"
11    }
12
13    return 200, headers, json.encode(ret)
14  end
15  return 400, headers, json.encode({result="not found"})
16end

定期ジョブは以下のような感じ。

 1function main()
 2  golbot.newbot("Null", { 
 3    http = "0.0.0.0:6669" ,
 4    crons = {
 5      { "0 * * * * * ", "job1"}
 6    }
 7  }):serve(function() end)
 8end
 9
10function job1()
11  print "hello!"
12end

チャットボットのためだけにNode.jsとnpmはちょっと・・・という場合にぜひ。


日本人である以上、いくらUTF-8が主権を得てきたといっても文字コード変換というカルマからは逃れられません。Pure Goでの文字コード変換はLLに比べるといろいろめんどくさい。

それに、そのままだと任意の文字コードを指定させるのが難しい。特定の文字コード決め打ちならいいんですけど、全世界の利用者に向けて任意の文字コード設定できるようにする場合とかはさらにめんどくさいし、かといって iconv に依存するのも嫌なのライブラリにしました。

以下のような感じで簡単に文字コードを変換できます。Pythonと同じで encode がUTF-8からUTF-8以外へ、 decode がUTF-8以外からUTF-8へ、です。内部では golang.org/x/net/html/charset を使っているので文字コードの指定もそれに習います。(WHATWGで定義されている名前になります) 。なので cp932 などではなく Windows-31J です。

 1b, err = EncodeString("こんにちわ", "Windows-31J")
 2b, err = Encode("こんにちわ", "Windows-31J")
 3b, err = EncodeBytes([]byte("こんにちわ"), "Windows-31J")
 4b, err = EncodeReader(strings.NewReader("こんにちわ"), "Windows-31J")
 5b = MustEncodeString("こんにちわ", "Windows-31J")
 6b = MustEncode("こんにちわ", "Windows-31J")
 7b = MustEncodeBytes([]byte("こんにちわ"), "Windows-31J")
 8b = MustEncodeReader(strings.NewReader("こんにちわ"), "Windows-31J")
 9
10s, err = DecodeString(string(source), "Windows-31J")
11s, err = Decode(source, "Windows-31J")
12s, err = DecodeBytes(source, "Windows-31J")
13s, err = DecodeReader(bytes.NewReader(source), "Windows-31J")
14s = MustDecodeString(string(source), "Windows-31J")
15s = MustDecode(source, "Windows-31J")
16s = MustDecodeBytes(source, "Windows-31J")
17s = MustDecodeReader(bytes.NewReader(source), "Windows-31J")