Skip to content

fix: Handle Wasm ValueTypes returned by imports - #141

Merged
arcjet-rei merged 14 commits into
mainfrom
phated/host-import-return-value
Oct 3, 2026
Merged

arcjet-rei merged 14 commits into
mainfrom
phated/host-import-return-value

Conversation

@blaine-arcjet

@blaine-arcjet blaine-arcjet commented Dec 18, 2025 •

Copy link
Copy Markdown
Contributor

This reworks Wasm imports to build functions with explicit Wasm ValueTypes based on the Wasm signature provided by the WIT tooling. This requires that we use the stack provided by Wazero but that makes using the various api.Decode* and api.Encode* functions work correctly on Wasm imports.

I believe this resolves some issues introduced by my changes in #96


Changes made to finish this pull request

blaine-arcjet has left Arcjet, so this pull request was completed with Claude Code. The last two commits replace the approach described above. The earlier commits stay in the branch history.

Why the approach changed. The original commits moved import handling out of func.rs into a separate imported_func.rs and built each host function from wazero's stack. Since then, main has kept import handling in func.rs behind Direction::Import and added records, variants, options, enum lifting and indirect parameters there, none of which the separate file has. At this branch's previous head, every scalar import return type except u32, bool and string still reached todo!("TODO(#9): handle return type"). Merging main into the branch therefore takes main's func.rs in full and removes imported_func.rs, and the fix is built on main's structure.

What was wrong on main. Gravity registers each host import with wazero's WithFunc, which reads the function's Wasm signature from its Go types: uint32 for i32, uint64 for i64, float32 and float64 for the floats. The scalar lowering instructions produced the uint64 encoding that api.Function.Call expects instead. An import returning s32, s8, u8, s16, u16, f32 or f64 generated Go that did not compile, and one returning s64 or char made gravity panic.

What this changes.

  • The integer lowering instructions emit uint32(x) and uint64(x). Go's conversion sign-extends signed values, so once CallWasm widens the result to uint64, exports receive the same value api.EncodeI32 produced.
  • The float lowering instructions pass the value through for imports and keep api.EncodeF32/EncodeF64 for exports, because uint64(float) converts the number rather than copying its bits.
  • S64FromI64, I32FromChar and CharFromI32 are implemented, and WIT char maps to Go rune. Lifting a char rejects surrogates and values above U+10FFFF.

Tests. The get-u32 round-trip test from the original commits is kept. The iface-method-returns-string example gains a host import and an export for each scalar type, with Go tests that send boundary values (minimum, maximum, -1, ±Inf, U+10FFFF) through the guest and back.

Co-authored-by: 零Rei 212411920+arcjet-rei@users.noreply.github.com

🤖 Generated with Claude Code

@blaine-arcjet
blaine-arcjet force-pushed the phated/host-import-return-value branch from 6ab6e43 to 1d1a789 Compare December 19, 2025 18:29
@blaine-arcjet
blaine-arcjet force-pushed the phated/host-import-return-value branch from 5b3a384 to 4c7e1ce Compare January 2, 2026 21:23
@blaine-arcjet
blaine-arcjet marked this pull request as ready for review January 2, 2026 21:27
@blaine-arcjet
blaine-arcjet requested a review from a team as a code owner January 2, 2026 21:27
@blaine-arcjet blaine-arcjet changed the title fix: Return WasmI32 types from imported functions fix: Handle WasmI32-encoded ValueTypes returned by imports Jan 2, 2026
@blaine-arcjet blaine-arcjet changed the title fix: Handle WasmI32-encoded ValueTypes returned by imports fix: Handle Wasm ValueTypes returned by imports Jan 2, 2026
@blaine-arcjet
blaine-arcjet force-pushed the phated/host-import-return-value branch from 4c7e1ce to 538bb51 Compare January 5, 2026 16:04
@blaine-arcjet
blaine-arcjet marked this pull request as draft January 5, 2026 16:20
@blaine-arcjet

Copy link
Copy Markdown
Contributor Author

This caused some other problems in codegen so I'm drafting it.

