edge case bug with `--asyncify`
Hello! :wave: I’m using wasm-opt --asyncify on a project of mine to allow the use of coroutines from C.
However, I recently ran into what really seems to me like a bug. I don’t know exactly what causes it, but it seems to happen consistently when compiling with -O0, but not with -O1 or higher.
I was able to reduce the reproduction to just a few dozen lines, but now it seems that if I try to make any further simplifications, the bug doesn’t show up.
Expected output: Empty, as there are supposedly no mistakes in the program.
Actual output: RuntimeError: null function or function signature mismatch
main.c
void *xxx_allocate(unsigned int m)
{
static unsigned char *memory[0x4000];
static unsigned int n = 0;
return memory + (n += m) - m;
}
struct xxx_behavior
{
void (*behave)(void *data);
void *data;
};
struct xxx_behavior *xxx_javascript_create_coroutine(void (*start)(struct xxx_behavior *behavior, void *data), void *data);
void xxx_javascript_yield(void *data);
void xxx_yield(void *data) { xxx_javascript_yield(data); }
void xxx_javascript_continue(void *data);
void xxx_continue(void *data) { xxx_javascript_continue(data); }
struct xxx_coroutine
{
struct xxx_behavior *behavior;
struct xxx_behavior *compound;
void (*start)(struct xxx_behavior *behavior, struct xxx_behavior *compound);
};
static void xxx_coroutine_start(struct xxx_behavior *behavior, void *data)
{
struct xxx_coroutine *coroutine = data;
(*coroutine->start)(behavior, coroutine->compound);
}
static void xxx_coroutine_behave(void *data)
{
struct xxx_coroutine *coroutine = data;
(*coroutine->behavior->behave)(coroutine->behavior->data);
(*coroutine->compound->behave)(coroutine->compound->data);
}
static void xxx_ignore(void *data) { }
static void xxx_behave(void *data)
{
void (*behavior)(void *data) = data;
(*behavior)(0);
}
static void xxx_f(struct xxx_behavior *behavior, struct xxx_behavior *compound)
{
struct xxx_behavior *other = xxx_allocate(sizeof *other);
other->behave = &xxx_behave;
other->data = &xxx_ignore;
compound->data = other;
for (;;) (*behavior->behave)(behavior->data);
}
static void xxx_compound_behave(void *data)
{
struct xxx_behavior *behavior = data;
(*behavior->behave)(behavior->data);
}
struct xxx_behavior *xxx(void)
{
struct xxx_coroutine *coroutine = xxx_allocate(sizeof *coroutine);
coroutine->start = &xxx_f;
coroutine->compound = xxx_allocate(sizeof *coroutine->compound);
coroutine->compound->behave = &xxx_compound_behave;
struct xxx_behavior *behavior = xxx_javascript_create_coroutine(&xxx_coroutine_start, coroutine);
coroutine->behavior = behavior;
struct xxx_behavior *result = xxx_allocate(sizeof *result);
result->behave = &xxx_coroutine_behave;
result->data = coroutine;
return result;
}
index.html
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>wasm-opt bug</title>
<script type="module">
let global = v =>
{
if (typeof v === "number") return v
return v.value
}
export let run = async () =>
{
let memcpy = (a, b, n) =>
{
new Uint8Array(memory.buffer, a, n).set(new Uint8Array(memory.buffer, b, n))
return a
}
let memmove = (a, b, n) =>
{
new Uint8Array(memory.buffer, a, n).set(new Uint8Array(memory.buffer, b, n).slice())
return a
}
let memset = (a, b, n) =>
{
new Uint8Array(memory.buffer, a, n).set(Array(n).fill(b))
return a
}
let memcmp = (a, b, n) =>
{
a = new Uint8Array(memory.buffer, a, n)
b = new Uint8Array(memory.buffer, b, n)
for (let i = 0 ; i < n ; i++)
if (a[i] !== b[i]) return a[i] - b[i]
return 0
}
let coroutineYield = coroutineBuffer =>
{
let struct = getView().getUint32(coroutineBuffer + 16, true)
if (asyncify_get_state() === 2)
asyncify_stop_rewind()
else
asyncify_start_unwind(struct)
}
let coroutineContinue = coroutineBuffer =>
{
let start = getView().getUint32(coroutineBuffer + 0, true)
let data = getView().getUint32(coroutineBuffer + 12, true)
let struct = getView().getUint32(coroutineBuffer + 16, true)
let behavior = getView().getUint32(coroutineBuffer + 20, true)
let state = getView().getUint32(coroutineBuffer + 24, true)
if (state !== 0)
asyncify_start_rewind(struct),
table.get(start)()
else
getView().setUint32(coroutineBuffer + 24, 1, true),
table.get(start)(behavior, data)
if (asyncify_get_state() === 1)
asyncify_stop_unwind()
}
let createCoroutine = (start, data) =>
{
let buffer = xxx_allocate(8192)
let struct = xxx_allocate(8)
getView().setUint32(struct + 0, buffer, true)
getView().setUint32(struct + 4, buffer + 8192, true)
let coroutineBuffer = xxx_allocate(32)
getView().setUint32(coroutineBuffer + 0, start, true)
getView().setUint32(coroutineBuffer + 8, buffer, true)
getView().setUint32(coroutineBuffer + 12, data, true)
getView().setUint32(coroutineBuffer + 16, struct, true)
getView().setUint32(coroutineBuffer + 24, 0, true)
let behavior = xxx_allocate(8)
getView().setUint32(behavior + 0, yieldIndex, true)
getView().setUint32(behavior + 4, coroutineBuffer, true)
getView().setUint32(coroutineBuffer + 20, behavior, true)
let coroutine = xxx_allocate(8)
getView().setUint32(coroutine + 0, continueIndex, true)
getView().setUint32(coroutine + 4, coroutineBuffer, true)
getView().setUint32(coroutineBuffer + 28, coroutine, true)
return coroutine
}
let imports =
{
env:
{
memcpy, memmove, memset, memcmp,
xxx_javascript_create_coroutine: createCoroutine,
xxx_javascript_yield: coroutineYield,
xxx_javascript_continue: coroutineContinue,
}
}
let body = await fetch("xxx.wasm")
let exports
try { exports = (await WebAssembly.instantiateStreaming(body, imports)).instance.exports }
catch (e) { exports = (await WebAssembly.instantiate(await body.arrayBuffer(), imports)).instance.exports }
let {memory, __indirect_function_table: table} = exports
let {xxx_allocate} = exports
let {xxx_yield, xxx_continue} = exports
let {asyncify_get_state} = exports
let {asyncify_start_rewind, asyncify_stop_rewind} = exports
let {asyncify_start_unwind, asyncify_stop_unwind} = exports
let {xxx: start} = exports
let getView = () => new DataView(memory.buffer)
let f = table.grow(2)
table.set(f + 0, xxx_yield)
table.set(f + 1, xxx_continue)
let yieldIndex = f + 0
let continueIndex = f + 1
let behavior = start()
let step = async () =>
{
let f = table.get(getView().getUint32(behavior, true))(getView().getUint32(behavior + 4, true))
setTimeout(step, 500)
}
step()
}
run()
</script>
I compile it as follows:
clang --target=wasm32 -ffreestanding -nostdlib -Wl,--no-entry \
-Wl,--export=xxx_yield \
-Wl,--export=xxx_continue \
-Wl,--export=xxx_create_coroutine \
-Wl,--export=xxx_coroutines \
-Wl,--export=xxx_allocate \
-Wl,--export=xxx \
-Wl,--allow-undefined \
-Wl,--export-table,--growable-table \
-o plain-xxx.wasm main.c
wasm-opt --asyncify plain-xxx.wasm -o xxx.wasm
Again, compiling with clang -O1 or higher makes the problem go away.
Compiling with optimizations can use fewer locals, and inlining can reduce the number of call frames, both of which can reduce the size of the Asyncify buffer that is needed. Try increasing it, and also you can build with assertions that should check if that is the issue, --pass-arg=asyncify-asserts
I tried to bump the buffer size by 1000 times (from 8192 to 8192000 bytes), and the error persisted.
--pass-arg=asyncify-asserts doesn’t change the error in any significant way (with 8192 and also with 8192000 bytes), but it does change the error (to an unreachable error) if I reduce the size to, say, just 64. So I don’t think the buffer size is the issue in this case.
Hmm, sounds like something else than the buffer size then.
Not sure I have a good idea other than to debug this manually. Unfortunately debugging this kind of thing is a little tricky. But I'd recommend adding some manual debug logging perhaps.