Skip to content

Free the parameter block of async exports in every generator - #1758

Draft
Aditya1404Sal wants to merge 3 commits into
bytecodealliance:mainfrom
Aditya1404Sal:upstream-free-async-export-params
Draft

Aditya1404Sal wants to merge 3 commits into
bytecodealliance:mainfrom
Aditya1404Sal:upstream-free-async-export-params

Conversation

@Aditya1404Sal

@Aditya1404Sal Aditya1404Sal commented Oct 27, 2026 •

Copy link
Copy Markdown

This fixes the leak from #1757 in every generator

When an async export's params don't fit in 16 flat values they arrive in a block the callee owns, and nothing ever freed it, so every call leaked it. Sync exports free it after the call, but core can't do that for async: the code after the call can end up in another function, or never run when the task is cancelled. So CallInterface now has params_block: Option<(size, align)>. When it's set, the block pointer is the last operand and the generator frees the block itself. Sync exports are unchanged.

That's a change to the public Instruction enum: a Bindgen outside this repo that matches on CallInterface needs .. in its pattern now, and if it generates async exports it has to pop that operand and free the block.

Where each generator frees it:

  • Rust: after the future is created and before the first .await, so a call that gets cancelled while waiting frees it too.
  • C: after the user's function returns, right before return ret; in the export wrapper.
  • C#: right after var task = XAsync(...);, since the arguments are already managed copies by then.
  • MoonBit: every argument is bound to a local first, then the block is freed, then the call runs.
  • Go: nothing to free, the block is Go-managed memory that the existing Unpin() releases; it just drops the extra operand.
  • D: only the match pattern changes, D has no async exports yet.

Two more fixes I needed along the way:

  • MoonBit: memory still grew on every async export call, even with no params. A finished task's run queue kept every coroutine it had run (Deque::pop_front leaves the slot filled, and each coroutine points back at the queue's lane), so the whole task stayed alive in a cycle. finish_waitableset now calls shrink_to_fit() on the run queue.
  • C#: an async export whose parameter is a record built in place generated invalid C#. The async method now gets a fresh parameter name and fully qualified parameter types.

The new runtime test async-many-arguments has a Rust runner and components in Rust, C, C#, MoonBit and Go. The runner makes 2000 calls to warm up, reads a heap mark, makes 2000 more and checks that the mark didn't move. It covers 9 strings, option/result payloads, one big record, calls cancelled while waiting, and a case with no block. That case uses 15 flat values, since Go can't compile an async export with 16. A supports-cancel export lets a component opt out of the cancel case. C# returns false because its async support doesn't handle a cancelled export yet, so the runner skips only that case for C#. There's also a codegen test with lists, a record and an async resource method; it joins the two async WITs that crates/test/src/go.rs expects to fail the c-shared link on stock Go.

I ran the test without the fix in Rust, C, MoonBit and C#, and it fails in all four:

  • Rust (locally): exact live bytes from a counting allocator, +144000 B over 2000 calls.
  • C (locally): memory.size, 4 → 6 pages.
  • MoonBit (locally): memory.size, 4 → 8 pages.
  • C# (in a Linux container): sbrk(0), +131072 B.

Go measures HeapAlloc after runtime.GC(); it runs only in CI, which has the patched Go.

MoonBit isn't in CI's language matrix at the moment, so I checked the MoonBit generator and runtime changes locally with moon 0.1.20261023 (moonc v0.10.17).

Fixes #1757

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Async exports never free the block of indirect parameters

1 participant