mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-04 23:59:52 +08:00

* Support variadic arguments and slice, pointer types * Fix computation of type namespaces * Improve comments and general formatting * Set default values correctly for composite types * Add templates for bindings Additionally: * fixes generation of tuple return type * improves imports and namespacing in JS mode * general cleanup of generated code * Simplify import list construction * Refactor type generation code Improves support for unknown types (encoded as any) and maps (using Typescript index signatures) * Support slices with pointer elements * Match encoding/json behaviour in struct parser * Update tests and example * Add tests for complex method signatures and json tag parsing * Add test `function_multiple_files` * Attempt looking up idents with missing denotation * Update test data * fix quoted bool field * Test quoted booleans * Delete old parser code * Remove old test data * Update bindgen flags * Makes call by ID the default * Add package loading code * Add static analyser * Temporarily ignore binding generation code * Add complex slice expressions test * Fix variable reference analysis * Unwrap casts to interface types * Complete code comments * Refactor static analyser * Restrict options struct usage * Update tests * Fix method selector sink and source processing * Improve Set API * Add package info collector * Rename analyser package to analyse * Improve template functions * Add index file templates * Add glue code for binding generation * Refactor collection and rendering code * Implement binding generator * Implement global index generation * Improve marshaler and alias handling * Use package path in binding calls by name * Implement model collection and rendering * Fix wrong exit condition in analyser * Fix enum rendering * Generate shortcuts for all packages. * Implement generator tests * Ignore non-pointer bound types * Treat main package specially * Compute stats * Plug new API into generate command * Support all named types * Update JS runtime * Report dual role types * Remove go1.22 syntax * Fix type assertion in TS bindings * encoding/json compliance for arrays and slices * Ignore got files in testdata * Cleanup type rendering mechanism * Update JS runtime * Implement generic models * Add missing field in renderer initialisation * Improve generic creation code * Add generic model test * Add error reporting infrastructure * Support configurable file names * Detect file naming collisions * Print final error report * New shortcut file structure + collision detection * Update test layout and data * Autoconfiguration for analyser tests * Live progress reporting * Update code comments * Fix model doc rendering * Simplify name resolution * Add test for out of tree types * Fix generic creation code * Fix potential collisions between methods and models * Fix generic class alias rendering * Report model discovery in debug mode * Add interface mode for JS * Collect interface method comments * Add interface methods test * Unwrap generic instantiations in method receivers * Fix rendering of nullable types in interface mode * Fix rendering of class aliases * Expose promise cancel method to typescript * Update test data * Update binding example * Fix rendering of aliased quoted type params * Move to strongly typed bindings * Implement lightweight analyser * Update test cases * Update binding example * Add complex instantiation test * Load full dependency tree * Rewrite collector * Update renderer to match new collector * Update generator to match new collector * Update test data * Update binding example * Configure includes and injections by language * Improve system path resolution * Support rich conditions in inject/include directives * Fix error handling in Generator.Generate * Retrieve compiled go file paths from fileset * Do not rely on struct info in struct flattening algorithm * Fix doc comment for findDeclaraion * Fix bugs in embedded field handling * Fix bugs and comments in package collection * Remove useless fields from ServiceInfo * Fix empty line at the beginning of TS indexes * Remove global index and shortcuts * Remove generation tests for individual packages * Enforce lower-case file names * Update test data * Improve error reporting * Update binding example * Reintroduce go1.22 syntax * Improve relative import path computation * Improve alias support * Add alias test * Update test data * Remove no services error * Rename global analyser test * Add workaround and test for bug in typeutil.Map * Update test data * Do not split fully qualified names * Update typeutil package and remove workaround * Unify alias/named type handling * Fix rendering of generic named class aliases * Fix rendering of array types * Minor tweaks and cleanups * Rmove namespaced export construct * Update test data * Update binding example * Break type cycles * Fix typo in comment * Fix creation code for cyclic types * Fix type of variadic params in interface mode * Update test data * Fix bad whitespace * Refactor type assertions inside bound methods * Update test data * Rename field application.Options.Bind to Services * Rename parser package to generator * Update binding example * Update test data * Update generator readme * Add typescript test harness * Move test output to new subfolder * Fix code generation bugs * Use .js extensions in TS mode imports * Update test data * Revert default generator output dir to frontend/bindings * Bump runtime package version * Update templates * Update changelog * Improve newline handling --------- Co-authored-by: Andreas Bichinger <andreas.bichinger@gmail.com>
381 lines
9.9 KiB
Go
381 lines
9.9 KiB
Go
package application
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/wailsapp/wails/v3/internal/hash"
|
|
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
type CallOptions struct {
|
|
MethodID uint32 `json:"methodID"`
|
|
MethodName string `json:"methodName"`
|
|
Args []json.RawMessage `json:"args"`
|
|
}
|
|
|
|
type PluginCallOptions struct {
|
|
Name string `json:"name"`
|
|
Args []json.RawMessage `json:"args"`
|
|
}
|
|
|
|
var reservedPluginMethods = []string{
|
|
"Name",
|
|
"Init",
|
|
"Shutdown",
|
|
"Exported",
|
|
}
|
|
|
|
// Parameter defines a Go method parameter
|
|
type Parameter struct {
|
|
Name string `json:"name,omitempty"`
|
|
TypeName string `json:"type"`
|
|
ReflectType reflect.Type
|
|
}
|
|
|
|
func newParameter(Name string, Type reflect.Type) *Parameter {
|
|
return &Parameter{
|
|
Name: Name,
|
|
TypeName: Type.String(),
|
|
ReflectType: Type,
|
|
}
|
|
}
|
|
|
|
// 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"`
|
|
Name string `json:"name"`
|
|
Inputs []*Parameter `json:"inputs,omitempty"`
|
|
Outputs []*Parameter `json:"outputs,omitempty"`
|
|
Comments string `json:"comments,omitempty"`
|
|
Method reflect.Value `json:"-"`
|
|
TypeName string
|
|
PackagePath string
|
|
|
|
needsContext bool
|
|
}
|
|
|
|
type Bindings struct {
|
|
boundMethods map[string]*BoundMethod
|
|
boundByID map[uint32]*BoundMethod
|
|
methodAliases map[uint32]uint32
|
|
}
|
|
|
|
func NewBindings(instances []Service, aliases map[uint32]uint32) (*Bindings, error) {
|
|
b := &Bindings{
|
|
boundMethods: make(map[string]*BoundMethod),
|
|
boundByID: make(map[uint32]*BoundMethod),
|
|
methodAliases: aliases,
|
|
}
|
|
for _, binding := range instances {
|
|
err := b.Add(binding.Instance())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// Add the given named type pointer methods to the Bindings
|
|
func (b *Bindings) Add(namedPtr interface{}) error {
|
|
methods, err := b.getMethods(namedPtr, false)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot bind value to app: %s", err.Error())
|
|
}
|
|
|
|
for _, method := range methods {
|
|
// Add it as a regular method
|
|
b.boundMethods[method.String()] = method
|
|
b.boundByID[method.ID] = method
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *Bindings) AddPlugins(plugins map[string]Plugin) error {
|
|
for pluginID, plugin := range plugins {
|
|
methods, err := b.getMethods(plugin, true)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot add plugin '%s' to app: %s", pluginID, err.Error())
|
|
}
|
|
|
|
exportedMethods := plugin.CallableByJS()
|
|
|
|
for _, method := range methods {
|
|
// Do not expose reserved methods
|
|
if lo.Contains(reservedPluginMethods, method.Name) {
|
|
continue
|
|
}
|
|
// Do not expose methods that are not in the exported list
|
|
if !lo.Contains(exportedMethods, method.Name) {
|
|
continue
|
|
}
|
|
|
|
// Add it as a regular method
|
|
b.boundMethods[fmt.Sprintf("wails-plugins.%s.%s", pluginID, method.Name)] = method
|
|
b.boundByID[method.ID] = method
|
|
globalApplication.debug("Added plugin method: "+pluginID+"."+method.Name, "id", method.ID)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Get returns the bound method with the given name
|
|
func (b *Bindings) Get(options *CallOptions) *BoundMethod {
|
|
method, ok := b.boundMethods[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.PackagePath, b.TypeName, b.Name)
|
|
}
|
|
|
|
func (b *Bindings) getMethods(value interface{}, isPlugin bool) ([]*BoundMethod, error) {
|
|
// Create result placeholder
|
|
var result []*BoundMethod
|
|
|
|
// Check type
|
|
if !isNamed(value) {
|
|
if isFunction(value) {
|
|
name := runtime.FuncForPC(reflect.ValueOf(value).Pointer()).Name()
|
|
return nil, fmt.Errorf("%s is a function, not a pointer to named type. Wails v2 has deprecated the binding of functions. Please define your functions as methods on a struct and bind a pointer to that struct", name)
|
|
}
|
|
|
|
return nil, fmt.Errorf("%s is not a pointer to named type", reflect.ValueOf(value).Type().String())
|
|
} else if !isPtr(value) {
|
|
return nil, fmt.Errorf("%s is a named type, not a pointer to named type", reflect.ValueOf(value).Type().String())
|
|
}
|
|
|
|
// Process Named Type
|
|
namedValue := reflect.ValueOf(value)
|
|
ptrType := namedValue.Type()
|
|
namedType := ptrType.Elem()
|
|
typeName := namedType.Name()
|
|
packagePath := namedType.PkgPath()
|
|
|
|
if strings.Contains(namedType.String(), "[") {
|
|
return nil, fmt.Errorf("%s.%s is a generic type. Generic bound types are not supported", packagePath, namedType.String())
|
|
}
|
|
|
|
ctxType := reflect.TypeFor[context.Context]()
|
|
|
|
// Process Methods
|
|
for i := 0; i < ptrType.NumMethod(); i++ {
|
|
methodDef := ptrType.Method(i)
|
|
methodName := methodDef.Name
|
|
method := namedValue.MethodByName(methodName)
|
|
|
|
// Create new method
|
|
boundMethod := &BoundMethod{
|
|
Name: methodName,
|
|
PackagePath: packagePath,
|
|
TypeName: typeName,
|
|
Inputs: nil,
|
|
Outputs: nil,
|
|
Comments: "",
|
|
Method: method,
|
|
}
|
|
var err error
|
|
boundMethod.ID, err = b.GenerateID(boundMethod.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
globalApplication.debug("Adding method:", args...)
|
|
}
|
|
// 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
|
|
}
|
|
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
|
|
}
|
|
|
|
var errorType = reflect.TypeFor[error]()
|
|
|
|
// Call will attempt to call this bound method with the given args
|
|
func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (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)
|
|
}
|
|
}()
|
|
|
|
argCount := len(args)
|
|
if b.needsContext {
|
|
argCount++
|
|
}
|
|
|
|
if argCount != len(b.Inputs) {
|
|
err = fmt.Errorf("%s expects %d arguments, received %d", b.Name, len(b.Inputs), argCount)
|
|
return
|
|
}
|
|
|
|
// Convert inputs to values of appropriate type
|
|
|
|
callArgs := make([]reflect.Value, argCount)
|
|
base := 0
|
|
|
|
if b.needsContext {
|
|
callArgs[0] = reflect.ValueOf(ctx)
|
|
base++
|
|
}
|
|
|
|
// Iterate over given arguments
|
|
for index, arg := range args {
|
|
value := reflect.New(b.Inputs[base+index].ReflectType)
|
|
err = json.Unmarshal(arg, value.Interface())
|
|
if err != nil {
|
|
err = fmt.Errorf("could not parse argument #%d: %w", index, err)
|
|
return
|
|
}
|
|
callArgs[base+index] = value.Elem()
|
|
}
|
|
|
|
// Do the call
|
|
var callResults []reflect.Value
|
|
if b.Method.Type().IsVariadic() {
|
|
callResults = b.Method.CallSlice(callArgs)
|
|
} else {
|
|
callResults = b.Method.Call(callArgs)
|
|
}
|
|
|
|
var nonErrorOutputs = make([]any, 0, len(callResults))
|
|
var errorOutputs []error
|
|
|
|
for _, result := range callResults {
|
|
if result.Type() == errorType {
|
|
if result.IsNil() {
|
|
continue
|
|
}
|
|
if errorOutputs == nil {
|
|
errorOutputs = make([]error, 0, len(callResults)-len(nonErrorOutputs))
|
|
nonErrorOutputs = nil
|
|
}
|
|
errorOutputs = append(errorOutputs, result.Interface().(error))
|
|
} else if nonErrorOutputs != nil {
|
|
nonErrorOutputs = append(nonErrorOutputs, result.Interface())
|
|
}
|
|
}
|
|
|
|
if errorOutputs != nil {
|
|
err = errors.Join(errorOutputs...)
|
|
} else if len(nonErrorOutputs) == 1 {
|
|
returnValue = nonErrorOutputs[0]
|
|
} else if len(nonErrorOutputs) > 1 {
|
|
returnValue = nonErrorOutputs
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// isPtr returns true if the value given is a pointer.
|
|
func isPtr(value interface{}) bool {
|
|
return reflect.ValueOf(value).Kind() == reflect.Ptr
|
|
}
|
|
|
|
// isFunction returns true if the given value is a function
|
|
func isFunction(value interface{}) bool {
|
|
return reflect.ValueOf(value).Kind() == reflect.Func
|
|
}
|
|
|
|
// isNamed returns true if the given value is of named type
|
|
// or pointer to named type.
|
|
func isNamed(value interface{}) bool {
|
|
rv := reflect.ValueOf(value)
|
|
if rv.Kind() == reflect.Ptr {
|
|
rv = rv.Elem()
|
|
}
|
|
|
|
return rv.Type().Name() != ""
|
|
}
|