Hi all!
I've needed a very simple plugin system, to separate the core, pure-go program from the specific, "cgo-polluted" parts.
And thanks to interfaces and net/rpc, I've found a very simple solution: just call the plugin from the core program
with os/exec, and use stdin/stdout for communication with RPC.
// NewClient creates a new RPC client.
func NewClient(pluginName string) (*rpc.Client, error) {
cmd := exec.Command("my-plugins-" + pluginName)
in, err := cmd.StdinPipe()
if err != nil {
return nil, err
}
out, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
return nil, err
}
cc := procCloser{cmd.Process}
inout := struct {
io.Reader
io.Writer
io.Closer
}{out, in, multiCloser{[]io.Closer{in, out, cc}}}
return rpc.NewClient(inout), nil
}
// NewServer creates a new server using the process' stdin and stdout.
// Does not return, starts serving.
func NewServerServe(rcvr HookRPC) {
s := rpc.NewServer()
s.RegisterName("Hook", rcvr)
s.ServeConn(struct {
io.Reader
io.Writer
io.Closer
}{os.Stdin, os.Stdout,
multiCloser{[]io.Closer{os.Stdout, os.Stdin, procCloser{}}},
})
}
A more complete example is here:
http://play.golang.org/p/TWq1vsB0Xj
Another nice trick I've found that is net/rpc finds exported function using reflect, and complains on non-rpcable functions.
This can be solved by simply wrapping our PluginRPC interface implementing thing in a
struct rpcWrap struct {
PluginRPC
}
and using that. This way net/rpc sees only those specific function in PluginRPC which we wanted to export.