Skip to main content

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.

Addressables

If you are using Addressable Loading refer to the Addressable Loading section.

Glossary

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.

tip

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();
danger

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 or Instance in any way while the wrapper is in use in a job.