Go1.8のpluginパッケージでGopherLuaに共有ライブラリロードを実装してみた

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

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

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

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

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

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

package main

// // No C code needed.
import "C"

import "fmt"

var V int

func F() { fmt.Printf("Hello, number %d\n", V) }

ポイントは

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

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

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

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

プラグインのロード

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

p, err := plugin.Open("plugin.so")
if err != nil {
    panic(err)
}
v, err := p.Lookup("V")
if err != nil {
    panic(err)
}
f, err := p.Lookup("F")
if err != nil {
    panic(err)
}
*v.(*int) = 7
f.(func())() // prints "Hello, number 7"

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

GopherLuaで使ってみた

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

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

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

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

package main

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

func Add(L *lua.LState) int {
    v1 := L.CheckInt(1)
    v2 := L.CheckInt(2)
    L.Push(lua.LNumber(v1 + v2))
    return 1
}

func LuaOpenPlugin(L *lua.LState) int {
    L.Push(
        L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{
            "add": Add,
        }))
    return 1
}

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

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

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

こうじゃ

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

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

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

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

package.loadlib も実装しました。

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

完璧ですね。

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

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

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

comments powered by Disqus