blog

12021-02-20

SEAN K.H. LIAO

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:

package plug

type Plug interface {
        SayHello(n string) (string, error)
}

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

package main

import plug "example.com/plug"

var A0 plug.Plug = A{}

type A struct{}

func (a A) SayHello(name string) (string, error) {
        return "hello, " + name, nil
}

example.com/main.go:

package main

import (
        "fmt"
        "log"
        "plugin"

        plug "go.seankhliao.com/testrepo-287/internal/plugin"
)

func main() {
        ap, err := plugin.Open("a.so")
        if err != nil {
                log.Fatal(err)
        }

        as, err := ap.Lookup("A0")
        if err != nil {
                log.Fatal(err)
        }
        a := *as.(*plug.Plug)

        fmt.Println(a.SayHello("a"))
}

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:

package hashi

import (
        "net/rpc"

        "github.com/hashicorp/go-plugin"
)

// Handshake is for ensuring the plugin is known/supported
var Handshake = plugin.HandshakeConfig{
        ProtocolVersion:  1,
        MagicCookieKey:   "HASHI",
        MagicCookieValue: "hola",
}

// Plug is the interface that clients (main) will call and servers (plugins) will implement
type Plug interface {
        SayHello(string) (string, error)
}

// P satisfies the hashicorp/go-plugin.Plugin interface for instantiating clients / servers
type P struct {
        Plug
}

func (p *P) Server(*plugin.MuxBroker) (interface{}, error) {
        return &Server{p.Plug}, nil
}

func (p *P) Client(m *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
        return &Client{c}, nil
}

// Server is a shim for converting a net/rpc call to our interface
type Server struct {
        p Plug
}

func (s *Server) SayHello(n string, r *string) (err error) {
        *r, err = s.p.SayHello(n)
        return
}

// Client is a shim for converting our nice interface to a net/rpc call
type Client struct {
        c *rpc.Client
}

func (c *Client) SayHello(n string) (string, error) {
        var r string
        // Plugin is the object hashicorp/go-plugin registers
        err := c.c.Call("Plugin.SayHello", n, &r)
        return r, err
}

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

package main

import (
        "github.com/hashicorp/go-plugin"
        "example.com/plug"
)

type A struct{}

func (a A) SayHello(n string) (string, error) {
        return "hello, " + n, nil
}

func main() {
        plugin.Serve(&plugin.ServeConfig{
                HandshakeConfig: plug.Handshake,
                Plugins: plugin.PluginSet{
                        "a": &plug.P{&A{}},
                },
        })
}

example.com/main.go:

package main

import (
        "fmt"
        "log"
        "os/exec"

        "github.com/hashicorp/go-plugin"
        "example.com/plug"
)

func main() {
        client := plugin.NewClient(&plugin.ClientConfig{
                HandshakeConfig: plug.Handshake,
                Plugins: plugin.PluginSet{
                        "a": &plug.P{},
                },
                Cmd: exec.Command("./a"),
        })
        defer client.Kill()

        realClient, err := client.Client()
        if err != nil {
                log.Fatal(err)
        }

        ai, err := realClient.Dispense("a")
        if err != nil {
                log.Fatal(err)
        }
        a := ai.(plug.Plug)

        fmt.Println(a.SayHello("a"))
}

grpc

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

example.com/plug/plug.proto:

syntax = "proto3";

option go_package = "example.com/plug";

service Plug {
  rpc SayHello(HelloReq) returns (HelloRes) {}
}
message HelloReq {
  string n = 1;
}
message HelloRes {
  string msg = 1;
}

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

package main

import (
        "context"
        "log"
        "net"
        "os"

        "example.com/plug"
        "google.golang.org/grpc"
)

type Server struct {
        grpcplug.UnimplementedPlugServer
}

func (s *Server) SayHello(ctx context.Context, req *plug.HelloReq) (*plug.HelloRes, error) {
        return &plug.HelloRes{Msg: "hello, " + req.N}, nil
}

func main() {
        lis, err := net.Listen("tcp", os.Args[1])
        if err != nil {
                log.Fatal(err)
        }
        s := grpc.NewServer()
        plug.RegisterPlugServer(s, &Server{})
        s.Serve(lis)
}

example.com/main.go:

package main

import (
        "context"
        "fmt"
        "log"
        "os/exec"

        "example.com/plug"
        "google.golang.org/grpc"
)

func main() {
        fmt.Println(exec.Command("./a", "127.0.0.1:8888").Start())
        conn, err := grpc.Dial("127.0.0.1:8888", grpc.WithInsecure(), grpc.WithBlock())
        if err != nil {
                log.Fatal(err)
        }
        client := plug.NewPlugClient(conn)

        res, err := client.SayHello(context.TODO(), &plug.HelloReq{
                N: "a",
        })
        fmt.Println(res.Msg, err)
}