mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-15 15:15:51 +01:00
* fix(v3): warm up dialog types in go-json cache to prevent Windows panic Add FileFilter, OpenFileDialogOptions, SaveFileDialogOptions, and MessageDialogOptions to the init() warmup to prevent index out of bounds panic on Windows when these types are first unmarshaled. Fixes goccy/go-json#474 for Wails internal dialog types. * fix(v3): revert goccy/go-json to stdlib encoding/json to fix Windows panic goccy/go-json has a type address calculation bug on Windows that causes index out of bounds panic when decoding user-defined types for the first time. This reverts all runtime usages of goccy/go-json back to stdlib encoding/json. Test and benchmark files are left unchanged. Partially reverts PR #4843.
427 lines
12 KiB
Go
427 lines
12 KiB
Go
package application
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/wailsapp/wails/v3/internal/hash"
|
|
"github.com/wailsapp/wails/v3/internal/sliceutil"
|
|
)
|
|
|
|
// CallOptions defines the options for a method call.
|
|
// Field order is optimized to minimize struct padding.
|
|
type CallOptions struct {
|
|
MethodName string `json:"methodName"`
|
|
Args []json.RawMessage `json:"args"`
|
|
MethodID uint32 `json:"methodID"`
|
|
}
|
|
|
|
type ErrorKind string
|
|
|
|
const (
|
|
ReferenceError ErrorKind = "ReferenceError"
|
|
TypeError ErrorKind = "TypeError"
|
|
RuntimeError ErrorKind = "RuntimeError"
|
|
)
|
|
|
|
// CallError represents an error that occurred during a method call.
|
|
// Field order is optimized to minimize struct padding.
|
|
type CallError struct {
|
|
Message string `json:"message"`
|
|
Cause any `json:"cause,omitempty"`
|
|
Kind ErrorKind `json:"kind"`
|
|
}
|
|
|
|
func (e *CallError) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// 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.
|
|
// Field order is optimized to minimize struct padding (136 bytes vs 144 bytes).
|
|
type BoundMethod struct {
|
|
Method reflect.Value `json:"-"`
|
|
Name string `json:"name"`
|
|
FQN string `json:"-"`
|
|
Comments string `json:"comments,omitempty"`
|
|
Inputs []*Parameter `json:"inputs,omitempty"`
|
|
Outputs []*Parameter `json:"outputs,omitempty"`
|
|
marshalError func(error) []byte
|
|
ID uint32 `json:"id"`
|
|
needsContext bool
|
|
isVariadic bool // cached at registration to avoid reflect call per invocation
|
|
}
|
|
|
|
type Bindings struct {
|
|
marshalError func(error) []byte
|
|
boundMethods map[string]*BoundMethod
|
|
boundByID map[uint32]*BoundMethod
|
|
methodAliases map[uint32]uint32
|
|
}
|
|
|
|
func NewBindings(marshalError func(error) []byte, aliases map[uint32]uint32) *Bindings {
|
|
return &Bindings{
|
|
marshalError: wrapErrorMarshaler(marshalError, defaultMarshalError),
|
|
boundMethods: make(map[string]*BoundMethod),
|
|
boundByID: make(map[uint32]*BoundMethod),
|
|
methodAliases: aliases,
|
|
}
|
|
}
|
|
|
|
// Add adds the given service to the bindings.
|
|
func (b *Bindings) Add(service Service) error {
|
|
methods, err := getMethods(service.Instance())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
marshalError := wrapErrorMarshaler(service.options.MarshalError, defaultMarshalError)
|
|
|
|
// Validate and log methods.
|
|
for _, method := range methods {
|
|
if _, ok := b.boundMethods[method.FQN]; ok {
|
|
return fmt.Errorf("bound method '%s' is already registered. Please note that you can register at most one service of each type; additional instances must be wrapped in dedicated structs", method.FQN)
|
|
}
|
|
if boundMethod, ok := b.boundByID[method.ID]; ok {
|
|
return 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 use this method, please rename it. Sorry :(", method.FQN, boundMethod.FQN)
|
|
}
|
|
|
|
// Log
|
|
attrs := []any{"fqn", method.FQN, "id", method.ID}
|
|
if alias, ok := sliceutil.FindMapKey(b.methodAliases, method.ID); ok {
|
|
attrs = append(attrs, "alias", alias)
|
|
}
|
|
globalApplication.debug("Registering bound method:", attrs...)
|
|
}
|
|
|
|
for _, method := range methods {
|
|
// Store composite error marshaler
|
|
method.marshalError = marshalError
|
|
|
|
// Register method
|
|
b.boundMethods[method.FQN] = method
|
|
b.boundByID[method.ID] = method
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Get returns the bound method with the given name
|
|
func (b *Bindings) Get(options *CallOptions) *BoundMethod {
|
|
return b.boundMethods[options.MethodName]
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
return b.boundByID[id]
|
|
}
|
|
|
|
// internalServiceMethod is a set of methods
|
|
// that are handled specially by the binding engine
|
|
// and must not be exposed to the frontend.
|
|
//
|
|
// For simplicity we exclude these by name
|
|
// without checking their signatures,
|
|
// and so does the binding generator.
|
|
var internalServiceMethods = map[string]bool{
|
|
"ServiceName": true,
|
|
"ServiceStartup": true,
|
|
"ServiceShutdown": true,
|
|
"ServeHTTP": true,
|
|
}
|
|
|
|
var ctxType = reflect.TypeFor[context.Context]()
|
|
|
|
// getMethods returns the list of BoundMethod descriptors for the methods of the named pointer type provided by value.
|
|
//
|
|
// It returns an error if value is not a pointer to a named type, if a function value is supplied (binding functions is deprecated), or if a generic type is supplied.
|
|
// The returned BoundMethod slice includes only exported methods that are not listed in internalServiceMethods. Each BoundMethod has its FQN, ID (computed from the FQN), Method reflect.Value, Inputs and Outputs populated, isVariadic cached from the method signature, and needsContext set when the first parameter is context.Context.
|
|
func getMethods(value any) ([]*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())
|
|
}
|
|
|
|
// Process Methods
|
|
for i := range ptrType.NumMethod() {
|
|
methodName := ptrType.Method(i).Name
|
|
method := namedValue.Method(i)
|
|
|
|
if internalServiceMethods[methodName] {
|
|
continue
|
|
}
|
|
|
|
fqn := fmt.Sprintf("%s.%s.%s", packagePath, typeName, methodName)
|
|
|
|
// Iterate inputs
|
|
methodType := method.Type()
|
|
|
|
// Create new method with cached flags
|
|
boundMethod := &BoundMethod{
|
|
ID: hash.Fnv(fqn),
|
|
FQN: fqn,
|
|
Name: methodName,
|
|
Inputs: nil,
|
|
Outputs: nil,
|
|
Comments: "",
|
|
Method: method,
|
|
isVariadic: methodType.IsVariadic(), // cache to avoid reflect call per invocation
|
|
}
|
|
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
|
|
}
|
|
|
|
func (b *BoundMethod) String() string {
|
|
return b.FQN
|
|
}
|
|
|
|
var errorType = reflect.TypeFor[error]()
|
|
|
|
// Call will attempt to call this bound method with the given args.
|
|
// If the call succeeds, result will be either a non-error return value (if there is only one)
|
|
// or a slice of non-error return values (if there are more than one).
|
|
//
|
|
// If the arguments are mistyped or the call returns one or more non-nil error values,
|
|
// result is nil and err is an instance of *[CallError].
|
|
func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (result any, err error) {
|
|
// Use a defer statement to capture panics
|
|
defer handlePanic(handlePanicOptions{skipEnd: 5})
|
|
argCount := len(args)
|
|
if b.needsContext {
|
|
argCount++
|
|
}
|
|
|
|
if argCount != len(b.Inputs) {
|
|
err = &CallError{
|
|
Message: fmt.Sprintf("%s expects %d arguments, got %d", b.FQN, len(b.Inputs), argCount),
|
|
Kind: TypeError,
|
|
}
|
|
return
|
|
}
|
|
|
|
// Use stack-allocated buffer for common case (<=8 args), heap for larger
|
|
var argBuffer [8]reflect.Value
|
|
var callArgs []reflect.Value
|
|
if argCount <= len(argBuffer) {
|
|
callArgs = argBuffer[:argCount]
|
|
} else {
|
|
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 = &CallError{
|
|
Message: fmt.Sprintf("could not parse argument #%d: %s", index, err),
|
|
Cause: json.RawMessage(b.marshalError(err)),
|
|
Kind: TypeError,
|
|
}
|
|
return
|
|
}
|
|
callArgs[base+index] = value.Elem()
|
|
}
|
|
|
|
// Do the call using cached isVariadic flag
|
|
var callResults []reflect.Value
|
|
if b.isVariadic {
|
|
callResults = b.Method.CallSlice(callArgs)
|
|
} else {
|
|
callResults = b.Method.Call(callArgs)
|
|
}
|
|
|
|
// Process results - optimized for common case of 0-2 return values
|
|
// to avoid slice allocation
|
|
var firstResult any
|
|
var hasFirstResult bool
|
|
var nonErrorOutputs []any // only allocated if >1 non-error results
|
|
var errorOutputs []error
|
|
|
|
for _, field := range callResults {
|
|
if field.Type() == errorType {
|
|
if field.IsNil() {
|
|
continue
|
|
}
|
|
if errorOutputs == nil {
|
|
errorOutputs = make([]error, 0, len(callResults))
|
|
}
|
|
errorOutputs = append(errorOutputs, field.Interface().(error))
|
|
} else if errorOutputs == nil {
|
|
// Only collect non-error outputs if no errors yet
|
|
val := field.Interface()
|
|
if !hasFirstResult {
|
|
firstResult = val
|
|
hasFirstResult = true
|
|
} else if nonErrorOutputs == nil {
|
|
// Second result - need to allocate slice
|
|
nonErrorOutputs = make([]any, 0, len(callResults))
|
|
nonErrorOutputs = append(nonErrorOutputs, firstResult, val)
|
|
} else {
|
|
nonErrorOutputs = append(nonErrorOutputs, val)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(errorOutputs) > 0 {
|
|
info := make([]json.RawMessage, len(errorOutputs))
|
|
for i, err := range errorOutputs {
|
|
info[i] = b.marshalError(err)
|
|
}
|
|
|
|
cerr := &CallError{
|
|
Message: errors.Join(errorOutputs...).Error(),
|
|
Cause: info,
|
|
Kind: RuntimeError,
|
|
}
|
|
if len(info) == 1 {
|
|
cerr.Cause = info[0]
|
|
}
|
|
|
|
err = cerr
|
|
} else if nonErrorOutputs != nil {
|
|
result = nonErrorOutputs
|
|
} else if hasFirstResult {
|
|
result = firstResult
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// wrapErrorMarshaler returns an error marshaling functions
|
|
// that calls the primary marshaler first,
|
|
// then falls back to the secondary one.
|
|
func wrapErrorMarshaler(primary func(error) []byte, secondary func(error) []byte) func(error) []byte {
|
|
if primary == nil {
|
|
return secondary
|
|
}
|
|
|
|
return func(err error) []byte {
|
|
result := primary(err)
|
|
if result == nil {
|
|
result = secondary(err)
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|
|
|
|
// defaultMarshalError implements the default error marshaling mechanism.
|
|
func defaultMarshalError(err error) []byte {
|
|
result, jsonErr := json.Marshal(&err)
|
|
if jsonErr != nil {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
|
|
// 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() != ""
|
|
}
|