Skip to content

Add memory64 and table64 support to the Canonical ABI#624

Draft
adambratschikaye wants to merge 12 commits intoWebAssembly:mainfrom
adambratschikaye:abk/memory64
Draft

Add memory64 and table64 support to the Canonical ABI#624
adambratschikaye wants to merge 12 commits intoWebAssembly:mainfrom
adambratschikaye:abk/memory64

Conversation

@adambratschikaye
Copy link

Parameterize the Canonical ABI to handle 32-bit or 64-bit memory addresses and table indices. This is done by adding two new fields to LiftOptions to indicate if the memory64/table64 feature is being used in a core module.

Parameterize the Canonical ABI to handle 32-bit or 64-bit memory
addresses and table indices. This is done by adding two new fields to
`LiftOptions` to indicate if the `memory64`/`table64` feature is being
used in a core module.
Copy link
Collaborator

@alexcrichton alexcrichton left a comment

Choose a reason for hiding this comment

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

Thanks so much for helping to pick this up! We chatted a bit on Zulip but for posterity I'll mention here about table-related index types how most of them want to remain 32-bits. I'll take a closer look once that's been passed over here, and in the meantime I skimmed over things and noted a few things here and there. I'm sure though that @lukewagner will have thoughts on this too!

(param $originalSize i32)
(func (param $originalPtr $addr)
(param $originalSize $addr)
(param $alignment i32)
Copy link
Collaborator

Choose a reason for hiding this comment

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

For this I might recommend making alignment have type $addr as well for consistency, and I think that matches the signature in Rust/C/etc as well.

| -------------------------- | ------------------------ |
| Approximate WIT signature | `func<T>(t: T) -> T.rep` |
| Canonical ABI signature | `[t:i32] -> [i32]` |
| Canonical ABI signature | `[t:$idx] -> [i32]` |
Copy link
Collaborator

Choose a reason for hiding this comment

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

This'll be a bit subtle, but this'll actually want to be a mapping of i32 as an argument (I talked with you a bit about this already, but the input here is a host-side index so always 32-bit), but the output here should be something variable. Resource "rep"s are generally pointers in linear memory so 64-bit components are going to want 64-bit storage values. In the component model resources are defined with (rep i32) and validation currently requires that i32 is the only one listed there, but this PR will relax that meaning that the resource type itself stores the lowered type which'll get plumbed here.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks, I've now changed it so that the types which are expected to be pointers to memory are allowed to be either i32 or i64 without forcing them to match the memory type (e.g. the return value here, or in context.{get,set}, or thread.new-indirect). This means we don't need to require the memory canonopt anywhere where it didn't previously exist.

Copy link
Collaborator

@alexcrichton alexcrichton left a comment

Choose a reason for hiding this comment

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

Looks good to me! I think this'll need minor adjustments over time but overall looks good 👍

Comment on lines +244 to +245
if opts.memory is None:
return 'i32'
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could this have an assert? This in theory shouldn't ever be called dynamically if memory is None

Comment on lines +3209 to +3211
* `memory` - this is a subtype of `(memory 1)`. In the rest of the explainer,
`PTR` will refer to either `i32` or `i64` core Wasm types as determined by the
type of this `memory`.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This'll technically want to say it's a subtype of memory 1 or memory i64 1 since those are separate types

feature (the "stackful" ABI), this restriction is lifted.
* 🔀 `callback` - the function has type `(func (param i32 i32 i32) (result i32))`
and cannot be present without `async` and is only allowed with
* 🔀 `callback` - the function has type `(func (param i32 i32 PTR) (result i32))`
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this one may remain a three i32s since they're all async-related codes/events/etc.

Copy link
Member

@lukewagner lukewagner left a comment

Choose a reason for hiding this comment

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

This looks great overall; thanks so much for all the work and adding tests too! There's only one relatively minor, but non-nit, suggestion below that I'm happy to discuss and then re-review based on what we decide.

Comment on lines +236 to +237
# A tuple consisting of the memory contents and the pointer type ('i32' or 'i64')
memory: Optional[tuple[bytearray, str]] = None
Copy link
Member

Choose a reason for hiding this comment

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

To avoid all the cx.opts.memory[0]s below (where it's a bit confusing to know what [0] means in isolation), could you perhaps have:

class MemInst:
  bytes: bytearray
  addrtype: Literal['i32', 'i64']
  def ptr_type(self):
    return self.addrtype
  def ptr_size(self):
    ...

and in the CanonicalABI.md desciption say that MemInst corresponds to https://webassembly.github.io/spec/core/exec/runtime.html#syntax-meminst?

def store_string_copy(cx, src, src_code_units, dst_code_unit_size, dst_alignment, dst_encoding):
dst_byte_length = dst_code_unit_size * src_code_units
trap_if(dst_byte_length > MAX_STRING_BYTE_LENGTH)
trap_if(dst_byte_length > max_string_byte_length(cx.opts))
Copy link
Member

Choose a reason for hiding this comment

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

My impression is that, to avoid interfaces that can only be implemented by 64-bit components we should institute a maximum length on strings and lists (dynamically guarded by trap_if() during lifting and assert()ed for incoming values during lowering) that is conservatively defined so that the worst-case byte-length always fits in an i32 and thus we'd never hit this trap_if(). Thus, if you want an interface that passes really-big values, you'd use a stream (or some custom scheme using resource types and methods) to chunk it up, which should often have performance benefits and would allow 32-bit components to have a fighting chance to implement.

Copy link
Author

Choose a reason for hiding this comment

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

For both lists and strings: Should we also trap when loading if the maximum lengths are exceeded? Just so the trap occurs closer to the source.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I think that's right; that way lowering can just assert() that the computed byte-length fits in an i32. This also means thinking carefully about what the lifting-side limit is, taking into account the worst-case for what can happen during lowering (e.g., lifting side has 32-bit pointers, lowering side has 64-bit pointers). I think we can also be somewhat conservative here, since limits can be relaxed over time (as long as they maintain the invariant that computed byte length fits in an i32).

trap_if(ptr != align_to(ptr, alignment(elem_type)))
trap_if(ptr + byte_length > len(cx.opts.memory))
byte_length = len(v) * elem_size(elem_type, cx.opts)
trap_if(byte_length >= (1 << (ptr_size(cx.opts) * 8)))
Copy link
Member

Choose a reason for hiding this comment

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

Similar comment as with strings applies here too

Comment on lines +1262 to +1263
def utf16_tag(opts):
return 1 << (ptr_size(opts) * 8 - 1)
Copy link
Member

Choose a reason for hiding this comment

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

If we agree on my comment below on store_string_copy(), then I think the tagged code units might stay as-is as an i32 value.

OwnType(rt),
OwnType(rt)
])
for _ in [0]:
Copy link
Member

