Custom WASM Instantiation
Wasmbox provides behaviours to automatically load a WASM asset into a state that allows code to be run. However, it is possible to do this process directly in your own code to take complete control over the entire process.
If you are using Addressable Loading
refer to the Addressable Loading section.
Refer to the glossary for an overview of all the components involved.
Step By Step
Required Resources
IWasmAsset Asset;
EngineConfig Config;
Before loading any WASM an IWasmAsset
and an EngineConfig
are required.
An IWasmAsset
represents a source that WASM can be loaded from. This may be a WasmAsset
(imported in editor) a DynamicWasmAsset
(loaded at runtime from a file) or a custom IWasmAsset
implementation.
An EngineConfig
configures how the WASM loaded from the asset should be compiled into executable code. Compile time features such as Fuel Usage can be enabled.
Loading
var module = Asset.Load(Config);
if (module == null)
throw new Exception("Loading Failed");
Loading the asset with an EngineConfig
creates a LoadedModule
which contains the compiled executable machine code in memory. If loading fails for some reason a null
object will be returned.
Compiling a WASM Module is potentially a slow process if the asset is large and has not been precompiled.
Configuring
var store = Config.CreateStore();
var linker = Config.CreateLinker();
linker.DefineFunction("demo", "add", (int a, int b) => a + b);
linker.DefineFunction("demo", "sub", (int a, int b) => a - b);
To create an Instance
from a module requires a Store
and a Linker
.
The Linker
can be used to expose C# functions to WASM. See the tutorial on linking for more information.
The Store
contains all the state of the executed WASM code. Multiple Instances
can share a Store
.
Instantiating
var instance = module.CreateInstance(linker, store);
var add = instance.GetFunction<int, int, int>("add");
var result = add(1, 2);
Finally an Instance
can be created using the module, Store
and Linker
.
Functions
, Memories
, Tables
and Globals
can be retrieved from the Instance
and used later.
Complete Example
public class DemoWasmLoading
: MonoBehaviour
{
private WasmAsset Asset;
private EngineConfig Config;
void OnEnable()
{
using var module = Asset.Load(Config);
if (module == null)
throw new Exception("Loading Failed");
using var store = Config.CreateStore();
using var linker = Config.CreateLinker();
linker.DefineFunction("demo", "add", (int a, int b) => a + b);
linker.DefineFunction("demo", "sub", (int a, int b) => a - b);
var instance = module.CreateInstance(linker, store);
var add = instance.GetFunction<int, int, int>("add");
var result = add(1, 2);
Debug.Log(result);
}
}
Potential Extensions
Now that you have complete control over the process of loading and instantiating WASM there are several interesting things you can do:
Addressable Loading
If "Addressable Loading" is enabled in the importer then most of these steps can be skipped by using the CreateAsync
method on the auto generated wrapper code. In particular no direct reference to the WasmAsset
is ever required, it is automatically acquired through the addressable asset system.
public class DemoAddressableLoading
{
private EngineConfig Config;
async Task<TheAutogeneratedWrapper> DemoAsync()
{
using var linker = Config.CreateLinker();
linker.DefineFunction("demo", "mul", (int a, int b) => a * b);
linker.DefineFunction("demo", "div", (int a, int b) => a / b);
using var wrapper = TheAutogeneratedWrapper.LoadAsync(linker);
var result = wrapper.Add(1, 2); // Assuming the WASM defines a method called `Add`
Debug.Log(result);
}
}
If you still wish to pass in a Linker
or a Store
that is possible, but not required:
var store = Config.CreateStore();
using var linker = Config.CreateLinker();
linker.DefineFunction("demo", "add", (int a, int b) => a + b);
linker.DefineFunction("demo", "sub", (int a, int b) => a - b);
var wrapper = await TheAutogeneratedWrapper.CreateAsync(Config, linker, store);
Linker Re-Use
The Linker
in the above example is created, used once and then disposed. However, a Linker
can be re-used as long as the EngineConfig
of the Linker
and the Module
are the same.
LoadedModule module;
Linker linker;
void OnEnable()
{
module = Asset.Load(Config);
if (module == null)
throw new Exception("Loading Failed");
// Do the linker setup just once
linker = module.CreateLinker();
linker.DefineFunction("demo", "add", (int a, int b) => a + b);
linker.DefineFunction("demo", "sub", (int a, int b) => a - b);
}
void Update()
{
// Create and use lots of instances using this linker
using var store = module.CreateStore();
var instance = module.CreateInstance(linker, store);
var add = instance.GetFunction<int, int, int>("add");
var result = add(1, 2);
Debug.Log(result);
}
void OnDisable()
{
module?.Dispose();
module = null;
linker?.Dispose();
linker = null;
}
Store Re-Use
A Store
can also be shared between multiple Instances
. However be cautious - the Store
never deallocates any resources (until it is disposed). This means that any resources created within the Store
(e.g. a Memory
object) by any Instance
will not be destroyed until the Store
is destroyed.
Wrapper Code
The autogenerated wrapper code can be used to make calling WASM code simpler.
using var t = new TheAutogeneratedWrapper(instance, store, disposeStore: true);
var result = t.add(1, 2);
The final argument (disposeStore
) indicates if the Store
should be disposed when the wrapper is disposed.
Unity Job System
The wrapper code is designed such that it can be passed into a Unity Job, internally this is passing the Store
& Instance
into the Job so that they can be used.
var wrapper = new TheAutogeneratedWrapper(instance, store, disposeStore: true);
var handle = new DemoJob(wrapper).Schedule();
handle.Complete();
The wrapper provides the integration with the Unity safety system, improper usage can easily bypass safety and cause hard to debug multithreading errors.
Do Not:
- Create multiple wrappers around the same
Instance
. - Share a
Store
between multiple wrappers which are scheduled. - Access the
Store
orInstance
in any way while the wrapper is in use in a job.