Trampolines are used to interface between the Go runtime and the generated code, in two cases:

  • when we need to enter the generated code from the Go runtime.
  • when we need to leave the generated code to invoke a host function (written in Go).

In this section we want to complete the picture of how a Wasm function gets translated from Wasm to executable code in the optimizing compiler, by describing how to jump into the execution of the generated code at run-time.

Entering the Generated Code

At run-time, user space invokes a Wasm function through the public api.Function interface, using methods Call() or CallWithStack(). The implementation of this method, in turn, eventually invokes an ASM trampoline. The signature of this trampoline in Go code is:

func entrypoint(
	preambleExecutable, functionExecutable *byte,
	executionContextPtr uintptr, moduleContextPtr *byte,
	paramResultStackPtr *uint64,
	goAllocatedStackSlicePtr uintptr)
  • preambleExecutable is a pointer to the generated code for the preamble (see below)
  • functionExecutable is a pointer to the generated code for the function (as described in the previous sections).
  • executionContextPtr is a raw pointer to the wazevo.executionContext struct. This struct is used to save the state of the Go runtime before entering or leaving the generated code. It also holds shared state between the Go runtime and the generated code, such as the exit code that is used to terminate execution on failure, or suspend it to invoke host functions.
  • moduleContextPtr is a pointer to the wazevo.moduleContextOpaque struct. This struct Its contents are basically the pointers to the module instance, specific objects as well as functions. This is sometimes called “VMContext” in other Wasm runtimes.
  • paramResultStackPtr is a pointer to the slice where the arguments and results of the function are passed.
  • goAllocatedStackSlicePtr is an aligned pointer to the Go-allocated stack for holding values and call frames. For further details refer to Backend § Prologue and Epilogue

The trampoline can be found inbackend/isa/<arch>/abi_entry_<arch>.s.

For each given architecture, the trampoline:

  • moves the arguments to specific registers to match the behavior of the entry preamble or trampoline function, and
  • finally, it jumps into the execution of the generated code for the preamble

The preamble that will be jumped from entrypoint function is generated per function signature.

This is implemented in machine.CompileEntryPreamble(*ssa.Signature).

The preamble sets the fields in the wazevo.executionContext.

At the beginning of the preamble:

  • Set a register to point to the *wazevo.executionContext struct.
  • Save the stack pointers, frame pointers, return addresses, etc. to that struct.
  • Update the stack pointer to point to paramResultStackPtr.

The generated code works in concert with the assumption that the preamble has been entered through the aforementioned trampoline. Thus, it assumes that the arguments can be found in some specific registers.

The preamble then assigns the arguments pointed at by paramResultStackPtr to the registers and stack location that the generated code expects.

Finally, it invokes the generated code for the function.

The epilogue reverses part of the process, finally returning control to the caller of the entrypoint() function, and the Go runtime. The caller of entrypoint() is also responsible for completing the cleaning up procedure by invoking afterGoFunctionCallEntrypoint() (again, implemented in backend-specific ASM). which will restore the stack pointers and return control to the caller of the function.

The arch-specific code can be found in backend/isa/<arch>/abi_entry_preamble.go.

Leaving the Generated Code

In “How do compiler functions work?”, we already outlined how leaving the generated code works with the help of a function. We will complete here the picture by briefly describing the code that is generated.

When the generated code needs to return control to the Go runtime, it inserts a meta-instruction that is called exitSequence in both amd64 and arm64 backends. This meta-instruction sets the exitCode in the wazevo.executionContext struct, restore the stack pointers and then returns control to the caller of the entrypoint() function described above.

As described in “How do compiler functions work?”, the mechanism is essentially the same when invoking a host function or raising an error. However, when a function is invoked the exitCode also indicates the identifier of the host function to be invoked.

The magic really happens in the backend.Machine.CompileGoFunctionTrampoline() method. This method is actually invoked when host modules are being instantiated. It generates a trampoline that is used to invoke such functions from the generated code.

This trampoline implements essentially the same prologue as the entrypoint(), but it also reserves space for the arguments and results of the function to be invoked.

A host function has the signature:

func(ctx context.Context, stack []uint64)

the function arguments in the stack parameter are copied over to the reserved slots of the real stack. For instance, on arm64 the stack layout would look as follows (on amd64 it would be similar):

sliScPeSizes(iRaaa(hzerrrflietgggsrog_u[[[lawhrraaaorxN10imeerrrfnx]]]ceaattggg_Ax/e_ddadxrrrSsddY0X10rdeeeiirrgrtttzzee_e[[[eessrsM10sses]]]))tg;os;Ciazmleil_gSohtfta_cabkaroeSrgrig_ipz0rgaereidetndtae0ldAAdrtdgor0RRmeeaggkeit16-bytealigned.

Finally, the trampoline jumps into the execution of the host function using the exitSequence meta-instruction.

Upon return, the process is reversed.

Code

  • The trampoline to enter the generated function is implemented by the backend.Machine.CompileEntryPreamble() method.
  • The trampoline to return traps and invoke host functions is generated by backend.Machine.CompileGoFunctionTrampoline() method.

You can find arch-specific implementations in backend/isa/<arch>/abi_go_call.go, backend/isa/<arch>/abi_entry_preamble.go, etc. The trampolines are found under backend/isa/<arch>/abi_entry_<arch>.s.

Further References