How far off am I with this mapping of Rust built-ins to the built-ins in Core Wasm and Wasm Components?
| Rust Types | Core Wasm | Wasm Components |
|---|---|---|
bool |
- | bool |
char |
- | char |
i8/i16/i32/i64/i128/isize |
-/-/i32/i64/-/i32 |
s8/s16/s32/s64/-/s32 |
u8/u16/u32/u64/u128/usize |
-/-/-/-/-/- | u8/u16/u32/u64/-/u32 |
f32/f64 |
f32/f64 |
f32/f64 |
str/&str/String |
-/-/- | -/-/string |
[T; N]/&[T]/Vec<T> |
-/-/- | -/-/list<T> |
(T, U, ...) |
- | tuple<...> |
Option<T> |
- | option<T> |
Result<T, E> |
- | result<T, E> |
enum (without/with data) |
-/- | enum/variant |
struct { pub x, pub y } |
- | record |
struct { x, y }: Drop (owned/borrowed) |
- | resource (own<T>/borrow<T>) |
fn(...) -> T |
funcref |
func |
async fn(...) -> T |
- | async func |
trait Logger/mod logger |
- | interface logger |
I don't know that much about core wasm, and whether for example we might ever get a native i8 equivalent type. So I'm trying to be as defensive as possible in the mappings here.
Also I feel the least confident in the func/async func/interface/resource mappings.
Also for some context here: for the Rust 2026 Wasm Component project goal we have "Experiment with #[repr(wasm)] and extern "wasm"" as a sub-goal. I've had someone volunteer to implement this, and they now have a basic hello-world compiling.
That looks mostly accurate to me, at least for components. Core wasm can have some updates (e.g. all integers are represented as wasm i32 or i64 except {i,u}128
for components though those analogs all seem roughly accurate, although [T; N] is more closely equivalent to list<T, N> which is an unstable feature at this moment (component-model-fixed-length-lists)
Alex Crichton said:
Core wasm can have some updates (e.g. all integers are represented as wasm
i32ori64except{i,u}128
Do you mean that core wasm will e.g. never gain a native i8 type, so we probably should just hard-code it to always be an i32?
Well I wouldn't say never, but I can at least say right now it's mapped to i32 (ish)
a table like this eschews a lot of details about how things are actually represented, for example in-memory a Rust i8 is still mapped to a single byte, but when it's "on the stack" that's basically an implementation detail of LLVM which can do whatever it wants. When it's a function parameter that's defined by the C ABI and it's not just i32 but it's also sign/zero extended according to the original source language type
yeah I get that
Maybe I'm missing something you're implying with this tho?
It's sort of a question of what this table is in the context of. For example are you classifying:
#[link(wasm_import_module = "something")]
extern "C" {
fn foo(some_type: SomeType);
}
you're changing SomeType, and seeing what pops out?
I'm very specifically trying to reason about what the representation of this at the ABI boundary; afaik that's separate from how LLVM ends up reasoning about those same values on stack compilation?
ok yeah for that core wasm follows this document
and for components it's a bit inverted where the chart here is sort of "this component model type is represented as this rust type in bindings generation"
as opposed to the core wasm direction of "this rust type is represented this way in core wasm ABI"
Do you reckon we need two ABIs here? One for core wasm (possibly versioned) and one for Components?
I'm saying "possibly versioned" because the doc states:
The current version of this ABI is 1.
Alex Crichton said:
It's sort of a question of what this table is in the context of. For example are you classifying:
#[link(wasm_import_module = "something")] extern "C" { fn foo(some_type: SomeType); }you're changing
SomeType, and seeing what pops out?
I'm still not sure I quite follow the question? I think you're asking whether this example is what I had in mind - and yeah, if you replace extern "C" with "extern "wasm" then that's pretty close!
e.g.
#[repr(wasm)]
struct SomeType(());
#[link(wasm_import_module = "something")]
unsafe extern "wasm" {
fn foo(some_type: SomeType);
}
Well, personally, I still have no idea what repr(wasm) would be. It basically doesn't make sense to me beyond the high level "it helps wasm interop". I don't think it makes any sense at the core wasm level because that's what the C abi is already for, and for components it kind of doesn't make sense because it needs to specifically be defined in terms of component types, and then the ABI of functions is dicated by the canonical ABI signatures
I might just be lost, and I might be misleading here too
Oh you're good; don't worry!
Actually, maybe this is a better example of what I'm thinking of:
#[repr(wasm)]
struct SomeType(Result<String, String>);
#[link(wasm_import_module = "something")]
unsafe extern "wasm" {
fn foo(some_type: SomeType);
}
repr(C) has no idea how to encode a Result or String. So we can't use those directly and we have to instead write code which converts Rust code to repr(C) code using the exact semantics that the component model expects.
My thinking was that #[repr(wasm)] would give us a built-in Rust way to say: "well, please just lay this out the way the component model would expect this to be laid out"
Extern blocks in Rust must be paired with repr attributes. If we omit them we get this safety warning:
Compiling playground v0.0.1 (/playground)
warning: `extern` block uses type `SomeType`, which is not FFI-safe
--> src/lib.rs:7:23
|
7 | fn foo(some_type: SomeType);
| ^^^^^^^^ not FFI-safe
|
= help: consider adding a `#[repr(C)]` or `#[repr(transparent)]` attribute to this struct
= note: this struct has unspecified layout
note: the type is defined here
--> src/lib.rs:3:1
|
3 | struct SomeType(());
| ^^^^^^^^^^^^^^^
= note: `#[warn(improper_ctypes)]` on by default
warning: `playground` (lib) generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.81s
does that make more sense?
I feel the reality of type mapping is more complex.
At the guest import level a string argument means &str, a string result is a Rust String. This inverts (sort of, because return types rely on cabi_post_f to release the "Rc") for guest exports.
This strange asymmetry caused by the guest host asymmetry and sandboxing motivated me to create the symmetric ABI between peers (without sandboxing).
Similarly for Vec and &[T]
Oh yeah, good point on the asymmetry between imports and exports. I hadn't really looked at that closely, thank you for catching that!
Btw, does by "much more complex" you don't meant "intractable" right?
Happy to iterate and sus out complexity. But if folks here think this cannot/should not be done, I'd also like to hear it.
I don't think fn() can map to funcref. An fn() is an index into a table. So returning an fn() back to rust would require allocating a new table entry, but there is no allocator for tables and afaik LLVM doesn't support modifying tables either.
All #[repr] still require each field to be assigned some offset that you can take a reference to, but for #[repr(wasm)] struct SomeType(String); the String would suddenly have to get an entirely different layout matching the component model, which is not valid. The best you can do is have a core::ffi::WasmComponentString type or something like that.
By the way if it has to match the component model, perhaps it should be called repr(wasm_component) and extern "wasm-component" or something like that? It is not representing core wasm types, but the component model canonical ABI.
All of that makes sense to me!
What im taking away from this is: what we probably actually want would be new the addition of new core::ffi types instead of repr(wasm)
That could potentially also be a way for us to add support for bitflags just for the component targets, without adding a general-purpose bitflags construct to Rust
I'll note that the component model type system is not fully compositional in the way that mapping from Rust types would expect: for example, one cannot borrow an enum that has a resource (== non-Copy struct) in one of its arms. I filed an issue and sketched out a generalization a few months ago; it entails seeing borrow<T> as a higher-order type constructor (as in Rust: & is conceptually a type-level function from Type to Type) rather than an argument modality (== second-class type).
All of that to say: while mapping very simple types may work out okay, I think there are deep mutual-expressability questions at the type system level that make this extremely difficult without potentially extending the component model (or alternately, having sharp cliffs where a type suddenly cannot be repr(wasm), in a way that is very surprising for Rust programmers)
Yosh Wuyts said:
Btw, does by "much more complex" you don't meant "intractable" right?
I think it is very worth trying this because the usability benefits are high. But reading these excellent answers above it won't be straightforward and might require additions on both sides (like list<T,N> and ffi::ComponentString).
Last updated: Jul 29 2026 at 05:03 UTC