Skip to main content

Thunk Functions

In computer programming, a thunk is a wrapper function that is used to inject code around another function. Thunks are used to insert operations before and/or after the wrapped function is being called to adapt it to changing requirements. The schema tool will generate such thunk functions to be able to properly set up calls to the smart contract functions. It also creates a mapping between the name/id of the function and the actual function, and generates code to properly communicate this mapping to the ISC host.

In our case we use thunks not only to inject code around the smart contract function, but also to make the smart contract function type-safe. The thunks all have an identical function signature, and each will set up a function-specific data structure so that the actual smart contract function will deal with them in a type-safe way. Having a common function signature for the thunks means that it is easy to generate a table of all functions and their names that can be used to generically call these functions.

All code for this table and the thunks is generated as part of lib.xx and it looks as follows for the dividend example smart contract (for simplicity the thunk function contents has been omitted for now):

var exportMap = wasmlib.ScExportMap{
Names: []string{
FuncDivide,
FuncInit,
FuncMember,
FuncSetOwner,
ViewGetFactor,
ViewGetOwner,
},
Funcs: []wasmlib.ScFuncContextFunction{
funcDivideThunk,
funcInitThunk,
funcMemberThunk,
funcSetOwnerThunk,
},
Views: []wasmlib.ScViewContextFunction{
viewGetFactorThunk,
viewGetOwnerThunk,
},
}

func OnLoad(index int32) {
if index >= 0 {
wasmlib.ScExportsCall(index, &exportMap)
return
}

wasmlib.ScExportsExport(&exportMap)
}

func funcDivideThunk(ctx wasmlib.ScFuncContext) {}
func funcInitThunk(ctx wasmlib.ScFuncContext) {}
func funcMemberThunk(ctx wasmlib.ScFuncContext) {}
func funcSetOwnerThunk(ctx wasmlib.ScFuncContext) {}
func viewGetFactorThunk(ctx wasmlib.ScViewContext) {}
func viewGetOwnerThunk(ctx wasmlib.ScViewContext) {}

The key functions here are the on_load() and on_call() Wasm callback functions (the Go version defines those in WasmLib and delegates them both to OnLoad()). The on_load() Wasm function will be called by the Wasm VM upon loading of the Wasm code. It will inform the ISC host of all the function ids and types (Func or View) that the smart contract provides.

When the host needs to call a function of the smart contract it will call the on_call() callback function with the corresponding function id, and then the on_call() function will dispatch the call to the proper thunk function according to the mapping table that was generated by the schema tool.

Here is an example implementation of a thunk function for the setOwner() contract function. You can examine the other thunk functions that all follow the same pattern in the generated lib.xx:

type SetOwnerContext struct {
Params ImmutableSetOwnerParams
State MutableDividendState
}

func funcSetOwnerThunk(ctx wasmlib.ScFuncContext) {
ctx.Log("dividend.funcSetOwner")
f := &SetOwnerContext{
Params: ImmutableSetOwnerParams{
proxy: wasmlib.NewParamsProxy(),
},
State: MutableDividendState{
proxy: wasmlib.NewStateProxy(),
},
}

// only defined owner of contract can change owner
access := f.State.Owner()
ctx.Require(access.Exists(), "access not set: owner")
ctx.Require(ctx.Caller() == access.Value(), "no permission")

ctx.Require(f.Params.Owner().Exists(), "missing mandatory owner")
funcSetOwner(ctx, f)
ctx.Log("dividend.funcSetOwner ok")
}

First, the thunk logs the contract and function name to show that the call has started. Then it sets up a strongly typed function-specific context structure. First, we add the function-specific immutable params interface structure, which is only present when the function actually can have parameters. Then we add the contract-specific state interface structure. In this case it is mutable because setOwner is a Func. For Views this would be an immutable state interface. Finally, we would add the function-specific mutable results interface structure, which is only present when the function actually returns results. Obviously, this is not the case for this setOwner() function.

Next it sets up access control for the function according to the schema definition file. In this case it retrieves the owner state variable through the function context, requires that the variable exists, and then requires that the caller() of the function equals that value. Any failing requirement will panic out of the thunk function with an error message. So this code makes sure that only the owner of the contract can call this function.

Now we get to the point where we can use the function-specific params interface to check for mandatory parameters. Each mandatory parameter is required to exist, or else we will panic out of the thunk function with an error message.

With the setup and automated checks completed, we now call the actual smart contract function implementation that is maintained by the user. After this function has completed, we would process the returned results for those functions that have any (in this case we obviously don't have results), and finally we log that the contract function has completed successfully. Remember that any error within the user function will cause a panic, so this logging will never occur in case that happens.

In the next section we will look at the specifics of view functions.