Skip to main content

Async WASM

Standard WASM execution is synchronous - a function is called, some computation happens and eventually a result it returned. This model does not work well for longer running programs which may need to wait for an event to happen (e.g. IVirtualEventPoll).

Async WASM transforms a synchronous program into an asynchronous program which can have execution suspended and resumed later. This allows a single function call to be spread across multiple frames. While a call is suspended it consumes no CPU resources.

tip

Enable async WASM by checking "asyncify" in the importer.

Calling Async WASM

When a WASM function is transformed into an async WASM call this changes the value returned from the autogenerated wrapper code. Instead of return type T the function will now return a Future<T>, which allows you to resume execution repeatedly until a result is produced.

The model is very similar to a Unity coroutine, where yielding allows you to delay execution of a method for one frame. Futures can be used in a coroutine:

Async WASM Coroutine
// Call the function like normal
var future = Wrapper.run_example_method(1, 2, 3, 4);

// Resume execution every frame until it is completed
while (!future.TryGetResult(out var result))
{
future.Resume();
yield return null;
}

// Do something with the final return result
Debug.Log(result);

Error Handling

Wasmbox supports two types of error handling: exceptions and results, both are supported by async wasm. If results are in use then the Future<T> will return the result container from TryGetResult. If exceptions are in use then the exception will be thrown when TryGetResult is called.

Unity Job System

A Future<T> cannot be passed into a job. It can be converted into a FutureRunner<T> by calling .ToJob(), this can be passed into a custom job.

note

A FutureRunner<T> cannot be used to retrieve a result. Once Resume() has returned true inside a Job then TryGetResult can be called back on the original Future<T> (on the main thread).

Async Job Coroutine
// Call the function like normal
var future = Wrapper.run_example_method(1, 2, 3, 4);

// Finish the future in jobs
yield return future.ToJob()

// Do something with the final return result
future.TryGetResult(out var result);
Debug.Log(result);

Caveats

No Concurrency

When using async WASM a given wrapper may only have one async call running at once! It would be possible to have two calls suspended at once, but it would introduce concurrency bugs in WASM which was not specifically designed to handle it, so it is not allowed by Wasmbox. The following code will throw an exception:

Don't Do This!
var future1 = Wrapper.run_example_method(1, 2, 3, 4);
var future2 = Wrapper.run_example_method(4, 3, 2, 1); // This will throw an `InvalidOperationException`

Single Result

When a result is retrieved by calling Future<T>.TryGetResult the memory backing that future is internally cleaned up. All calls to Resume and TryGetResult after this will throw an InvalidOperationException. The following code will throw an exception:

Don't Do This!
// Call an async method
var future = Wrapper.run_example_method(1, 2, 3, 4);

// Wait for it to complete
yield return future;

// Get the result
future.TryGetResult(out var result);
Debug.Log(result);

// These will throw an `InvalidOperationException`
future.TryGetResult(out var result);
future.Resume();

Void

Methods which return nothing (i.e. void) will return Future<Void> when converted to async. Void is an empty struct which contains nothing.

This is fine!
// Call an async method
var future = Wrapper.this_returns_void(1, 2, 3, 4);

// Wait for it to complete
yield return future;

// Get the "result", you can't do anything with it because it represents nothing, but it's there!
future.TryGetResult(out var result);