├── .gitignore ├── host ├── host ├── demo │ ├── demo │ └── demo.go ├── host.go └── README.md ├── README.md ├── examples ├── simple │ ├── example.js │ └── simple.go └── extensions │ ├── module2.js │ ├── module1.js │ └── extensions.go ├── LICENSE ├── ottojs └── ottojs.go └── scripting.go /.gitignore: -------------------------------------------------------------------------------- 1 | examples/simple/simple 2 | examples/extensions/extensions -------------------------------------------------------------------------------- /host/host: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrium/go-scripting/master/host/host -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-scripting 2 | 3 | Common API to support scripting languages in Go -------------------------------------------------------------------------------- /host/demo/demo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrium/go-scripting/master/host/demo/demo -------------------------------------------------------------------------------- /examples/simple/example.js: -------------------------------------------------------------------------------- 1 | 2 | function helloworld() { 3 | println("Hello world!") 4 | } -------------------------------------------------------------------------------- /examples/extensions/module2.js: -------------------------------------------------------------------------------- 1 | implements(__module__, "ProgramObserver") 2 | 3 | function ProgramStarted() { 4 | println("module2 got started") 5 | } 6 | 7 | function ProgramFinished() { 8 | println("module2 got finished") 9 | } 10 | -------------------------------------------------------------------------------- /examples/extensions/module1.js: -------------------------------------------------------------------------------- 1 | implements(__module__, "ProgramObserver") 2 | 3 | function ProgramStarted() { 4 | println(__module__ + " got started") 5 | } 6 | 7 | function ProgramFinished() { 8 | println(__module__ + " got finished") 9 | } 10 | -------------------------------------------------------------------------------- /examples/simple/simple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/progrium/go-scripting" 7 | "github.com/progrium/go-scripting/ottojs" 8 | ) 9 | 10 | func main() { 11 | ottojs.Register() 12 | scripting.LoadModulesFromPath(".") 13 | scripting.UpdateGlobals(map[string]interface{}{ 14 | "println": fmt.Println, 15 | }) 16 | scripting.Call("example", "helloworld", nil) 17 | } -------------------------------------------------------------------------------- /host/host.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "fmt" 6 | 7 | "github.com/progrium/duplex/prototype" 8 | ) 9 | 10 | type EventArgs struct { 11 | Payload string 12 | } 13 | 14 | type EventReply struct {} 15 | 16 | type RegisterArgs struct { 17 | Interface string 18 | Name string 19 | Service string 20 | } 21 | 22 | type RegisterReply struct {} 23 | 24 | type EventPrinter int 25 | 26 | func (p *EventPrinter) Event(args EventArgs, reply *EventReply) error { 27 | fmt.Println("EventPrinter:", args.Payload) 28 | return nil 29 | } 30 | 31 | func main() { 32 | peer := duplex.NewPeer() 33 | defer peer.Close() 34 | if err := peer.Connect("127.0.0.1:9877"); err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | err := peer.Call("Extensions.Register", &RegisterArgs{"EventObserver", peer.Name(), "EventPrinter"}, new(RegisterReply)) 39 | if err != nil { 40 | log.Fatal(err) 41 | 42 | } 43 | 44 | peer.Register(new(EventPrinter)) 45 | peer.Serve() 46 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Jeff Lindsay 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /examples/extensions/extensions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/progrium/go-extensions" 8 | "github.com/progrium/go-scripting" 9 | "github.com/progrium/go-scripting/ottojs" 10 | ) 11 | 12 | var observers = extensions.ExtensionPoint(new(ProgramObserver)) 13 | 14 | type ProgramObserver struct { 15 | ProgramStarted func() 16 | ProgramFinished func() 17 | } 18 | 19 | func main() { 20 | ottojs.Register() 21 | scripting.UpdateGlobals(map[string]interface{}{ 22 | "println": fmt.Println, 23 | "implements": func(module, iface string) { 24 | proxy := extensions.NewProxy(iface, 25 | func (method string, args []interface{}) interface{} { 26 | value, err := scripting.Call(module, method, args) 27 | if err != nil { 28 | log.Println("error calling into", module, "with", method) 29 | return nil 30 | } 31 | return value 32 | }, 33 | ) 34 | extensions.RegisterWithName(iface, proxy, module) 35 | }, 36 | }) 37 | scripting.LoadModulesFromPath(".") 38 | 39 | for _, observer := range observers.All() { 40 | observer.(ProgramObserver).ProgramStarted() 41 | } 42 | 43 | fmt.Println("NORMALLY A PROGRAM DOES STUFF HERE") 44 | 45 | for _, observer := range observers.All() { 46 | observer.(ProgramObserver).ProgramFinished() 47 | } 48 | } -------------------------------------------------------------------------------- /host/README.md: -------------------------------------------------------------------------------- 1 | # host / rpc extensions experiment 2 | 3 | This is eventually going to be a sub project that wraps go-scripting in a binary that connects the scripted extensions with a remote application. This binary could be run by go-coproc from the application. 4 | 5 | However, for now it's just testing how you can expose go-extensions across applications using Duplex RPC. 6 | 7 | The `demo` project folder is a pretend application that has an `EventObserver` extension point. It will allow other applications to extend it, in this case the application here called `host`. 8 | 9 | `demo` provides an `EventObserver` extension point and uses this to produce an event every 5 seconds using the single method `Event`. It also binds a Duplex socket exposing a `Extensions.Register` service. This is for applications to connect to that want to remotely register against the extension point. 10 | 11 | `host` implements an `EventPrinter` with the `EventObserver` interface which just prints out the event payload. It exposes this as a service over Duplex RPC. It calls the `Extensions.Register` service on its remote peer to register it. 12 | 13 | `demo` implements a `RemoteProxy` that acts as a wrapper for making the RPC calls for any remote extension to `EventObserver`. This is unfortunate boilerplate and is tightly coupled to the interfaces a remote program wants to implement. There is a commented out attempt at a dynamic version of `RemoteProxy`, however you can see that the static typing involved in RPC calls does not match with the `interface{}` based generic function signature of the proxy. -------------------------------------------------------------------------------- /host/demo/demo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "time" 6 | "fmt" 7 | 8 | "github.com/progrium/go-extensions" 9 | "github.com/progrium/duplex/prototype" 10 | ) 11 | 12 | var observers = extensions.ExtensionPoint(new(EventObserver)) 13 | 14 | type EventObserver interface { 15 | Event(payload string) 16 | } 17 | 18 | type RemoteProxy struct{ 19 | peer *duplex.Peer 20 | service string 21 | peerName string 22 | } 23 | 24 | type EventArgs struct { 25 | Payload string 26 | } 27 | 28 | func (p *RemoteProxy) Event(payload string) { 29 | // TODO: replace with "CallTo" (Call that uses OpenWith) using p.peerName 30 | err := p.peer.Call(p.service+".Event", &EventArgs{payload}, new(struct{})) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | } 35 | 36 | 37 | type Extensions struct { 38 | peer *duplex.Peer 39 | } 40 | 41 | type RegisterArgs struct { 42 | Interface string 43 | Name string 44 | Service string 45 | } 46 | 47 | type RegisterReply struct {} 48 | 49 | 50 | func (e *Extensions) Register(args RegisterArgs, reply *RegisterReply) error { 51 | fmt.Println("register:", args.Interface, args.Name, args.Service) 52 | /* An attempt to do a dynamic proxy fails because of the types involved in doing Calls 53 | proxy := extensions.NewProxy(args.Interface, 54 | func (method string, a []interface{}) interface{} { 55 | err := e.peer.Call(args.Service+"."+method, &EventArgs{a[0].(string)}, new(struct{})) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | return nil 60 | }, 61 | ) */ 62 | extensions.RegisterWithName(args.Interface, &RemoteProxy{e.peer, args.Service, args.Name}, args.Name) 63 | return nil 64 | } 65 | 66 | 67 | 68 | func main() { 69 | peer := duplex.NewPeer() 70 | defer peer.Close() 71 | if err := peer.Bind("127.0.0.1:9877"); err != nil { 72 | log.Fatal(err) 73 | } 74 | 75 | peer.Register(&Extensions{peer}) 76 | go peer.Serve() 77 | 78 | for { 79 | time.Sleep(5 * time.Second) 80 | for _, observer := range observers.All() { 81 | observer.(EventObserver).Event("Hello") 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /ottojs/ottojs.go: -------------------------------------------------------------------------------- 1 | package ottojs 2 | 3 | import ( 4 | "reflect" 5 | "sync" 6 | "strings" 7 | 8 | "github.com/robertkrimen/otto" 9 | "github.com/progrium/go-scripting" 10 | ) 11 | 12 | func Register() { 13 | scripting.Runtimes.Register("js", &RuntimeEngine{ 14 | modules: make(map[string]*otto.Otto), 15 | }) 16 | } 17 | 18 | type RuntimeEngine struct { 19 | sync.Mutex 20 | modules map[string]*otto.Otto 21 | } 22 | 23 | func (r *RuntimeEngine) FileExtension() string { 24 | return ".js" 25 | } 26 | 27 | func (r *RuntimeEngine) InitModule(name, source string, globals map[string]interface{}) error { 28 | r.Lock() 29 | defer r.Unlock() 30 | r.modules[name] = otto.New() 31 | r.modules[name].Run(`__module__ = "`+name+`"`) 32 | r.setModuleGlobals(name, globals) 33 | r.modules[name].Run(source) 34 | return nil 35 | } 36 | 37 | func (r *RuntimeEngine) CallModule(name, function string, args []interface{}) (interface{}, error) { 38 | r.Lock() 39 | context := r.modules[name] 40 | r.Unlock() 41 | value, err := context.Call(function, nil, args...) 42 | if err != nil { 43 | return nil, err 44 | } 45 | exported, _ := value.Export() 46 | return exported, nil 47 | } 48 | 49 | func (r *RuntimeEngine) setModuleGlobals(name string, globals map[string]interface{}) { 50 | context := r.modules[name] 51 | for k, v := range globals { 52 | if reflect.TypeOf(v).Kind() == reflect.Func { 53 | setValueAtPath(context, k, funcToOtto(context, reflect.ValueOf(v))) 54 | } else { 55 | setValueAtPath(context, k, v) 56 | } 57 | } 58 | } 59 | 60 | func (r *RuntimeEngine) UpdateGlobals(globals map[string]interface{}) { 61 | r.Lock() 62 | defer r.Unlock() 63 | for module := range r.modules { 64 | r.setModuleGlobals(module, globals) 65 | } 66 | } 67 | 68 | func setValueAtPath(context *otto.Otto, path string, value interface{}) { 69 | parts := strings.Split(path, ".") 70 | parentCount := len(parts) - 1 71 | if parentCount > 0 { 72 | parentPath := strings.Join(parts[0:parentCount], ".") 73 | parent, err := context.Object("(" + parentPath + ")") 74 | if err != nil { 75 | emptyObject, _ := context.Object(`({})`) 76 | setValueAtPath(context, parentPath, emptyObject) 77 | } 78 | parent, _ = context.Object("(" + parentPath + ")") 79 | parent.Set(parts[parentCount], value) 80 | } else { 81 | context.Set(path, value) 82 | } 83 | } 84 | 85 | func funcToOtto(context *otto.Otto, fn reflect.Value) interface{} { 86 | return func(call otto.FunctionCall) otto.Value { 87 | convertedArgs := make([]reflect.Value, 0) 88 | for _, v := range call.ArgumentList { 89 | exported, _ := v.Export() 90 | convertedArgs = append(convertedArgs, reflect.ValueOf(exported)) 91 | } 92 | ret := fn.Call(convertedArgs) 93 | if len(ret) > 0 { 94 | val, _ := context.ToValue(ret[0].Interface()) 95 | return val 96 | } else { 97 | return otto.UndefinedValue() 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /scripting.go: -------------------------------------------------------------------------------- 1 | package scripting 2 | 3 | import ( 4 | "sync" 5 | "strings" 6 | "errors" 7 | "io/ioutil" 8 | "path/filepath" 9 | 10 | "github.com/progrium/go-extensions" 11 | ) 12 | 13 | type RuntimeEngine interface { 14 | FileExtension() string 15 | CallModule(name, function string, args []interface{}) (interface{}, error) 16 | InitModule(name, source string, globals map[string]interface{}) error 17 | UpdateGlobals(globals map[string]interface{}) 18 | } 19 | 20 | var Runtimes = extensions.ExtensionPoint(new(RuntimeEngine)) 21 | 22 | var scripting = struct { 23 | sync.Mutex 24 | globals map[string]interface{} 25 | modules map[string]RuntimeEngine 26 | } { 27 | globals: make(map[string]interface{}), 28 | modules: make(map[string]RuntimeEngine), 29 | } 30 | 31 | func UpdateGlobals(globals map[string]interface{}) { 32 | scripting.Lock() 33 | defer scripting.Unlock() 34 | for k, v := range globals { 35 | scripting.globals[k] = v 36 | } 37 | for _, r := range Runtimes.All() { 38 | r.(RuntimeEngine).UpdateGlobals(scripting.globals) 39 | } 40 | } 41 | 42 | func GetGlobal(name string) interface{} { 43 | scripting.Lock() 44 | defer scripting.Unlock() 45 | return scripting.globals[name] 46 | } 47 | 48 | func LoadModule(name, source string, runtime string) error { 49 | scripting.Lock() 50 | defer scripting.Unlock() 51 | r := Runtimes.Get(runtime).(RuntimeEngine) 52 | err := r.InitModule(name, source, scripting.globals) 53 | if err != nil { 54 | return err 55 | } 56 | scripting.modules[name] = r 57 | return nil 58 | } 59 | 60 | func findRuntimeForFile(path string) string { 61 | for name, runtime := range Runtimes.All() { 62 | fileExt := runtime.(RuntimeEngine).FileExtension() 63 | if fileExt != "" && strings.HasSuffix(path, fileExt) { 64 | return name 65 | } 66 | } 67 | return "" 68 | } 69 | 70 | func LoadModuleFile(path string) error { 71 | data, err := ioutil.ReadFile(path) 72 | if err != nil { 73 | return err 74 | } 75 | runtime := findRuntimeForFile(path) 76 | if runtime == "" { 77 | return errors.New("scripting: no runtime found to handle: " + path) 78 | } 79 | name := strings.Split(filepath.Base(path), ".")[0] 80 | return LoadModule(name, string(data), runtime) 81 | } 82 | 83 | func LoadModulesFromPath(path string) error { 84 | dir, err := ioutil.ReadDir(path) 85 | if err != nil { 86 | return err 87 | } 88 | for _, entry := range dir { 89 | filepath := path + "/" + entry.Name() 90 | runtime := findRuntimeForFile(filepath) 91 | if runtime != "" { 92 | err = LoadModuleFile(filepath) 93 | if err != nil { 94 | return err 95 | } 96 | } 97 | } 98 | return nil 99 | } 100 | 101 | func Call(module, function string, args []interface{}) (interface{}, error) { 102 | scripting.Lock() 103 | runtime, ok := scripting.modules[module] 104 | scripting.Unlock() 105 | if !ok { 106 | return nil, errors.New("scripting: no such module loaded: " + module) 107 | } 108 | return runtime.(RuntimeEngine).CallModule(module, function, args) 109 | } 110 | --------------------------------------------------------------------------------