golang 1.8 pluginを使ってみた

2017/02にgolang 1.8が出ましたね。
所々変わってますが、`plugin`パッケージもその１つかと思います。
あえてプラグインにするための方法が標準パッケージに現れたということで試してみました。

## 環境

* 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`やそういった型が引数や戻り値の関数はそのままキャストできない

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

1. `interface{}`のまま`reflect`パッケージを駆使して扱う
    * `encoding/json`パッケージの`json.Marshal()`に渡すとか
    * 関数は無理
2. `type interface`でインターフェースを定義したファイルを用意して、それをプラグイン側と`main()`側の両方で使うようにする
    * 普通の使い方と同じように`struct`の必要なメソッドは`interface`で定義しておけば良い
    * 変数にも使えるけど直接メンバー変数を参照するときは`reflect`パッケージに頼るしかない
        * メンバー変数を返すメソッドを定義すれば大体の場合は問題ないはず

## 以上を踏まえたサンプル

### A. シンプルに試す

* ドキュメントにサンプルが載ってるので割愛
* https://golang.org/pkg/plugin/#Symbol

### B. 解決案1を試す

* [main.go](https://play.golang.org/p/a003WITqCJ)

```go
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)
}
```

* [sample.go](https://play.golang.org/p/H49x-qFtD4)

```go
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"},
}
```

* 実行

```shell-session
$ 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](https://play.golang.org/p/QiGCkNGwwK)
    * コマンドライン引数で使用する素数送信プラグインを選ぶ
    * プラグインの読み込みとプラグインで実装しておいてほしい`TickPrimeNumbers()`関数の実行は`main.tickPrimeNumbers()`関数内で実施
    * `main()`は`main.tickPrimeNumbers()`の実行結果から`types.Receiver`を受け取って、持ってる`channel`を取得して待ち受ける
    * 5秒待ち受けても送信されてこなかったらタイムアウトして`types.Receiver{}.Stop()`を呼び出す

```go
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](https://play.golang.org/p/u5WHNm1hXY)
    * `Channel()`は素数を受信する`channel`を返すための定義
    * `Stop()`は素数の送信元の終了処理を実行するための定義

```go
package types

// Receiver is an interface for returning PrimeNumberReceiver
type Receiver interface {
	Channel() <-chan int
	Stop()
}
```

* [plugins/base.go](https://play.golang.org/p/YVXyEkApJR)
    * プラグインで極力共通で使いたい部品を定義
    * `type PrimeNumberReceiver struct`は`main()`で使用する素数を受信するためのデータ構造
    * `TickPrimeNumbers()`は素数の送信処理の開始と受信用データ構造の返却を担い、`main()`にプラグインを読み込んでまず実行してもらう関数
    * `isPrimeNumber()`は素数かどうかチェックする関数

```go
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](https://play.golang.org/p/2fEPQJBUsS)
    * 実際の素数生成と送信処理 その１
    * 一秒ごとにカウントアップする整数が素数かチェックして、素数だったら送信する

```go
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](https://play.golang.org/p/JWuo_KplkM)
    * 実際の素数生成と送信処理 その２
    * 処理といってもこっちは未実装バージョン
    * 読み込み時に未実装な旨表示して、受信側が`Stop()`による終了処理をするまで何もせず待つだけ

```go
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.")
}
```

* 実行

```shell-session
$ 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`などの実装に必要な仕様のみを公開するとかしたいなら使えそう
        * 読み込むプラグインをファイルに定義するとかすれば良いのかな
    * 読み込み速度は知らない