├── .travis.yml ├── sample ├── app │ ├── extensions │ │ └── hello.go │ └── main.go ├── plugin │ └── hello.go └── README.md ├── .gitignore ├── cmd └── multibuild │ ├── README.md │ └── multibuild.go ├── plugin_test.go ├── LICENSE ├── plugin.go └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.3 5 | - 1.4 6 | - 1.5 7 | - tip 8 | -------------------------------------------------------------------------------- /sample/app/extensions/hello.go: -------------------------------------------------------------------------------- 1 | package extensions 2 | 3 | // Hello is an example extension interface 4 | type Hello interface { 5 | Say() 6 | } 7 | -------------------------------------------------------------------------------- /sample/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/cafxx/pluggo" 5 | "github.com/cafxx/pluggo/sample/app/extensions" 6 | ) 7 | 8 | func main() { 9 | h := pluggo.Get("hello") 10 | if hello, ok := h.(extensions.Hello); ok { 11 | hello.Say() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /sample/plugin/hello.go: -------------------------------------------------------------------------------- 1 | package helloplugin 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cafxx/pluggo" 7 | ) 8 | 9 | func init() { 10 | pluggo.Register("hello", func() interface{} { 11 | return &hello{} 12 | }) 13 | } 14 | 15 | type hello struct { 16 | } 17 | 18 | func (*hello) Say() { 19 | fmt.Println("Hello pluggo") 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /sample/README.md: -------------------------------------------------------------------------------- 1 | This directory contains a sample application (in `app/`) and plugin (in 2 | `plugin/`). The app will call the Say() method on the interface instance 3 | provided by the plugin. 4 | 5 | # Try it out 6 | 7 | Let's build just the application first and check that it prints nothing because 8 | the plugin is not loaded: 9 | 10 | ``` 11 | $ go build github.com/cafxx/pluggo/sample/app 12 | $ ./app 13 | ``` 14 | 15 | Let's build and link the application and the plugin: the plugin gets called and 16 | it prints "Hello pluggo": 17 | 18 | ``` 19 | $ multibuild github.com/cafxx/pluggo/sample/app github.com/cafxx/pluggo/sample/plugin 20 | $ ./app 21 | Hello pluggo 22 | ``` 23 | -------------------------------------------------------------------------------- /cmd/multibuild/README.md: -------------------------------------------------------------------------------- 1 | # multibuild 2 | A small tool to build and link together multiple go packages 3 | 4 | ## Purpose 5 | This tool was designed to work with [pluggo](https://github.com/CAFxX/pluggo), 6 | to perform linking of plugins with the application/library they're intended for. 7 | 8 | It can also be used in other cases, where you're importing a package only for 9 | its side-effects, e.g.: 10 | 11 | - To link additional `database/sql` drivers 12 | - To link `pprof` at compile-time 13 | - ... 14 | 15 | ## Installation 16 | go install github.com/cafxx/pluggo/cmd/multibuild 17 | 18 | ## Usage 19 | `multibuild ...` will build `` (as 20 | if `go build ` had been invoked) and link it together with the 21 | additional ``s. 22 | 23 | `multibuild -h` prints usage instructions. 24 | 25 | ## FAQ 26 | 27 | ### Why is this not part of `go build`? 28 | Good question. It is literally beyond me. 29 | -------------------------------------------------------------------------------- /plugin_test.go: -------------------------------------------------------------------------------- 1 | package pluggo 2 | 3 | import "testing" 4 | 5 | func TestRegister1(t *testing.T) { 6 | err := Register("ep1", func() interface{} { 7 | return "1" 8 | }) 9 | if err != nil { 10 | t.Fatal(err) 11 | } 12 | } 13 | 14 | func TestRegister2(t *testing.T) { 15 | err := Register("ep2", func() interface{} { 16 | return "2" 17 | }) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | } 22 | 23 | func TestRegisterDuplicated(t *testing.T) { 24 | err := Register("ep1", func() interface{} { 25 | return "3" 26 | }) 27 | if err == nil { 28 | t.Fatal("expected to fail duplicated registration") 29 | } 30 | } 31 | 32 | func TestGet(t *testing.T) { 33 | ep1 := Get("ep1").(string) 34 | if ep1 != "1" { 35 | t.Fatal("plugin returned unexpected instance") 36 | } 37 | } 38 | 39 | func TestGetUnknown(t *testing.T) { 40 | ep := Get("ep-unknown") 41 | if ep != nil { 42 | t.Fatal("unexpected plugin instance returned for unknown extension point") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Carlo Alberto Ferraris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /plugin.go: -------------------------------------------------------------------------------- 1 | // Package pluggo provides a compile-time, in-process plugin framework. It 2 | // allows to define interface-based extension points in your code, so that users 3 | // of your code can plug in their modifications at compile time while keeping 4 | // the application code and plugin code in completely separated packages or 5 | // repositories. 6 | package pluggo 7 | 8 | import ( 9 | "fmt" 10 | "sync" 11 | ) 12 | 13 | // Factory functions should return instances of the interface appropriate for 14 | // the extension point they have been registered for. The pluggo framework 15 | // enforces no rules regarding the kind of interface factories should return: 16 | // this is delegated to the contract between calling code and plugins. 17 | type Factory func() interface{} 18 | 19 | var plugins = make(map[string]Factory) 20 | var lock = sync.RWMutex{} 21 | 22 | // Register should be called from a plugin init() function to register a Factory 23 | // for the named extension point. Naming of the extension points is delegated to 24 | // the contract between calling code and plugins. It is illegal to register more 25 | // than one Factory for the same extension point. 26 | // 27 | // It is strongly recommended that plugins perform no initialization in init() 28 | // aside from registering with pluggo. Any initialization procedures should be 29 | // deferred at least until the first call to the Factory function. 30 | func Register(name string, f Factory) error { 31 | lock.Lock() 32 | defer lock.Unlock() 33 | if _, exists := plugins[name]; exists { 34 | return fmt.Errorf("extension point %s has already a registered factory", name) 35 | } 36 | plugins[name] = f 37 | return nil 38 | } 39 | 40 | // Get is used to get an instance of the interface returned by the Factory 41 | // registered for the specified extension point. The instantiation semantics 42 | // are not defined by pluggo: they should be defined by the contract between 43 | // calling code and plugins. It is safe to call Get concurrently from multiple 44 | // goroutines. 45 | func Get(name string) interface{} { 46 | lock.RLock() 47 | factory := plugins[name] 48 | lock.RUnlock() 49 | if factory == nil { 50 | return nil 51 | } 52 | return factory() 53 | } 54 | -------------------------------------------------------------------------------- /cmd/multibuild/multibuild.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "go/build" 7 | "io/ioutil" 8 | "os" 9 | "os/exec" 10 | 11 | "github.com/kisielk/gotool" 12 | ) 13 | 14 | func usage() { 15 | fmt.Println("multibuild builds one or more packages and links them together") 16 | fmt.Println("multibuild [*]") 17 | fmt.Println(" main package to build") 18 | fmt.Println(" additional package to link with ") 19 | fmt.Println(" it is possible to specify more than one ") 20 | fmt.Println(" can't be a package named \"main\"") 21 | fmt.Println("The resulting build will have the package name of ") 22 | } 23 | 24 | func multibuild(packages []string) (error, int) { 25 | // parse the names of the paths/packages provided on the command-line 26 | pkgs := gotool.ImportPaths(packages) 27 | 28 | // parse the main package: we need the package name and the path 29 | mainpkg, err := build.Import(pkgs[0], ".", 0) 30 | if err != nil { 31 | return fmt.Errorf("unable to import main package %s: %s", pkgs[0], err), -2 32 | } 33 | 34 | // create the linker file in the main package directory, so that it will be 35 | // compiled and linked with the main package when we `go build` is invoked 36 | // the linker file is a go file in the same package of the main package that 37 | // imports all additional packages (normally for their side-effects) 38 | tmpFile, err := ioutil.TempFile(mainpkg.Dir, "pluggo") 39 | if err != nil { 40 | return fmt.Errorf("unable to create temporary file: %s", err), -3 41 | } 42 | 43 | fmt.Fprintf(tmpFile, "package %s\n", mainpkg.Name) 44 | for _, pkgname := range pkgs[1:] { 45 | fmt.Fprintf(tmpFile, "import _ \"%s\"\n", pkgname) 46 | } 47 | 48 | tmpFile.Close() 49 | os.Rename(tmpFile.Name(), tmpFile.Name()+".go") 50 | defer os.Remove(tmpFile.Name() + ".go") 51 | 52 | // run go build on the main package 53 | output, err := exec.Command("go", "build", packages[0]).CombinedOutput() 54 | if err != nil { 55 | return fmt.Errorf("error executing go build: %s\ngo build output:\n%s", err, string(output)), -4 56 | } 57 | 58 | return nil, 0 59 | } 60 | 61 | func main() { 62 | flag.Usage = usage 63 | flag.Parse() 64 | 65 | if len(flag.Args()) == 0 { 66 | fmt.Println("missing arguments") 67 | usage() 68 | os.Exit(-1) 69 | } 70 | 71 | err, rv := multibuild(flag.Args()) 72 | if err != nil { 73 | fmt.Println(err) 74 | os.Exit(rv) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pluggo 2 | Golang compile-time, in-process plugin framework 3 | 4 | [![GoDoc](https://godoc.org/github.com/CAFxX/pluggo?status.svg)](https://godoc.org/github.com/CAFxX/pluggo) 5 | [![Build Status](https://travis-ci.org/CAFxX/pluggo.svg?branch=master)](https://travis-ci.org/CAFxX/pluggo) 6 | [![Coverage](http://gocover.io/_badge/github.com/CAFxX/pluggo)](http://gocover.io/github.com/CAFxX/pluggo) 7 | 8 | ## Purpose 9 | Pluggo allows you to define interface-based extension points in your code, so 10 | that users of your code can plug in their modifications at compile time while 11 | keeping the application code and plugin code in completely separated packages 12 | and repositories. 13 | 14 | Compared to RPC/IPC approaches, plugins using this framework run in the same 15 | process as the application with no IPC/RPC overhead. 16 | 17 | ## How it works 18 | Similarly as how `database/sql` drivers registers themselves: pluggo keeps an 19 | internal factory registry (just a `map[string]func() interface{}`) where 20 | plugins `Register` their factories. 21 | Application code at each extension point requests to the registry instances of 22 | the plugin using `Get`. Application and plugins are then compiled and linked 23 | together in the same executable with the [`multibuild`](cmd/multibuild) tool. 24 | 25 | ## Examples 26 | 27 | ### Extension point pattern 28 | Let's say you have a place in your code where you greet the user: 29 | 30 | ``` 31 | fmt.Printf("Hello %s!\n", username) 32 | ``` 33 | 34 | Now, let's pretend you want plugins to be able to override what gets printed. 35 | 36 | First, you define a public interface that plugins can implement: 37 | 38 | ``` 39 | type Greeter interface { 40 | Greet(who string) string 41 | } 42 | ``` 43 | 44 | then you change your greeting code to: 45 | 46 | ``` 47 | userGreeter := pluggo.Get("userGreeter").(Greeter) 48 | 49 | if userGreeter != nil { 50 | fmt.Print(userGreeter.Greet(username)) 51 | } else { 52 | fmt.Printf("Hello %s!", username) // behavior in case no plugin was defined 53 | } 54 | ``` 55 | 56 | Now if somebody else wants to implement a plugin to change the greeting they 57 | just have to implement the interface and register it: 58 | 59 | ``` 60 | func init() { 61 | pluggo.Register("userGreeter", func() interface{} { 62 | return &shouter{} 63 | }) 64 | } 65 | 66 | type shouter struct { 67 | } 68 | 69 | func (*shouter) Greet(who string) string { 70 | return fmt.Sprintf("WASSUP %s!!!", strings.ToUpper(who)) 71 | } 72 | ``` 73 | 74 | To enable the plugin you have to link it in at compile time. To simplify this 75 | process a very rudimental tool called `multibuild` is provided. This tool builds 76 | and links together a main package (the application) with one or more additional 77 | packages (the plugins). 78 | 79 | ``` 80 | $ multibuild appMainPkg pluginPkg1 pluginPkg2 ... 81 | ``` 82 | 83 | You can have a look at the [sample](sample) directory for a ready-to-run 84 | example. 85 | 86 | Note that if a unknown extension point name (or `""`) is requested, `Get` will 87 | simply return `nil`. 88 | 89 | ### Factory pattern 90 | 91 | Whereas in the extension point pattern plugin code is responsible to register 92 | for a specific extension point, in the factory pattern plugins register 93 | themselves with a unique name and choosing which one to use for a certain 94 | extension point is delegated to the application. 95 | 96 | In the previous example, this would mean replacing the plugin instantiation code 97 | from: 98 | 99 | ``` 100 | userGreeter := pluggo.Get("userGreeter").(Greeter) 101 | ``` 102 | 103 | to 104 | 105 | ``` 106 | userGreeter := pluggo.Get(conf.plugins.userGreeter).(Greeter) 107 | ``` 108 | 109 | where `conf.plugins.userGreeter` will likely come from the configuration 110 | mechanism in use by your application. This allows to choose which plugin to use 111 | at runtime. Note that if a unknown plugin name (or `""`) is requested, `Get` 112 | will simply return `nil`. 113 | 114 | ## FAQ 115 | 116 | ### Why did you write this? 117 | Sometimes the functionalities you need in a upstream application or library are 118 | not useful in the context of the upstream project. Sure, you can fork the 119 | upstream but that creates a great deal of maintenance burden. 120 | 121 | Extension points have negligible performance overhead unless used in very tight 122 | loops (where, most likely, you shouldn't use them anyway). Having extensions 123 | points defined upstream may prove less controversial and will allow any 124 | interested users to easily provide their own extensions. 125 | 126 | ### Can it dynamically link/unlink plugins at runtime? 127 | Short answer: no. Plugins are linked at compile-time and can't be unlinked. 128 | 129 | Longer answer: something could be hacked together using a mixture of this 130 | approach, CGO and LD_PRELOAD. But nothing of the sort has been implemented here: 131 | for now you can only load plugins at compile time. 132 | 133 | That being said, you can link multiple plugins at compile time and then choose 134 | which ones to load at runtime. See the "Can it dynamically load/unload plugins 135 | at runtime?" question below. 136 | 137 | While not being able to link plugins at runtime is a limitation, it has some 138 | clear upsides: you are effectively vendoring plugins so you sidestep all kind 139 | of version incompatibility issues (DLL hell, anyone?), you maintain the "single 140 | binary" nature of compiled Go programs and, most importantly, all Go tools keep 141 | working correctly (test, pprof, etc.). 142 | 143 | ### Can it dynamically load/unload plugins at runtime? 144 | Plugins all only registered at process initialization time, but normally this is 145 | really quick (it boils down to inserting one entry per factory in a map). 146 | 147 | Plugin instantiation ("loading") on the other side is delegated to the interface 148 | between the calling code (the application) and plugins. Because instantiation 149 | is triggered by the calling code, it may never happen for a certain plugin if 150 | the calling code decides that it is not needed (normally in response to 151 | configuration or user input). See the factory pattern above for a generic 152 | example of how to do it. 153 | 154 | Similarly, plugin unloading is delegated to the interface between the calling 155 | code and the plugins: if a plugin is able to shutdown and clean after itself 156 | (e.g. terminate its goroutines, remove global state, etc.) all memory used by 157 | the plugin instance should be eventually reclaimed by the go GC. 158 | 159 | ### Can I supply some parameters/configurations to a plugin? 160 | It's not part of the framework right now, but I'm considering it. I'd like to 161 | avoid feature creep if possible, so I'm taking some time to come up with a 162 | minimal yet flexible design. 163 | 164 | Keep in mind that nothing prevents you from designing the interface that plugins 165 | should implement to enable providing configuration to plugin implementations. 166 | 167 | Recycling the `Greeter` example above, you could add a `Init` functions that has 168 | to be called once after instantiation: 169 | 170 | ``` 171 | type Greeter interface { 172 | Init(conf GreeterConfig) 173 | Greet(who string) string 174 | } 175 | 176 | type GreeterConfig struct { 177 | // ... 178 | } 179 | ``` 180 | 181 | ``` 182 | userGreeter := pluggo.Get("userGreeter").(Greeter) 183 | 184 | if userGreeter != nil { 185 | err := userGreeter.Init(userGreeterConfig) 186 | if err != nil { 187 | // handle error 188 | } 189 | 190 | fmt.Print(userGreeter.Greet(username)) 191 | } else { 192 | fmt.Printf("Hello %s!", username) // behavior in case no plugin was defined 193 | } 194 | ``` 195 | 196 | ### Do I have to instantiate the plugin every time I use it? 197 | It depends on the contract between calling code and plugins. Pluggo does not 198 | dictate any convention about this: if you define the contract to be "the plugin 199 | is instantiated only once per process" you can definitely instantiate it once 200 | and reuse it multiple times. 201 | 202 | ### Isn't all of this very low-level? 203 | Yes, it is. Pluggo is just the foundation of a plugin system right now. But 204 | because every additional feature risks being very opinionated I'm inclined to 205 | keep this library small and well-scoped and build a higher-level framework on 206 | top of it. 207 | 208 | A possible way forward is to define a set of strictly optional interfaces that 209 | plugins can choose to implement to respond to standard plugin lifecycle events. 210 | This would be very generic and completely optional. 211 | 212 | ## Potential directions 213 | - Implement lifecycle interfaces 214 | - start plugin 215 | - configure plugin 216 | - enumerate plugin interfaces 217 | - start plugin instance 218 | - stop plugin instance 219 | - stop plugin 220 | - Implement plugin enumeration 221 | 222 | ## License 223 | [MIT](LICENSE) 224 | --------------------------------------------------------------------------------