From e5571defb7a5efb5ec5477cf1219b51b7b920a72 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sun, 27 Aug 2023 20:39:35 +1000 Subject: [PATCH] [v3] Support bound methodID aliases. Support `[]any` for bindings generation. Use `CallByID` in bindings. --- v3/V3 Changes.md | 26 ++++++++++- v3/examples/binding/bindings_main.js | 4 +- v3/examples/binding/go.sum | 9 ++-- v3/examples/binding/main.go | 6 ++- v3/internal/parser/bindings.go | 2 +- v3/internal/parser/parser.go | 12 ++++- v3/pkg/application/application.go | 2 +- v3/pkg/application/bindings.go | 54 ++++++++++++++++++----- v3/pkg/application/options_application.go | 1 + 9 files changed, 91 insertions(+), 25 deletions(-) diff --git a/v3/V3 Changes.md b/v3/V3 Changes.md index 89201f952..6174fcba8 100644 --- a/v3/V3 Changes.md +++ b/v3/V3 Changes.md @@ -72,7 +72,31 @@ The clipboard API has been simplified. There is now a single `Clipboard` object ## Bindings -TBD +Bindings work in a similar way to v2, by providing a means to bind struct methods to the frontend. These can be called in the frontend using the binding wrappers generated by the `wails3 generate bindings` command. +Bound methods are identified as uint32 IDs, calculated using the [FNV hashing algorithm](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function). This is to prevent the method name from being exposed in production builds. +In debug mode, the method IDs are logged along with the calculated ID of the method to aid in debugging. If you wish to add an extra layer of obfuscation, you can use the `BindAliases` option. This allows you to specify a map of alias IDs to method IDs. When the frontend calls a method using an ID, the method ID will be looked up in the alias map first for a match. If it does not find it, it assumes it's a standard method ID and tries to find the method in the usual way. + +Example: + +```go + app := application.New(application.Options{ + Bind: []any{ + &GreetService{}, + }, + BindAliases: map[uint32]uint32{ + 1: 1411160069, + 2: 4021313248, + }, + Assets: application.AssetOptions{ + FS: assets, + }, + Mac: application.MacOptions{ + ApplicationShouldTerminateAfterLastWindowClosed: true, + }, + }) +``` + +We can now call using this alias in the frontend: `wails.Call(1, "world!")`. ## Dialogs diff --git a/v3/examples/binding/bindings_main.js b/v3/examples/binding/bindings_main.js index 8c6aba068..9e03a2fd7 100644 --- a/v3/examples/binding/bindings_main.js +++ b/v3/examples/binding/bindings_main.js @@ -14,7 +14,7 @@ window.go.main = { * @param name {string} * @returns {Promise} **/ - Greet: function(name) { wails.Call({"wails-method-id":1411160069, args: Array.prototype.slice.call(arguments, 0)}); }, + Greet: function(name) { wails.CallByID(1411160069, ...Array.prototype.slice.call(arguments, 0)); }, /** * GreetService.GreetPerson @@ -22,7 +22,7 @@ window.go.main = { * @param person {main.Person} * @returns {Promise} **/ - GreetPerson: function(person) { wails.Call({"wails-method-id":4021313248, args: Array.prototype.slice.call(arguments, 0)}); }, + GreetPerson: function(person) { wails.CallByID(4021313248, ...Array.prototype.slice.call(arguments, 0)); }, }, }; diff --git a/v3/examples/binding/go.sum b/v3/examples/binding/go.sum index bee3906b1..4b65f44fe 100644 --- a/v3/examples/binding/go.sum +++ b/v3/examples/binding/go.sum @@ -58,8 +58,7 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-task/task/v3 v3.27.1 h1:cftsoOqUo7/pCdtO7fDa4HreXKDvbrRhfhhha8bH9xc= -github.com/go-task/task/v3 v3.27.1/go.mod h1:SJBNIm6TFMCcFAMohmcqbJ0o9slGoZmzcydspFX5BLk= +github.com/go-task/task/v3 v3.29.1 h1:q4mqGSR40qTOf9XZp2ySY3cM6enb2d+AqaxI/pEBiLk= github.com/go-task/task/v3 v3.29.1/go.mod h1:7AYcvV29++Yp64pejTjvnJgz/MjNMYdcPuUJgawDoyI= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= @@ -243,15 +242,13 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= -golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/v3/examples/binding/main.go b/v3/examples/binding/main.go index f1d7e4668..872e6fbdc 100644 --- a/v3/examples/binding/main.go +++ b/v3/examples/binding/main.go @@ -13,9 +13,13 @@ var assets embed.FS func main() { app := application.New(application.Options{ - Bind: []interface{}{ + Bind: []any{ &GreetService{}, }, + BindAliases: map[uint32]uint32{ + 1: 1411160069, + 2: 4021313248, + }, Assets: application.AssetOptions{ FS: assets, }, diff --git a/v3/internal/parser/bindings.go b/v3/internal/parser/bindings.go index b98fe263d..53155d2d8 100644 --- a/v3/internal/parser/bindings.go +++ b/v3/internal/parser/bindings.go @@ -22,7 +22,7 @@ const bindingTemplate = ` * @param name {string} * @returns {Promise} **/ - {{methodName}}: function({{inputs}}) { wails.Call({"wails-method-id":{{ID}}, args: Array.prototype.slice.call(arguments, 0)}); }, + {{methodName}}: function({{inputs}}) { wails.CallByID({{ID}}, ...Array.prototype.slice.call(arguments, 0)); }, ` var reservedWords = []string{ diff --git a/v3/internal/parser/parser.go b/v3/internal/parser/parser.go index 64588dc29..0cab18ab4 100644 --- a/v3/internal/parser/parser.go +++ b/v3/internal/parser/parser.go @@ -329,8 +329,16 @@ func (p *Project) findApplicationNewCalls(pkgs map[string]*ParsedPackage) (err e } // Check array type is of type "interface{}" - if _, ok := arrayType.Elt.(*ast.InterfaceType); !ok { - continue + _, isInterfaceType := arrayType.Elt.(*ast.InterfaceType) + if !isInterfaceType { + // Check it's an "any" type + ident, isAnyType := arrayType.Elt.(*ast.Ident) + if !isAnyType { + continue + } + if ident.Name != "any" { + continue + } } callFound = true // Iterate through the slice elements diff --git a/v3/pkg/application/application.go b/v3/pkg/application/application.go index 8d3633f95..bb1df4615 100644 --- a/v3/pkg/application/application.go +++ b/v3/pkg/application/application.go @@ -86,7 +86,7 @@ func New(appOptions Options) *App { result.assets.LogDetails() - result.bindings, err = NewBindings(appOptions.Bind) + result.bindings, err = NewBindings(appOptions.Bind, appOptions.BindAliases) if err != nil { println("Fatal error in application initialisation: ", err.Error()) os.Exit(1) diff --git a/v3/pkg/application/bindings.go b/v3/pkg/application/bindings.go index ac7ac1931..1e81ccb86 100644 --- a/v3/pkg/application/bindings.go +++ b/v3/pkg/application/bindings.go @@ -74,16 +74,18 @@ type BoundMethod struct { } type Bindings struct { - boundMethods map[string]map[string]map[string]*BoundMethod - boundByID map[uint32]*BoundMethod + boundMethods map[string]map[string]map[string]*BoundMethod + boundByID map[uint32]*BoundMethod + methodAliases map[uint32]uint32 } -func NewBindings(bindings []any) (*Bindings, error) { +func NewBindings(structs []any, aliases map[uint32]uint32) (*Bindings, error) { b := &Bindings{ - boundMethods: make(map[string]map[string]map[string]*BoundMethod), - boundByID: make(map[uint32]*BoundMethod), + boundMethods: make(map[string]map[string]map[string]*BoundMethod), + boundByID: make(map[uint32]*BoundMethod), + methodAliases: aliases, } - for _, binding := range bindings { + for _, binding := range structs { err := b.Add(binding) if err != nil { return nil, err @@ -174,6 +176,12 @@ func (b *Bindings) Get(options *CallOptions) *BoundMethod { // GetByID returns the bound method with the given ID func (b *Bindings) GetByID(id uint32) *BoundMethod { + // Check method aliases + if b.methodAliases != nil { + if alias, ok := b.methodAliases[id]; ok { + id = alias + } + } result := b.boundByID[id] return result } @@ -249,7 +257,14 @@ func (b *Bindings) getMethods(value interface{}, isPlugin bool) ([]*BoundMethod, } if !isPlugin { - globalApplication.Logger.Info("Adding method", "name", boundMethod, "id", boundMethod.ID) + args := []any{"name", boundMethod, "id", boundMethod.ID} + if b.methodAliases != nil { + alias, found := lo.FindKey(b.methodAliases, boundMethod.ID) + if found { + args = append(args, "alias", alias) + } + } + globalApplication.info("Adding method:", args...) } // Iterate inputs methodType := method.Type() @@ -280,7 +295,27 @@ func (b *Bindings) getMethods(value interface{}, isPlugin bool) ([]*BoundMethod, } // Call will attempt to call this bound method with the given args -func (b *BoundMethod) Call(args []interface{}) (interface{}, error) { +func (b *BoundMethod) Call(args []interface{}) (returnValue interface{}, err error) { + + // Use a defer statement to capture panics + defer func() { + if r := recover(); r != nil { + if str, ok := r.(string); ok { + if strings.HasPrefix(str, "reflect: Call using") { + // Remove prefix + str = strings.Replace(str, "reflect: Call using ", "", 1) + // Split on "as" + parts := strings.Split(str, " as type ") + if len(parts) == 2 { + err = fmt.Errorf("invalid argument type: got '%s', expected '%s'", parts[0], parts[1]) + return + } + } + } + err = fmt.Errorf("%v", r) + } + }() + // Check inputs expectedInputLength := len(b.Inputs) actualInputLength := len(args) @@ -315,9 +350,6 @@ func (b *BoundMethod) Call(args []interface{}) (interface{}, error) { callResults := b.Method.Call(callArgs) //** Check results **// - var returnValue interface{} - var err error - switch len(b.Outputs) { case 1: // Loop over results and determine if the result diff --git a/v3/pkg/application/options_application.go b/v3/pkg/application/options_application.go index 5970caaab..38d7263a2 100644 --- a/v3/pkg/application/options_application.go +++ b/v3/pkg/application/options_application.go @@ -13,6 +13,7 @@ type Options struct { Mac MacOptions Windows WindowsApplicationOptions Bind []any + BindAliases map[uint32]uint32 Logger *slog.Logger Assets AssetOptions Plugins map[string]Plugin