Files

405 lines
11 KiB
Go
Raw Permalink Normal View History

2023-02-26 15:06:05 +11:00
package application
import (
"context"
2023-02-26 15:06:05 +11:00
"fmt"
"reflect"
"runtime"
"strings"
2023-03-24 08:43:00 +11:00
2023-09-28 11:38:59 -05:00
"github.com/wailsapp/wails/v3/internal/hash"
2023-03-24 08:43:00 +11:00
"github.com/samber/lo"
2023-02-26 15:06:05 +11:00
)
2023-02-26 20:49:29 +11:00
type CallOptions struct {
2023-08-27 16:52:07 +10:00
MethodID uint32 `json:"methodID"`
2023-02-26 20:49:29 +11:00
PackageName string `json:"packageName"`
StructName string `json:"structName"`
MethodName string `json:"methodName"`
Args []any `json:"args"`
}
2023-08-18 09:35:03 +10:00
func (c CallOptions) Name() string {
return fmt.Sprintf("%s.%s.%s", c.PackageName, c.StructName, c.MethodName)
}
2023-03-18 08:11:36 +11:00
type PluginCallOptions struct {
Name string `json:"name"`
Args []any `json:"args"`
}
2023-03-18 09:12:44 +11:00
var reservedPluginMethods = []string{
"Name",
"Init",
"Shutdown",
2023-03-21 08:55:55 +11:00
"Exported",
2023-03-18 09:12:44 +11:00
}
2023-02-26 15:06:05 +11:00
// Parameter defines a Go method parameter
type Parameter struct {
Name string `json:"name,omitempty"`
TypeName string `json:"type"`
2023-03-20 20:28:33 +11:00
ReflectType reflect.Type
2023-02-26 15:06:05 +11:00
}
func newParameter(Name string, Type reflect.Type) *Parameter {
return &Parameter{
Name: Name,
TypeName: Type.String(),
2023-03-20 20:28:33 +11:00
ReflectType: Type,
2023-02-26 15:06:05 +11:00
}
}
// IsType returns true if the given
func (p *Parameter) IsType(typename string) bool {
return p.TypeName == typename
}
// IsError returns true if the parameter type is an error
func (p *Parameter) IsError() bool {
return p.IsType("error")
}
// BoundMethod defines all the data related to a Go method that is
// bound to the Wails application
type BoundMethod struct {
ID uint32 `json:"id"`
2023-02-26 15:06:05 +11:00
Name string `json:"name"`
Inputs []*Parameter `json:"inputs,omitempty"`
Outputs []*Parameter `json:"outputs,omitempty"`
Comments string `json:"comments,omitempty"`
Method reflect.Value `json:"-"`
PackageName string
StructName string
PackagePath string
needsContext bool
2023-02-26 15:06:05 +11:00
}
type Bindings struct {
boundMethods map[string]map[string]map[string]*BoundMethod
boundByID map[uint32]*BoundMethod
methodAliases map[uint32]uint32
2023-02-26 15:06:05 +11:00
}
func NewBindings(structs []any, aliases map[uint32]uint32) (*Bindings, error) {
2023-02-26 15:06:05 +11:00
b := &Bindings{
boundMethods: make(map[string]map[string]map[string]*BoundMethod),
boundByID: make(map[uint32]*BoundMethod),
methodAliases: aliases,
2023-02-26 15:06:05 +11:00
}
for _, binding := range structs {
2023-02-26 15:06:05 +11:00
err := b.Add(binding)
if err != nil {
return nil, err
}
}
return b, nil
}
// Add the given struct methods to the Bindings
func (b *Bindings) Add(structPtr interface{}) error {
2023-08-19 16:56:41 +10:00
methods, err := b.getMethods(structPtr, false)
2023-02-26 15:06:05 +11:00
if err != nil {
return fmt.Errorf("cannot bind value to app: %s", err.Error())
}
for _, method := range methods {
packageName := method.PackageName
structName := method.StructName
methodName := method.Name
// Add it as a regular method
if _, ok := b.boundMethods[packageName]; !ok {
b.boundMethods[packageName] = make(map[string]map[string]*BoundMethod)
}
if _, ok := b.boundMethods[packageName][structName]; !ok {
b.boundMethods[packageName][structName] = make(map[string]*BoundMethod)
}
b.boundMethods[packageName][structName][methodName] = method
b.boundByID[method.ID] = method
2023-03-18 09:12:44 +11:00
}
return nil
}
func (b *Bindings) AddPlugins(plugins map[string]Plugin) error {
for pluginID, plugin := range plugins {
2023-08-19 16:56:41 +10:00
methods, err := b.getMethods(plugin, true)
2023-03-18 09:12:44 +11:00
if err != nil {
return fmt.Errorf("cannot add plugin '%s' to app: %s", pluginID, err.Error())
}
2023-03-22 20:42:20 +11:00
exportedMethods := plugin.CallableByJS()
2023-03-21 08:55:55 +11:00
2023-03-18 09:12:44 +11:00
for _, method := range methods {
2023-03-21 08:55:55 +11:00
// Do not expose reserved methods
2023-03-18 09:12:44 +11:00
if lo.Contains(reservedPluginMethods, method.Name) {
continue
}
2023-03-21 08:55:55 +11:00
// Do not expose methods that are not in the exported list
if !lo.Contains(exportedMethods, method.Name) {
continue
}
2023-03-18 09:12:44 +11:00
packageName := "wails-plugins"
structName := pluginID
methodName := method.Name
// Add it as a regular method
if _, ok := b.boundMethods[packageName]; !ok {
b.boundMethods[packageName] = make(map[string]map[string]*BoundMethod)
}
if _, ok := b.boundMethods[packageName][structName]; !ok {
b.boundMethods[packageName][structName] = make(map[string]*BoundMethod)
}
b.boundMethods[packageName][structName][methodName] = method
2023-08-27 16:52:07 +10:00
b.boundByID[method.ID] = method
2023-10-15 13:53:31 +11:00
globalApplication.debug("Added plugin method: "+structName+"."+methodName, "id", method.ID)
2023-03-18 09:12:44 +11:00
}
2023-02-26 15:06:05 +11:00
}
return nil
}
// Get returns the bound method with the given name
2023-02-26 20:49:29 +11:00
func (b *Bindings) Get(options *CallOptions) *BoundMethod {
_, ok := b.boundMethods[options.PackageName]
if !ok {
return nil
}
_, ok = b.boundMethods[options.PackageName][options.StructName]
if !ok {
return nil
}
method, ok := b.boundMethods[options.PackageName][options.StructName][options.MethodName]
if !ok {
return nil
}
return method
}
// 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
}
// GenerateID generates a unique ID for a binding
func (b *Bindings) GenerateID(name string) (uint32, error) {
id, err := hash.Fnv(name)
if err != nil {
return 0, err
}
// Check if we already have it
boundMethod, ok := b.boundByID[id]
if ok {
return 0, fmt.Errorf("oh wow, we're sorry about this! Amazingly, a hash collision was detected for method '%s' (it generates the same hash as '%s'). To continue, please rename it. Sorry :(", name, boundMethod.String())
}
return id, nil
}
func (b *BoundMethod) String() string {
return fmt.Sprintf("%s.%s.%s", b.PackageName, b.StructName, b.Name)
}
2023-08-19 16:56:41 +10:00
func (b *Bindings) getMethods(value interface{}, isPlugin bool) ([]*BoundMethod, error) {
2023-02-26 15:06:05 +11:00
// Create result placeholder
var result []*BoundMethod
// Check type
if !isStructPtr(value) {
if isStruct(value) {
name := reflect.ValueOf(value).Type().Name()
return nil, fmt.Errorf("%s is a struct, not a pointer to a struct", name)
}
if isFunction(value) {
name := runtime.FuncForPC(reflect.ValueOf(value).Pointer()).Name()
2023-03-24 08:43:00 +11:00
return nil, fmt.Errorf("%s is a function, not a pointer to a struct. Wails v2 has deprecated the binding of functions. Please wrap your functions up in a struct and bind a pointer to that struct", name)
2023-02-26 15:06:05 +11:00
}
2023-02-26 20:49:29 +11:00
return nil, fmt.Errorf("not a pointer to a struct")
2023-02-26 15:06:05 +11:00
}
// Process Struct
structType := reflect.TypeOf(value)
structValue := reflect.ValueOf(value)
structTypeString := structType.String()
baseName := structTypeString[1:]
ctxType := reflect.TypeOf((*context.Context)(nil)).Elem()
2023-02-26 15:06:05 +11:00
// Process Methods
for i := 0; i < structType.NumMethod(); i++ {
methodDef := structType.Method(i)
methodName := methodDef.Name
packageName, structName, _ := strings.Cut(baseName, ".")
method := structValue.MethodByName(methodName)
2023-02-26 20:49:29 +11:00
packagePath, _ := lo.Coalesce(structType.PkgPath(), "main")
2023-02-26 15:06:05 +11:00
// Create new method
boundMethod := &BoundMethod{
2023-02-26 20:49:29 +11:00
Name: methodName,
2023-02-26 15:06:05 +11:00
PackageName: packageName,
2023-02-26 20:49:29 +11:00
PackagePath: packagePath,
2023-02-26 15:06:05 +11:00
StructName: structName,
Inputs: nil,
Outputs: nil,
Comments: "",
Method: method,
}
var err error
boundMethod.ID, err = hash.Fnv(boundMethod.String())
if err != nil {
return nil, err
}
2023-02-26 15:06:05 +11:00
2023-08-19 16:56:41 +10:00
if !isPlugin {
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)
}
}
2023-10-15 13:53:31 +11:00
globalApplication.debug("Adding method:", args...)
2023-08-19 16:56:41 +10:00
}
2023-02-26 15:06:05 +11:00
// Iterate inputs
methodType := method.Type()
inputParamCount := methodType.NumIn()
var inputs []*Parameter
for inputIndex := 0; inputIndex < inputParamCount; inputIndex++ {
input := methodType.In(inputIndex)
if inputIndex == 0 && input.AssignableTo(ctxType) {
boundMethod.needsContext = true
}
2023-02-26 15:06:05 +11:00
thisParam := newParameter("", input)
inputs = append(inputs, thisParam)
}
boundMethod.Inputs = inputs
outputParamCount := methodType.NumOut()
var outputs []*Parameter
for outputIndex := 0; outputIndex < outputParamCount; outputIndex++ {
output := methodType.Out(outputIndex)
thisParam := newParameter("", output)
outputs = append(outputs, thisParam)
}
boundMethod.Outputs = outputs
// Save method in result
result = append(result, boundMethod)
}
return result, nil
}
2023-02-26 20:49:29 +11:00
// Call will attempt to call this bound method with the given args
func (b *BoundMethod) Call(ctx context.Context, 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)
}
}()
if b.needsContext {
args = append([]any{ctx}, args...)
}
2023-02-26 20:49:29 +11:00
// Check inputs
expectedInputLength := len(b.Inputs)
actualInputLength := len(args)
2023-03-22 20:36:46 +11:00
// If the method is variadic, we need to check the minimum number of inputs
if b.Method.Type().IsVariadic() {
if actualInputLength < expectedInputLength-1 {
return nil, fmt.Errorf("%s takes at least %d inputs. Received %d", b.Name, expectedInputLength, actualInputLength)
}
} else {
if expectedInputLength != actualInputLength {
return nil, fmt.Errorf("%s takes %d inputs. Received %d", b.Name, expectedInputLength, actualInputLength)
}
2023-02-26 20:49:29 +11:00
}
/** Convert inputs to reflect values **/
// Create slice for the input arguments to the method call
2023-03-22 20:36:46 +11:00
callArgs := make([]reflect.Value, actualInputLength)
2023-02-26 20:49:29 +11:00
// Iterate over given arguments
for index, arg := range args {
// Save the converted argument
2023-03-20 20:28:33 +11:00
if arg == nil {
callArgs[index] = reflect.Zero(b.Inputs[index].ReflectType)
continue
}
2023-02-26 20:49:29 +11:00
callArgs[index] = reflect.ValueOf(arg)
}
// Do the call
callResults := b.Method.Call(callArgs)
//** Check results **//
switch len(b.Outputs) {
case 1:
// Loop over results and determine if the result
// is an error or not
for _, result := range callResults {
interfac := result.Interface()
temp, ok := interfac.(error)
if ok {
err = temp
} else {
returnValue = interfac
}
}
case 2:
returnValue = callResults[0].Interface()
if temp, ok := callResults[1].Interface().(error); ok {
err = temp
}
}
return returnValue, err
}
2023-02-26 15:06:05 +11:00
// isStructPtr returns true if the value given is a
// pointer to a struct
func isStructPtr(value interface{}) bool {
return reflect.ValueOf(value).Kind() == reflect.Ptr &&
reflect.ValueOf(value).Elem().Kind() == reflect.Struct
}
// isFunction returns true if the given value is a function
func isFunction(value interface{}) bool {
return reflect.ValueOf(value).Kind() == reflect.Func
}
// isStructPtr returns true if the value given is a struct
func isStruct(value interface{}) bool {
return reflect.ValueOf(value).Kind() == reflect.Struct
}