Thunk Functions
In software development, thunk functions are a crucial part of setting up smart contracts correctly, facilitating type-safety and easy mapping of function names to their actual implementations.
Overview
A thunk is a wrapper function employed to inject code before and/or after a function call. It helps in adapting functions to meet changing demands. Through the Schema Tool, thunks are generated to establish correct calls to smart contract functions and to ensure type-safety. They share a common function signature, fostering a straightforward creation of a function table for generic calls.
Role in Wasm
Working hand in hand with the WebAssembly (Wasm) format,
thunks play a pivotal role in the lib.xx
component.
Below is how a chunk of a dividend
smart contract would look (the detailed thunk function content is omitted):
- Go
- Rust
- Typescript
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 OnDispatch(index int32) {
exportMap.Dispatch(index)
}
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) {}
const EXPORT_MAP: ScExportMap = ScExportMap {
names: &[
FUNC_DIVIDE,
FUNC_INIT,
FUNC_MEMBER,
FUNC_SET_OWNER,
VIEW_GET_FACTOR,
VIEW_GET_OWNER,
],
funcs: &[
func_divide_thunk,
func_init_thunk,
func_member_thunk,
func_set_owner_thunk,
],
views: &[
view_get_factor_thunk,
view_get_owner_thunk,
],
};
pub fn on_dispatch(index: i32) {
EXPORT_MAP.dispatch(index);
}
fn func_divide_thunk(ctx: &ScFuncContext) {}
fn func_init_thunk(ctx: &ScFuncContext) {}
fn func_member_thunk(ctx: &ScFuncContext) {}
fn func_set_owner_thunk(ctx: &ScFuncContext) {}
fn view_get_factor_thunk(ctx: &ScViewContext) {}
fn view_get_owner_thunk(ctx: &ScViewContext) {}
const exportMap: wasmlib.ScExportMap = {
names: [
sc.FuncDivide,
sc.FuncInit,
sc.FuncMember,
sc.FuncSetOwner,
sc.ViewGetFactor,
sc.ViewGetOwner,
],
funcs: [funcDivideThunk, funcInitThunk, funcMemberThunk, funcSetOwnerThunk],
views: [viewGetFactorThunk, viewGetOwnerThunk],
};
export function on_dispatch(index: i32): void {
exportMap.dispatch(index);
}
function funcDivideThunk(ctx: ScFuncContext) {}
function funcInitThunk(ctx: ScFuncContext) {}
function funcMemberThunk(ctx: ScFuncContext) {}
function funcSetOwnerThunk(ctx: ScFuncContext) {}
function viewGetFactorThunk(ctx: ScViewContext) {}
function viewGetOwnerThunk(ctx: ScViewContext) {}
The Dispatch Process
The central player here is the OnDispatch()
function,
called by the primary Wasm file, essentially a dynamic link library.
This mechanism keeps the smart contract (SC) code self-contained and versatile,
fitting for both Wasm requirements and direct client-side executions.
To meet Wasm's demands, we implement on_load()
and on_call()
callbacks that collaborate with OnDispatch()
in the lib.xx
,
orchestrating a seamless dispatch process.
on_load()
The on_load()
Wasm function will be called by the Wasm VM host upon loading of the Wasm
code. It will inform the host of all the function ids and types (Func or View) that this
smart contract provides.
on_call()
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 via the ScExportMap
mapping table that was generated by the
Schema Tool to the proper associated thunk function.
Generated Main Entry Point
Here is the generated main.xx
that forms the main entry point for the Wasm code:
- Go
- Rust
- Typescript
//go:build wasm
// +build wasm
package main
import "github.com/iotaledger/wasp/packages/wasmvm/wasmvmhost/go/wasmvmhost"
import "github.com/iotaledger/wasp/contracts/wasm/dividend/go/dividend"
func main() {
}
func init() {
wasmvmhost.ConnectWasmHost()
}
//export on_call
func onCall(index int32) {
dividend.OnDispatch(index)
}
//export on_load
func onLoad() {
dividend.OnDispatch(-1)
}
use dividend::*;
use wasmvmhost::*;
#[no_mangle]
fn on_call(index: i32) {
WasmVmHost::connect();
on_dispatch(index);
}
#[no_mangle]
fn on_load() {
WasmVmHost::connect();
on_dispatch(-1);
}
import * as wasmvmhost from 'wasmvmhost';
import * as sc from './dividend';
export function on_call(index: i32): void {
wasmvmhost.WasmVMHost.connect();
sc.onDispatch(index);
}
export function on_load(): void {
wasmvmhost.WasmVMHost.connect();
sc.onDispatch(-1);
}
Finally, 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
:
- Go
- Rust
- Typescript
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")
}
pub struct SetOwnerContext {
params: ImmutableSetOwnerParams,
state: MutableDividendState,
}
fn func_set_owner_thunk(ctx: &ScFuncContext) {
ctx.log("dividend.funcSetOwner");
let f = SetOwnerContext {
params: ImmutableSetOwnerParams { proxy: params_proxy() },
state: MutableDividendState { proxy: state_proxy() },
};
// only defined owner of contract can change owner
let 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");
func_set_owner(ctx, &f);
ctx.log("dividend.funcSetOwner ok");
}
// this class is actually defined in contract.ts
export class SetOwnerContext {
params: sc.ImmutableSetOwnerParams = new sc.ImmutableSetOwnerParams(
wasmlib.paramsProxy(),
);
state: sc.MutableDividendState = new sc.MutableDividendState(
wasmlib.ScState.proxy(),
);
}
function funcSetOwnerThunk(ctx: wasmlib.ScFuncContext): void {
ctx.log('dividend.funcSetOwner');
let f = new sc.SetOwnerContext();
// only defined owner of contract can change owner
const access = f.state.owner();
ctx.require(access.exists(), 'access not set: owner');
ctx.require(ctx.caller().equals(access.value()), 'no permission');
ctx.require(f.params.owner().exists(), 'missing mandatory owner');
sc.funcSetOwner(ctx, f);
ctx.log('dividend.funcSetOwner ok');
}
The Thunk Function in Action
Log the Contract and Function Name
First, the thunk logs the contract and function name to show that the call has started.
Set Up Strongly Typed Context
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 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 returns results.
This is not the case for this setOwner()
function.
Set Up Access Control
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 function's caller()
equals that value. Any failing requirement will panic out of the thunk function with an
error message. So, this code ensures that only the contract owner can call this
function.
Verify Function Parameters
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.
Call the Smart Contract Function
With the setup and automated checks completed, we now call the actual smart contract function implementation the user maintains. After this function has been completed, we would process the returned results for those functions that have any (in this case, we don't have results). Finally, we log that the contract function has been completed successfully. Remember that any error within the user function will cause a panic, so this logging will never occur in case that happens.