2023-02-26 15:06:05 +11:00
package application
import (
2024-03-04 07:50:08 +01:00
"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 {
2023-08-19 14:25:19 +10:00
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
2024-03-04 07:50:08 +01:00
needsContext bool
2023-02-26 15:06:05 +11:00
}
type Bindings struct {
2023-08-27 20:39:35 +10:00
boundMethods map [ string ] map [ string ] map [ string ] * BoundMethod
boundByID map [ uint32 ] * BoundMethod
methodAliases map [ uint32 ] uint32
2023-02-26 15:06:05 +11:00
}
2023-08-27 20:39:35 +10:00
func NewBindings ( structs [] any , aliases map [ uint32 ] uint32 ) ( * Bindings , error ) {
2023-02-26 15:06:05 +11:00
b := & Bindings {
2023-08-27 20:39:35 +10:00
boundMethods : make ( map [ string ] map [ string ] map [ string ] * BoundMethod ),
boundByID : make ( map [ uint32 ] * BoundMethod ),
methodAliases : aliases ,
2023-02-26 15:06:05 +11:00
}
2023-08-27 20:39:35 +10: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
2023-08-19 14:25:19 +10:00
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
}
2023-08-19 14:25:19 +10:00
// 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
}
2023-08-19 14:25:19 +10:00
// GetByID returns the bound method with the given ID
func ( b * Bindings ) GetByID ( id uint32 ) * BoundMethod {
2023-08-27 20:39:35 +10:00
// Check method aliases
if b . methodAliases != nil {
if alias , ok := b . methodAliases [ id ]; ok {
id = alias
}
}
2023-08-19 14:25:19 +10:00
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 :]
2024-03-04 07:50:08 +01:00
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 ,
}
2023-08-19 14:25:19 +10:00
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 {
2023-08-27 20:39:35 +10:00
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 )
2024-03-04 07:50:08 +01:00
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
2024-03-04 07:50:08 +01:00
func ( b * BoundMethod ) Call ( ctx context . Context , args [] interface {}) ( returnValue interface {}, err error ) {
2023-08-27 20:39:35 +10:00
// 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 )
}
}()
2024-03-04 07:50:08 +01:00
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
}