github.com/primecitizens/pcz/std@v0.2.1/ffi/js/callback.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright 2023 The Prime Citizens 3 4 package js 5 6 import ( 7 "unsafe" 8 9 stdconst "github.com/primecitizens/pcz/std/builtin/const" 10 "github.com/primecitizens/pcz/std/core/abi" 11 "github.com/primecitizens/pcz/std/core/assert" 12 "github.com/primecitizens/pcz/std/core/mark" 13 "github.com/primecitizens/pcz/std/core/math" 14 "github.com/primecitizens/pcz/std/ffi" 15 "github.com/primecitizens/pcz/std/ffi/js/bindings" 16 "github.com/primecitizens/pcz/std/ffi/js/callback" 17 ) 18 19 // CallbackDispatcher defines the interface of a js callback dispatcher. 20 type CallbackDispatcher interface { 21 ffi.CallbackDispatcher[CallbackContext] 22 } 23 24 // UntypedCallback dispatches js callbacks without resolving argument types. 25 // 26 // By convension, args[0] refers to the js `this` context. 27 type UntypedCallback func(args []Ref) 28 29 // Register registers this function to js and return the reference to that js function. 30 func (fn UntypedCallback) Register() Func[func([]Ref)] { 31 return RegisterCallback[func([]Ref)](fn, abi.FuncPCABIInternal(fn)) 32 } 33 34 // DispatchCallback implements [CallbackDispatcher]. 35 func (fn UntypedCallback) DispatchCallback(_ uintptr, ctx *CallbackContext) { 36 fn(ctx.Args()) 37 } 38 39 // dispFunc for type checking 40 type dispFunc[T any] ffi.DispatchFunc[T, CallbackContext] 41 42 // RegisterCallback creates a js function to be called from js. 43 // 44 // See [CallbackContext] for more details. 45 // 46 // NOTE: type param D MUST be a pointer type, that is, it either 47 // - comes with star sign (e.g. *Foo) 48 // - or being a function (e.g. PromiseCallbackHandleFunc). 49 func RegisterCallback[F any, D CallbackDispatcher](dispatcher D, targetPC uintptr) Func[F] { 50 if unsafe.Sizeof(dispatcher) != stdconst.SizePointer { 51 assert.Throw("invalid", "callback", "handler:", "not", "a", "pointer", "value") 52 } 53 54 var _ dispFunc[D] = D.DispatchCallback // type check 55 return Func[F]{ 56 ref: callback.Func( 57 unsafe.Pointer(abi.FuncPCABIInternal(D.DispatchCallback)), 58 // NOTE: this is still the `dispatcher` 59 // we just converted it to pointer value as assumed 60 Pointer(*(**byte)(mark.NoEscapePointer(&dispatcher))), 61 SizeU(targetPC), 62 ), 63 } 64 } 65 66 // CallbackContext is the context of a function call from JS to Go. 67 // 68 // Limitation: At most 16 args (including `this`) are allowed. 69 // 70 // The call can be made synchronously (go -> js -> go) and 71 // asynchronously (js -> go), the library supports both in one form. 72 // 73 // It is user's responsibility to ensure the callback does not call await-like 74 // functions such as js.Promise.Await() and yield.Thread() when the js side 75 // expects returning result synchronously, and if not so, the js side will 76 // get a Promise instead of immediate value. 77 // 78 // To return value, call one of .ReturnXxx functions. 79 // If none of the return functions was called, it returns js value "undefined". 80 // If more than one return functions were called, it returns the value passed 81 // in the last .ReturnXxx function. 82 // 83 // After returning from the target Go function, 84 // All heap references available in the slice returned by .Args() are 85 // freed, so users MUST NOT retain any of these references, and if 86 // you do want to use after the callback, call Ref.Clone() to make a copy. 87 type CallbackContext struct { 88 dispFnPC uint32 89 handler uint32 90 targetPC uint32 91 retRef bindings.Ref 92 resolveFnRef bindings.Ref 93 nargs uint32 94 args [16]bindings.Ref 95 } 96 97 func (ctx *CallbackContext) Args() []Ref { 98 return unsafe.Slice((*Ref)(mark.NoEscape(&ctx.args[0])), ctx.nargs) 99 } 100 101 func (ctx *CallbackContext) Return(ref Ref) bool { 102 return bindings.Ref(True) == bindings.Replace( 103 ctx.retRef, bindings.Ref(ref), 104 ) 105 } 106 107 func (ctx *CallbackContext) ReturnNum(n float64) bool { 108 return bindings.Ref(True) == bindings.ReplaceNum( 109 ctx.retRef, n, 110 ) 111 } 112 113 func (ctx *CallbackContext) ReturnBool(b bool) bool { 114 return bindings.Ref(True) == bindings.ReplaceBool( 115 ctx.retRef, bindings.Ref(Bool(b)), 116 ) 117 } 118 119 func (ctx *CallbackContext) ReturnString(s string) bool { 120 return bindings.Ref(True) == bindings.ReplaceString( 121 ctx.retRef, 122 StringData(s), 123 SizeU(len(s)), 124 ) 125 } 126 127 func init() { 128 const offsetsOK = true && 129 unsafe.Offsetof(CallbackContext{}.dispFnPC) == 0 && 130 unsafe.Offsetof(CallbackContext{}.handler) == 4 && 131 unsafe.Offsetof(CallbackContext{}.targetPC) == 8 && 132 unsafe.Offsetof(CallbackContext{}.retRef) == 12 && 133 unsafe.Offsetof(CallbackContext{}.resolveFnRef) == 16 && 134 unsafe.Offsetof(CallbackContext{}.nargs) == 20 && 135 unsafe.Offsetof(CallbackContext{}.args) == 24 136 137 if !offsetsOK { 138 assert.Throw("invalid", "field", "offsets") 139 } 140 } 141 142 // handleCallback called from wasm_export_run. 143 // 144 // NOTE: When inserting code after `callDispatcher` and `DONE` 145 // also update the LR update code pointing to this function 146 // inside callDispatcher (asm implementation). 147 func handleCallback(ref bindings.Ref) { 148 var cb CallbackContext 149 150 // defensive, to ensure js side sets nargs. 151 cb.nargs = math.MaxUint32 152 153 callback.Context(ref, Pointer(&cb)) 154 if cb.nargs > 16 { 155 assert.Throw("too", "many", "args", "to", "callback", "func") 156 } 157 158 callDispatcher( 159 uintptr(cb.handler), 160 uintptr(cb.targetPC), 161 mark.NoEscape(&cb), 162 uintptr(cb.dispFnPC), 163 ) 164 165 // DONE: 166 Func[func(bool)]{ref: cb.resolveFnRef}.CallVoid(Undefined, false) 167 }