Choose a reason for hiding this comment

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

Could this for _ in [0]: line be removed and the following lines un-indented?

Copy link
Author

Choose a reason for hiding this comment

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

Sorry I'd actually meant to fix this before and forgot to push the changes.

Comment on lines +416 to +419
The thread-local storage array's length is currently fixed to contain exactly 2
`i32`s or `i64`s with the goal of allowing this array to be stored inline in
whatever existing runtime data structure is already efficiently reachable from
ambient compiled wasm code. Because module instantiation is declarative in the
Copy link
Member

Choose a reason for hiding this comment

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

Pairing with my comment in Explainer.md, I think the way we should semantically define this (which aligns with what the Python code in this PR implements) is that thread-local storage is 2 i64 values (always), and if you context.get i32, it ignores the high 32 bits and if you context.set i32, it clears the high 32-bits (incidentally, like x64 does for 32-bit instructions) which is observable if you mix them.

Comment on lines +439 to +441
When [wasm-gc] is integrated into the Canonical ABI, `context.{get,set}` will be
relaxed so that these integral context values can serve as indices into
guest-managed tables of typed GC references.
Copy link
Member

Choose a reason for hiding this comment

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

Can you remove "context.{get,set} will be relaxed so that" (as in: there isn't a plan to change context.{get,set} for wasm-gc)?

```
where `$f` is the index of a future (not a pointer to one) while while
`$out-ptr` is a pointer to a linear memory location that will receive an `i32`
`$out-ptr` is a pointer to a linear memory location that will receive an `i32`
Copy link
Member

Choose a reason for hiding this comment

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

Could you revert this and the next hunk?

Comment on lines +3210 to +3211
of the explainer, `PTR` will refer to either `i32` or `i64` core Wasm types
as determined by the type of this `memory`.
Copy link
Member

Choose a reason for hiding this comment

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

Instead of introducing this PTR convention which, iiuc, is only used in the next bullet and then once near stream_event, could do something sensible for the next bullet and then expand the use of PTR near stream_event to say something along the lines of "... the addrtype of the memory immediate..."?

@@ -775,8 +785,8 @@ class BufferGuestImpl(Buffer):
def __init__(self, t, cx, ptr, length):
trap_if(length > Buffer.MAX_LENGTH)
Copy link
Member

Choose a reason for hiding this comment

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

This PR leaves Buffer.MAX_LENGTH at 228-1, which I think is a reasonable choice and can been backwards-compatibly relaxed later if need be, but it might be good to call out here, and in the descriptions for canon_stream_{read,write} that the buffer length limit does not vary based on pointer size.

adambratschikaye and others added 6 commits March 24, 2026 10:47
Co-authored-by: Luke Wagner <mail@lukewagner.name>
Co-authored-by: Luke Wagner <mail@lukewagner.name>
Co-authored-by: Luke Wagner <mail@lukewagner.name>
Co-authored-by: Luke Wagner <mail@lukewagner.name>
Co-authored-by: Luke Wagner <mail@lukewagner.name>
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