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
:
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"))
}
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"))
}
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)
}