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  }