Posted at

golang 1.8 pluginを使ってみた

More than 1 year has passed since last update.

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.Pluginerrorを受け取る


  • plugin.Plugin{}.Lookup("{publicな変数や関数}")でデータを取得し、plugin.Symbolerrorを受け取る


  • plugin.Symbolはデータをinterface{}型にしたものなので、あとは良しなにキャストして使う


structな値


  • 前述の通り取得したデータはinterface{}の別名定義されたSymbol

  • そのためプラグイン側で独自定義したstructやそういった型が引数や戻り値の関数はそのままキャストできない

これに対する自分が思いつく解決案は以下



  1. interface{}のままreflectパッケージを駆使して扱う



    • encoding/jsonパッケージのjson.Marshal()に渡すとか

    • 関数は無理




  2. type interfaceでインターフェースを定義したファイルを用意して、それをプラグイン側とmain()側の両方で使うようにする


    • 普通の使い方と同じようにstructの必要なメソッドはinterfaceで定義しておけば良い

    • 変数にも使えるけど直接メンバー変数を参照するときはreflectパッケージに頼るしかない


      • メンバー変数を返すメソッドを定義すれば大体の場合は問題ないはず






以上を踏まえたサンプル


A. シンプルに試す


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 structmain()で使用する素数を受信するためのデータ構造


    • 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などの実装に必要な仕様のみを公開するとかしたいなら使えそう


      • 読み込むプラグインをファイルに定義するとかすれば良いのかな



    • 読み込み速度は知らない