2017/02にgolang 1.8が出ましたね。
所々変わってますが、plugin
パッケージもその1つかと思います。
あえてプラグインにするための方法が標準パッケージに現れたということで試してみました。
環境
- docker (moby?)
FROM golang:1.8
ENV GOPATH /go
ENV PATH $GOPATH/bin:$PATH
plugin
の書き方
- 基本的にはパッケージの場合と同じ
- 呼び出し方がちょっと特殊なだけ
プラグイン側
- プラグインとして呼び出せるようにしたいものは
public
な命名規則で定義する -
go build -buildmode=plugin -o {プラグインファイル名.so} {プラグインのソースファイル名}.go [...]
でビルド - 通常のパッケージと同じで
func init() { ... }
があれば呼び出し時に実行される
main()
側
- 起動は普通に
go run main.go
とかで良い - プラグインを読み込むためには
plugin
パッケージをインポートする -
plugin.Open("{プラグインファイル名.so}")
で読み込んで、*plugin.Plugin
とerror
を受け取る -
plugin.Plugin{}.Lookup("{publicな変数や関数}")
でデータを取得し、plugin.Symbol
とerror
を受け取る -
plugin.Symbol
はデータをinterface{}
型にしたものなので、あとは良しなにキャストして使う
struct
な値
- 前述の通り取得したデータは
interface{}
の別名定義されたSymbol
型 - そのためプラグイン側で独自定義した
struct
やそういった型が引数や戻り値の関数はそのままキャストできない
これに対する自分が思いつく解決案は以下
-
interface{}
のままreflect
パッケージを駆使して扱う-
encoding/json
パッケージのjson.Marshal()
に渡すとか - 関数は無理
-
-
type interface
でインターフェースを定義したファイルを用意して、それをプラグイン側とmain()
側の両方で使うようにする- 普通の使い方と同じように
struct
の必要なメソッドはinterface
で定義しておけば良い - 変数にも使えるけど直接メンバー変数を参照するときは
reflect
パッケージに頼るしかない- メンバー変数を返すメソッドを定義すれば大体の場合は問題ないはず
- 普通の使い方と同じように
以上を踏まえたサンプル
A. シンプルに試す
- ドキュメントにサンプルが載ってるので割愛
- https://golang.org/pkg/plugin/#Symbol
B. 解決案1を試す
package main
import (
"encoding/json"
"os"
"plugin"
)
func main() {
plug, _ := plugin.Open("sample.so")
symbol, _ := plug.Lookup("Group")
bytes, _ := json.Marshal(symbol)
os.Stdout.Write(bytes)
}
package main
// ColorGroup https://golang.org/pkg/encoding/json/#Marshal
type ColorGroup struct {
ID int
Name string
Colors []string
}
// Group https://golang.org/pkg/encoding/json/#Marshal
var Group = ColorGroup{
ID: 1,
Name: "Reds",
Colors: []string{"Crimson", "Red", "Ruby", "Maroon"},
}
- 実行
$ go build -buildmode=plugin -o sample.so sample.go
$ go run main.go
{"ID":1,"Name":"Reds","Colors":["Crimson","Red","Ruby","Maroon"]}
- 結論
- キャストしなくても
reflect
パッケージ使えば型の解決くらいなら何とかなりそう
- キャストしなくても
C. 解決案2を試す
素数を受け取って表示するプログラムです
正直、色々やりすぎてサンプルとしてはアレだなーとは思ったけど、やっちゃったもんはしょうがない
-
main.go
- コマンドライン引数で使用する素数送信プラグインを選ぶ
- プラグインの読み込みとプラグインで実装しておいてほしい
TickPrimeNumbers()
関数の実行はmain.tickPrimeNumbers()
関数内で実施 -
main()
はmain.tickPrimeNumbers()
の実行結果からtypes.Receiver
を受け取って、持ってるchannel
を取得して待ち受ける - 5秒待ち受けても送信されてこなかったらタイムアウトして
types.Receiver{}.Stop()
を呼び出す
package main
import (
"flag"
"fmt"
"log"
"plugin"
"time"
"./types"
)
func tickPrimeNumbers(pluginFileName string) types.Receiver {
plug, err := plugin.Open(pluginFileName)
if err != nil {
log.Fatal(err)
}
symbol, err := plug.Lookup("TickPrimeNumbers")
if err != nil {
log.Fatal(err)
}
return symbol.(func() types.Receiver)()
}
func main() {
mode := flag.String("mode", "tick-1s", "tick-1s / any")
pluginFileName := flag.String("plugin", "", "target plugin file (.so)")
flag.Parse()
var receiver types.Receiver
switch *mode {
case "tick-1s":
receiver = tickPrimeNumbers("tick_prime_number_1s.so")
case "any":
receiver = tickPrimeNumbers(*pluginFileName)
default:
panic(fmt.Errorf("Unknown mode: %s", *mode))
}
loop:
for {
// 素数を受け取り表示する(5sでタイムアウト)
select {
case num := <-receiver.Channel():
fmt.Println(num)
case <-time.After(5 * time.Second):
fmt.Println("timed out")
receiver.Stop()
break loop
}
}
}
-
types/receiver.go
-
Channel()
は素数を受信するchannel
を返すための定義 -
Stop()
は素数の送信元の終了処理を実行するための定義
-
package types
// Receiver is an interface for returning PrimeNumberReceiver
type Receiver interface {
Channel() <-chan int
Stop()
}
-
plugins/base.go
- プラグインで極力共通で使いたい部品を定義
-
type PrimeNumberReceiver struct
はmain()
で使用する素数を受信するためのデータ構造 -
TickPrimeNumbers()
は素数の送信処理の開始と受信用データ構造の返却を担い、main()
にプラグインを読み込んでまず実行してもらう関数 -
isPrimeNumber()
は素数かどうかチェックする関数
package main
import (
"../types"
)
// PrimeNumberReceiver is a struct that stores channels for get prime numbers
type PrimeNumberReceiver struct {
// ch is a channel to exchanging prime number
ch <-chan int
// done is a channel to stop ticker
done chan<- bool
}
// Channel is a method to get channel to recieve prime numbers
func (receiver *PrimeNumberReceiver) Channel() <-chan int {
return receiver.ch
}
// Stop is a method to end receipt of prime numbers
func (receiver *PrimeNumberReceiver) Stop() {
close(receiver.done)
}
// TickPrimeNumbers is a function to start tick prime numbers
func TickPrimeNumbers() types.Receiver {
ch := make(chan int, 1)
done := make(chan bool, 1)
go tickPrimeNumbers(ch, done)
receiver := &PrimeNumberReceiver{
ch: ch,
done: done,
}
return receiver
}
// private) isPrimeNumber is a function to check prime number
func isPrimeNumber(x int) bool {
if x < 2 {
return false
}
for n := 2; n < x; n++ {
if (x % n) == 0 {
return false
}
}
return true
}
-
plugins/1s.go
- 実際の素数生成と送信処理 その1
- 一秒ごとにカウントアップする整数が素数かチェックして、素数だったら送信する
package main
import (
"fmt"
"time"
)
// TickPrimeNumbers is a function to start tick prime numbers
func tickPrimeNumbers(sender chan<- int, done <-chan bool) {
fmt.Println("start ticker")
// cntを1/sでインクリメントして素数を探し続ける
cnt := 0
ticker := time.NewTicker(1 * time.Second)
loop:
for {
select {
case now := <-ticker.C:
fmt.Printf("%v\n", now)
if isPrimeNumber(cnt) {
sender <- cnt
}
cnt++
case <-done:
ticker.Stop()
close(sender)
break loop
}
}
fmt.Println("stop ticker")
}
-
plugins/not_implemented.go
- 実際の素数生成と送信処理 その2
- 処理といってもこっちは未実装バージョン
- 読み込み時に未実装な旨表示して、受信側が
Stop()
による終了処理をするまで何もせず待つだけ
package main
import (
"fmt"
)
// tickPrimeNumbers is not inplemented
func tickPrimeNumbers(sender chan<- int, done <-chan bool) {
<-done
close(sender)
}
func init() {
fmt.Println("Setup tick_prime_number_not_implemented.so")
fmt.Println("This plugin is not implemented.")
}
- 実行
$ go build -buildmode=plugin -o tick_prime_number_1s.so plugins/base.go plugins/1s.go
$ go build -buildmode=plugin -o not_implemented.so plugins/base.go plugins/not_implemented.go
$ go run main.go -h
Usage of /tmp/go-build059816819/command-line-arguments/_obj/exe/main:
-mode string
tick-1s / any (default "tick-1s")
-plugin string
target plugin file (.so)
$ go run main.go
Setup tick_prime_number_1s.so
start ticker
2017-04-20 07:23:14.015709191 +0000 UTC m=+1.003486062
2017-04-20 07:23:15.015716262 +0000 UTC m=+2.003493121
2017-04-20 07:23:16.015704403 +0000 UTC m=+3.003481267
2
2017-04-20 07:23:17.015708984 +0000 UTC m=+4.003485841
3
2017-04-20 07:23:18.015693418 +0000 UTC m=+5.003470310
...
...
2017-04-20 07:23:42.015733076 +0000 UTC m=+29.003509952
timed out
stop ticker
fin
$ go run main.go -mode any -plugin not_implemented.so
This plugin is not implemented.
timed out
fin
- 結論
- 工夫次第な感じがした
- ベースの部分を作って
interface
などの実装に必要な仕様のみを公開するとかしたいなら使えそう- 読み込むプラグインをファイルに定義するとかすれば良いのかな
- 読み込み速度は知らない