Skip to content

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

Open
Aditya1404Sal wants to merge 4 commits into
golemcloud:wit-bindgen-golem-1.6from
Aditya1404Sal:free-async-export-params
Open

Aditya1404Sal wants to merge 4 commits into
golemcloud:wit-bindgen-golem-1.6from
Aditya1404Sal:free-async-export-params

Conversation

@Aditya1404Sal

@Aditya1404Sal Aditya1404Sal commented Oct 26, 2026 •

Copy link
Copy Markdown

Part of GOL-987.

This brings the fix from bytecodealliance#1758 to this branch. It replaces the Rust-only fix that this PR had before. The upstream issue is bytecodealliance#1757.

Problem

When the parameters of an export flatten to more than 16 core values, the caller writes them into a block of the callee's linear memory. The caller allocates this block with the callee's cabi_realloc. The callee must free it.

The generated code frees this block only for sync exports. For async exports, nothing frees it, so each call leaks the block. In Golem, each method call of a Rust agent goes through the async export golem:agent/guest.invoke. Each call leaks 136 bytes, and linear memory does not shrink.

Change

wit-bindgen-core cannot free the block of an async export itself:

  • A free before the call is too early. C and Rust read some arguments only at the call.
  • A free after the call is too late. In Rust it comes after .await, so a cancelled call never reaches it. In C, C# and MoonBit it is in a different function, where the block pointer is not available.

So core now tells the generator about the block. Instruction::CallInterface has a new field, params_block: Option<(ArchitectureSize, Alignment)>. Core sets it only for an async export whose parameters arrive in a block. Then the block pointer is the last operand, and core does not free the block. Sync exports do not change.

Each generator frees the block after it has read all arguments and before the task can wait:

  • Rust: between the creation of the future and the first .await. A call that is cancelled while it waits also frees the block.
  • C: after the user's function returns, before return ret;.
  • C#: after var task = XAsync(...);. At that point the arguments are managed copies.
  • MoonBit: each argument goes into a local, then the block is freed, then the call runs. The generator first reserves the name of the export, so that a local cannot hide it, for example for an export named arg.
  • Go: removes the extra operand and frees nothing. The Go runtime owns that memory, and the existing Unpin() releases it.

The change also fixes two errors in the C# generator for async exports. Both gave invalid C#:

  • Where the parameter name goes, it wrote the lifted value when that value is an expression, for example for a bool or a record. Now each parameter has a simple name.
  • It wrote the payload type of a stream<T> or future<T> parameter without its namespace. In the interop class, that name can refer to a different class or to no class. Now the type is fully qualified.

Instruction is public API. A Bindgen outside this repo that matches on CallInterface must add .. to its pattern. If it generates async exports, it must remove the extra operand and free the block.

Differences from the upstream PR:

  • The upstream PR also changes the MoonBit async runtime, because there a finished task's run queue kept its coroutines alive in a cycle. The MoonBit scheduler on this branch has no such cycle, and the leak test passes for MoonBit without that change. So it is not included.
  • This branch has no D generator.
  • crates/test/src/go.rs adds the new codegen test to the list of tests that fail to link on stock Go, as upstream does. The list on this branch has no --pkg-name case.

Tests

The runtime test async-many-arguments is the same as upstream. It has a Rust runner and test components in Rust, C, C#, MoonBit and Go. For each case, the runner makes 2000 calls, reads a heap mark, makes 2000 more calls, and checks that the mark did not change. The cases are:

When core never announces the block, the test fails in Rust (144,000 more live bytes after 2000 calls), C (4 to 6 pages), MoonBit (4 to 8 pages) and C#. With this change, it passes in all five languages.

Other results:

  • Runtime: Rust and C, 167 pairs. Rust, C and C#, 247 pairs. Rust, C and Go, 289 pairs (patched Go 1.27.1). Rust and MoonBit, 125 pairs.
  • Codegen: Rust 735, C 420, Go 105 (patched Go). On stock Go, the new codegen test fails to link without the go.rs entry and is an expected failure with it.
  • Crate tests, cargo clippy --workspace --all-targets -- -D warnings and cargo fmt -- --check pass.
  • For all codegen tests, the Rust generator output is the same as with the earlier Rust-only fix. So the Golem numbers below still apply.

C# and Go ran on Linux arm64. For C# with WASI SDK 34, the test tool needs the arm64 compiler package and -Wl,--no-stack-first; this PR does not change the test tool for that. MoonBit ran with moon 0.1.20261023, after moon fmt converts the generated moon.mod.json.

These tests fail with and without this change: C++ and C# codegen outlined-lowering.wit and outlined-resources.wit, C++ runtime fixed-length-lists, and MoonBit codegen and runtime variants. The numbers runner does not compile with Rust 1.99 (deprecated min_value).

Measured in Golem

A local Golem server ran a Rust agent with a counting allocator. Its method returns the live heap bytes.

Run Before After
6 calls from the CLI 1864, 2000, 2136, 2272, 2408, 2544 1594 on each call
2,000 calls from another agent 2,696 to 274,696 bytes 1,636 bytes at each sample
Linear memory after these calls 17 to 22 pages 17 pages

🤖 Generated with Claude Code

@Aditya1404Sal
Aditya1404Sal marked this pull request as ready for review October 27, 2026 08:15
@Aditya1404Sal
Aditya1404Sal marked this pull request as draft October 27, 2026 08:52
@Aditya1404Sal Aditya1404Sal changed the title rust: free the parameter block of async exports before the first await Free the parameter block of async exports in every generator Oct 28, 2026
@Aditya1404Sal
Aditya1404Sal marked this pull request as ready for review October 29, 2026 14:57
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.

1 participant