go plugins

plugins in go

SEAN K.H. LIAO

go plugins

plugins in go

plugins

Standalone, extensible, what's not to like about plugins? Except that they're weird and poorly supported...

Anyway, here's 3 ways to do it in Go

plugin

The standard library comes with plugin. Problems abound, such as open (once) only, needs exact compiler versions for main/plugin, limited platform support, you have to write Go, etc...

Anyway, you look up symbols by string name and get access to them as pointers.

Not strictly necessary but nice to have an authoritative definition of the interface example.com/plug/plug.go:

1package plug
2
3type Plug interface {
4        SayHello(n string) (string, error)
5}

example.com/plug/a/a.go:

 1package main
 2
 3import plug "example.com/plug"
 4
 5var A0 plug.Plug = A{}
 6
 7type A struct{}
 8
 9func (a A) SayHello(name string) (string, error) {
10        return "hello, " + name, nil
11}

example.com/main.go:

 1package main
 2
 3import (
 4        "fmt"
 5        "log"
 6        "plugin"
 7
 8        plug "go.seankhliao.com/testrepo-287/internal/plugin"
 9)
10
11func main() {
12        ap, err := plugin.Open("a.so")
13        if err != nil {
14                log.Fatal(err)
15        }
16
17        as, err := ap.Lookup("A0")
18        if err != nil {
19                log.Fatal(err)
20        }
21        a := *as.(*plug.Plug)
22
23        fmt.Println(a.SayHello("a"))
24}

github.com/hashicorp/go-plugin

This is probably the one the sees the widest support, plugins are executables that talk net/rpc (encoding/gob) or grpc over a local unix socket

Shame it makes you write shims converting between Go interfaces and actual rpc implementations, so verbose and so much interface{}

example.com/plug/plug.go:

 1package hashi
 2
 3import (
 4        "net/rpc"
 5
 6        "github.com/hashicorp/go-plugin"
 7)
 8
 9// Handshake is for ensuring the plugin is known/supported
10var Handshake = plugin.HandshakeConfig{
11        ProtocolVersion:  1,
12        MagicCookieKey:   "HASHI",
13        MagicCookieValue: "hola",
14}
15
16// Plug is the interface that clients (main) will call and servers (plugins) will implement
17type Plug interface {
18        SayHello(string) (string, error)
19}
20
21// P satisfies the hashicorp/go-plugin.Plugin interface for instantiating clients / servers
22type P struct {
23        Plug
24}
25
26func (p *P) Server(*plugin.MuxBroker) (interface{}, error) {
27        return &Server{p.Plug}, nil
28}
29
30func (p *P) Client(m *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
31        return &Client{c}, nil
32}
33
34// Server is a shim for converting a net/rpc call to our interface
35type Server struct {
36        p Plug
37}
38
39func (s *Server) SayHello(n string, r *string) (err error) {
40        *r, err = s.p.SayHello(n)
41        return
42}
43
44// Client is a shim for converting our nice interface to a net/rpc call
45type Client struct {
46        c *rpc.Client
47}
48
49func (c *Client) SayHello(n string) (string, error) {
50        var r string
51        // Plugin is the object hashicorp/go-plugin registers
52        err := c.c.Call("Plugin.SayHello", n, &r)
53        return r, err
54}

example.com/plug/a/a.go:

 1package main
 2
 3import (
 4        "github.com/hashicorp/go-plugin"
 5        "example.com/plug"
 6)
 7
 8type A struct{}
 9
10func (a A) SayHello(n string) (string, error) {
11        return "hello, " + n, nil
12}
13
14func main() {
15        plugin.Serve(&plugin.ServeConfig{
16                HandshakeConfig: plug.Handshake,
17                Plugins: plugin.PluginSet{
18                        "a": &plug.P{&A{}},
19                },
20        })
21}

example.com/main.go:

 1package main
 2
 3import (
 4        "fmt"
 5        "log"
 6        "os/exec"
 7
 8        "github.com/hashicorp/go-plugin"
 9        "example.com/plug"
10)
11
12func main() {
13        client := plugin.NewClient(&plugin.ClientConfig{
14                HandshakeConfig: plug.Handshake,
15                Plugins: plugin.PluginSet{
16                        "a": &plug.P{},
17                },
18                Cmd: exec.Command("./a"),
19        })
20        defer client.Kill()
21
22        realClient, err := client.Client()
23        if err != nil {
24                log.Fatal(err)
25        }
26
27        ai, err := realClient.Dispense("a")
28        if err != nil {
29                log.Fatal(err)
30        }
31        a := ai.(plug.Plug)
32
33        fmt.Println(a.SayHello("a"))
34}

grpc

What if you sacrifice some lifecycle handlers go-plugin gives you and write grpc directly?

example.com/plug/plug.proto:

 1syntax = "proto3";
 2
 3option go_package = "example.com/plug";
 4
 5service Plug {
 6  rpc SayHello(HelloReq) returns (HelloRes) {}
 7}
 8message HelloReq {
 9  string n = 1;
10}
11message HelloRes {
12  string msg = 1;
13}

example.com/plug/a/a.go:

 1package main
 2
 3import (
 4        "context"
 5        "log"
 6        "net"
 7        "os"
 8
 9        "example.com/plug"
10        "google.golang.org/grpc"
11)
12
13type Server struct {
14        grpcplug.UnimplementedPlugServer
15}
16
17func (s *Server) SayHello(ctx context.Context, req *plug.HelloReq) (*plug.HelloRes, error) {
18        return &plug.HelloRes{Msg: "hello, " + req.N}, nil
19}
20
21func main() {
22        lis, err := net.Listen("tcp", os.Args[1])
23        if err != nil {
24                log.Fatal(err)
25        }
26        s := grpc.NewServer()
27        plug.RegisterPlugServer(s, &Server{})
28        s.Serve(lis)
29}

example.com/main.go:

 1package main
 2
 3import (
 4        "context"
 5        "fmt"
 6        "log"
 7        "os/exec"
 8
 9        "example.com/plug"
10        "google.golang.org/grpc"
11)
12
13func main() {
14        fmt.Println(exec.Command("./a", "127.0.0.1:8888").Start())
15        conn, err := grpc.Dial("127.0.0.1:8888", grpc.WithInsecure(), grpc.WithBlock())
16        if err != nil {
17                log.Fatal(err)
18        }
19        client := plug.NewPlugClient(conn)
20
21        res, err := client.SayHello(context.TODO(), &plug.HelloReq{
22                N: "a",
23        })
24        fmt.Println(res.Msg, err)
25}