Summary
On single-threaded runtimes (Unity WebGL / IL2CPP → WASM), a script that calls coroutine.yield deadlocks the only thread when the ExecuteAsync ValueTask is consumed synchronously via GetAwaiter().GetResult(). Pure-compute scripts run fine; only paths that actually suspend the async state machine (coroutines) hang.
Environment
- Unity 6000.x, IL2CPP, WebGL player (single-threaded WASM, no thread pool)
- Lua-CSharp (latest NuGet
LuaCSharp, Lua.dll, netstandard2.1)
- Desktop Mono/.NET (Windows) and Android IL2CPP work fine — threads available.
Repro (conceptual)
var state = LuaState.Create();
state.OpenBasicLibrary(); state.OpenCoroutineLibrary(); /* string/table/math ... */
LuaValue[] Run(LuaClosure c) => state.ExecuteAsync(c).GetAwaiter().GetResult();
// OK on WebGL:
Run(state.Load("return (2+3)*4"));
// HANGS on WebGL (never returns, freezes the player):
Run(state.Load([[
local co = coroutine.create(function(a) local b = coroutine.yield(a+1); return b*2 end)
local _, x = coroutine.resume(co, 10)
local _, y = coroutine.resume(co, 5)
return x + y]]));
Observed
- arithmetic / strings / tables / closures / recursion / metatables: all correct on WebGL.
- the coroutine case:
ExecuteAsync(...).GetAwaiter().GetResult() never returns; the WASM main thread is blocked permanently.
Analysis
coroutine.yield genuinely suspends the async state machine, so ExecuteAsync returns an incomplete ValueTask whose continuation must be pumped. On desktop the continuation runs on another thread while the caller blocks, so .GetResult() eventually returns. On single-threaded WASM there is no other thread, and the only thread is parked in the blocking wait, so the continuation can never run — a classic sync-over-async deadlock.
Ask
Would you consider: (1) documenting the single-threaded / WebGL constraint and the recommended way to drive ExecuteAsync without blocking (pumping the ValueTask across frames on the main thread), ideally with a small sample; and/or (2) clarifying whether coroutines are expected to work under a synchronous drive on single-threaded hosts?
Thanks for the library — on desktop/Android it is noticeably faster and lighter than MoonSharp in our benchmarks; we are evaluating it as a Unity modding runtime and this is the one remaining blocker for WebGL parity.
Summary
On single-threaded runtimes (Unity WebGL / IL2CPP → WASM), a script that calls
coroutine.yielddeadlocks the only thread when theExecuteAsyncValueTaskis consumed synchronously viaGetAwaiter().GetResult(). Pure-compute scripts run fine; only paths that actually suspend the async state machine (coroutines) hang.Environment
LuaCSharp,Lua.dll, netstandard2.1)Repro (conceptual)
Observed
ExecuteAsync(...).GetAwaiter().GetResult()never returns; the WASM main thread is blocked permanently.Analysis
coroutine.yieldgenuinely suspends the async state machine, soExecuteAsyncreturns an incompleteValueTaskwhose continuation must be pumped. On desktop the continuation runs on another thread while the caller blocks, so.GetResult()eventually returns. On single-threaded WASM there is no other thread, and the only thread is parked in the blocking wait, so the continuation can never run — a classic sync-over-async deadlock.Ask
Would you consider: (1) documenting the single-threaded / WebGL constraint and the recommended way to drive
ExecuteAsyncwithout blocking (pumping theValueTaskacross frames on the main thread), ideally with a small sample; and/or (2) clarifying whether coroutines are expected to work under a synchronous drive on single-threaded hosts?Thanks for the library — on desktop/Android it is noticeably faster and lighter than MoonSharp in our benchmarks; we are evaluating it as a Unity modding runtime and this is the one remaining blocker for WebGL parity.