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.
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:
// 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.
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).
// 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:
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:
// 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.
// 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);