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
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}
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}
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}