blaine-arcjet and others added 13 commits January 5, 2026 11:06
Take main's import codegen in full. This branch moved import handling out
of func.rs into a separate imported_func.rs; since then main has kept
imports in func.rs behind Direction::Import and added records, variants,
options, enum lift and indirect parameters there, none of which the copy
has. The import return types this branch set out to fix are addressed on
top of main's structure in the following commits instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A host import is registered with wazero's WithFunc, which takes the
function's Wasm signature from its Go types: uint32 for i32, uint64 for
i64, float32 for f32 and float64 for f64. The scalar lowering
instructions produced the uint64 encoding api.Function.Call expects
instead, so an import returning s32, s8, u8, s16, u16, f32 or f64
generated Go that did not compile, and one returning s64 or char made
gravity panic.

- I32FromS32, I32FromS16, I32FromU16, I32FromS8 and I32FromU8 emit
  uint32(x), and I64FromS64 emits uint64(x). Go's conversion
  sign-extends signed values, so uint64 of the result is the same value
  api.EncodeI32 produced when CallWasm passes it to an export.
- CoreF32FromF32 and CoreF64FromF64 pass the float through for imports
  and keep api.EncodeF32/EncodeF64 for exports, since uint64(float) is a
  numeric conversion rather than a bit copy.
- Implement S64FromI64, I32FromChar and CharFromI32, and resolve WIT
  char to Go rune. Lifting a char panics on a surrogate or a value
  above U+10FFFF, as the canonical ABI requires.

The iface-method-returns-string example gains an import and export per
scalar type, with Go tests that round-trip boundary values through the
guest, alongside the get-u32 case from this pull request's earlier
commits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@arcjet-rei
arcjet-rei marked this pull request as ready for review October 3, 2026 01:10
@arcjet-review arcjet-review Bot added the needs review Awaiting human review label Oct 3, 2026

@arcjet-rei arcjet-rei left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vale @blaine-arcjet! Thanks for putting this together, and I hope this implementation meets with your approval.

@arcjet-review arcjet-review Bot removed the needs review Awaiting human review label Oct 3, 2026
@arcjet-rei
arcjet-rei merged commit 2dc1000 into main Oct 3, 2026
4 checks passed
@arcjet-rei
arcjet-rei deleted the phated/host-import-return-value branch October 3, 2026 01:29
arcjet-rei added a commit that referenced this pull request Oct 3, 2026
* fix: store and load every value type at its own width

Values that pass through guest memory (record fields, list elements, a
host import's result, an export's indirect parameters) were written and
read with the wrong width or type:

- F32Store and F64Store called WriteUint64Le. On the import side, where
  #141 made floats plain float32 and float64, the generated
  Go no longer compiled; on the export side, F32Store wrote 8 bytes for
  a 4-byte value and overwrote whatever followed it. Imports now use
  WriteFloat32Le/WriteFloat64Le and exports write the IEEE bits at the
  value's width. F32Load read 8 bytes as well; it now reads 4.
- I32Store8 only accepted 0 and 1 and panicked otherwise, so a u8, s8,
  enum or option<u8> payload above 1 panicked. It now stores the low
  byte of any value.
- I32Store16, I64Store, I32Load8S, I32Load16U and I32Load16S were
  todo!(), so u16, s16, u64, s64 and s8 values in memory crashed
  gravity. They are implemented, with the loads sharing a read_memory
  helper for the failed-read check.
- Each store converts its operand explicitly, and the integer lifts use
  Go conversions instead of api.DecodeI32/DecodeU32, so they accept the
  uint64 CallWasm returns as well as the narrower values loads produce.
  The float lifts pass Go floats through on the import side.

The new memory example passes a record of every scalar type (17
flattened values, so it always travels through memory) to an export,
back from an export, to a host import and back from it, plus a 68-byte
record whose trailing f32 sits directly before a string allocation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: round-trip IEEE special floats through memory

Send +Inf, -Inf, NaN and -0 as the f32 and f64 fields through an export's
result, a host import's parameter and a host import's result. The
comparison treats any NaN as equal to any NaN, since reflect.DeepEqual
cannot, and checks the sign so -0 is not mistaken for +0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

3 participants