Skip to content

RFC: Externref lang item for Wasm targets - #3987

Open
guybedford wants to merge 4 commits into
rust-lang:masterfrom
guybedford:externref-lang-item
Open

RFC: Externref lang item for Wasm targets#3987
guybedford wants to merge 4 commits into
rust-lang:masterfrom
guybedford:externref-lang-item

Conversation

@guybedford

@guybedford guybedford commented Jul 29, 2026

Copy link
Copy Markdown

This RFC proposes a new externref lang type (core::arch::wasm32::externref, feature(wasm_externref)): an opaque, unforgeable reference to a WebAssembly host value, lowering to the Wasm externref reference type in function signatures.

Following the semantics of Clang's __externref_t (shipped since Clang 15), it is a bare-position-only type, legal solely as the top-level type of function parameters, return values and local bindings and enforced at type-check time. This allows host references (e.g. JavaScript values) to be marshalled from one foreign call to another directly enabling better performance and interoperability between Wasm ecosystem crates.

Rendered

let r: &externref = &v; // ERROR: no references
let o: Option<externref> = ...; // ERROR: no generic arguments
let a: [externref; 2] = ...; // ERROR: no arrays
let f = |x: externref| x; // ERROR: no closures

@programmerjake programmerjake Jul 29, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why forbid externref in closure arguments/return values? forbidding it in closure captures is reasonable since you can't store it in structs but a capture-less closure should behave the same as a function -- where externref is allowed.

View changes since the review

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

You're correct that this could work mechanically, but it is a consequence of the blanket generics ban - FnOnce<Args> has Args = (externref, ...) which is banned as a generic externref type.

Note that my original version of this proposal only allowed externref in extern block foreign function definitions and extern "C" fn exports. That could still be a possible reframing of this proposal to avoid the complexity of arbitrary function scenarios.

@clarfonthey clarfonthey Jul 29, 2026

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.

My immediate gut feeling toward this is that simply having the type "opt out" of the type system is a hammer that feels like it will lead to loads of annoying hacks, but the amount of restrictions on externref feel like they wouldn't be possible to cover via other means, which is a compelling reason to not support it.

Essentially, listing all the requirements:

  • Cannot be referenced (no &-refs, or even &raw-refs)
  • No defined layout (similar to extern type)
  • No Type ID (!Any)
  • Cannot be moved onto the stack (!Move, except…)
  • Can be arguments or return values of C or Rust ABIs

Note that stuff like "never instantiated as a generic" could be useful as lints, but as type-system guarantees they seem fragile.

Note that being ABI-only have interesting consequences, like you could define repr(transparent) structs with them, but it would effectively be stuck with whatever struct you used at the ABI level, based upon what you've defined your extern fns as, which could be helpful.

Basically, I think that it's a steep bar to potentially satisfy all these requirements with explicit type-system details but it feels a lot better than just "this sidesteps the type system entirely" which is gonna lead to so many awful hacks.

View changes since the review

@Nadrieril Nadrieril Jul 29, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also, without a lot more opsem work:

  • Cannot be the type of a struct/enum field (except maybe transparent structs)

@clarfonthey clarfonthey Jul 29, 2026

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.

So, I kind of felt that would be implicit due to "no defined layout" but we can add it explicitly too. (I guess that without layout it could still technically be bolted onto the end of a struct but I'm not sure extern types allow that, especially if alignment is undefined.)

Comment on lines +11 to +21
## Motivation
[motivation]: #motivation

WebAssembly applications embedded in JS environments need to interoperate with foreign JavaScript references.

In the Rust Wasm ecosystem today, toolchains face a number of hard limitations with regard to externref interoperability:

- **Third-party Wrappers are Required**: Without native support in Rust, a third-party wrapper technique must be used instead. Typically, this involves creating a WebAssembly table, inserting the `externref` into that table, then using a numeric index to represent all foreign objects (exposed as `JsValue` in wasm-bindgen). Projects like wasm-bindgen are forced to fill this for functionality that could be in core.
- **Interoperability is Harmed**: A consequence of third party wrappers being required is harming interoperability. Sharing any JS value between separate Rust crates requires a shared representation of the table. wasm-bindgen's `JsValue` does not interoperate with other custom table wrapping systems. This in turn makes specific toolchain foreign wrappers "viral" and results in toolchain silos for all `wasm-*` targets, harming the entire Rust Wasm ecosystem.
- **Performance**: All operations on foreign references require multiple table accesses or updates, resulting in a performance cost that adds up over hot paths. Explicit drops are needed for all values from the table instead of being able to rely on implicit handles.
- **Optimization**: Direct use of `externref` allows more advanced static analysis of Wasm code enabling more optimization opportunities both for optimizers and V8. Without this direct codegen all usages as indirect table references hinder these opportunities for optimization.

@Jules-Bertholet Jules-Bertholet Jul 29, 2026

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.

For a proposal like this one, if you want the slightest chance of acceptance, you cannot afford to skimp on this section or the Rationale and Alternatives.

First, do not assume everyone in your audience has more than passing familiarity with WASM, or any idea what an externref is. You will need to explain this in the RFC itself. RFC 3884 is a good example of providing background for a niche target feature.

Second, to convince the lang team to accept an invasive and controversial change like this one, you will need to, at minimum, prove that nothing less can meet the needs of WASM users. You cite two major motivations; you should elaborate on both:

  • Interoperability and fewer custom wrappers: What are the specific patterns that break because of lack of interoperability, that would become possible with this proposal? And in the Rationale, why can't interoperability be achieved another way, e.g. standardizing on a common table system (similar to the C ABI and other tool conventions)? Ideally, give examples from specific real-world projects.
  • Performance and optimization: What are the specific usage patterns you anticipate for this feature (taking into account the many limits you impose on how externref can be used)? And in the Rationale, why is this proposal the only reasonable way to meet the performance requirements of those specific patterns? Ideally, you should cite benchmarks from a real-world project.

View changes since the review

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.

Note that whether or not this RFC ends up accepted in something like its current form, none of this work will be wasted. By fully describing the problem space, you enable everyone working on Rust to consider how what they are working on or thinking about intersects with Wasm's specific needs, and the end result will be a language that better supports them.

Comment on lines +139 to +144
## Prior art
[prior-art]: #prior-art

- Clang / LLVM `__externref_t` (since LLVM 15): identical position semantics, with this RFC as a port of its rules to Rust's type system. Emscripten supports it in `EM_JS` and exports.
- wasm-bindgen externref mode: post-link signature rewriting via walrus, as motivating precedent for doing it in the language.
- Wasm-GC languages (MoonBit, Kotlin/Wasm): expose host references as reference-typed values in FFI signatures directly, including storing them in language-level aggregates.

@Jules-Bertholet Jules-Bertholet Jul 29, 2026

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.

Please elaborate here as well.

  • Clang / LLVM __externref_t: What was their motivation for doing this? Are people using and enjoying this feature despite the restrictions on where __externref_t can appear? Did they face any unexpected challenges, and how did they overcome them? Link to their proposals and documentation.
  • "wasm-bindgen externref mode: post-link signature rewriting via walrus": What is this? Why is it "motivating precendent"?
  • Wasm-GC languages: So, is this feature necessary to do FFI with those languages on WASM? If so, that's great motivation! Make it part of your case, don't be shy about it. How is "storing [externref] in language-level aggregates" represented in FFI?

View changes since the review

- Restricted usefulness without storage. The bare type alone supports only transient marshalling, while real applications always need table-backed wrappers. This requires `global_asm!` (asm_experimental_arch) or future table support. The counterargument, validated by the prototype is that it is the optimal primitive to build table semantics from.
- Ecosystem generics friction. `fn foo<T>(t: T)` cannot accept externref. This is inherent (it is the point of the restriction), but users will encounter it.

## Rationale and alternatives

@Darksonn Darksonn Jul 29, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could we change wasm to make this type less special?

You've explained that the table representation is currently bespoke, which causes interoperability problems. But why can't wasm just pick one and standardize it on the wasm side? Then it'd no longer be a bespoke representation, but just the standard ABI and we can build that into the language and support it as a completely normal type with integer size.

View changes since the review

@guybedford guybedford Jul 29, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is covered in the future possibilities section briefly but I could expand on it further.

Basically, the shared table would support &externref as the storable variant as an additional feature (__externref_t* in llvm/llvm-project#201466) This layers onto the base externref support though which itself keeps all the strong guards defined by this RFC.

So the design of this externref remains a requirement even when &externref exists - none of the work in this proposal is wasted and the layering works out naturally.

If we made externref just an alias for __externref_t* ignoring support for __externref_t then that would force all FFI values to be table indices, which is exactly the situation we are trying to get away from here.

If we made externref a magical alias that picks one or the other representation depending on usage position, that would break a whole bunch more invariants in having a far more complex Heisenberg type.

Comment on lines +88 to +89
## Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

@RalfJung RalfJung Jul 29, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The semantics of this RFC cannot be supported by the Abstract Machine that we have developed so far to talk about Rust. That's a big setback since that Abstract Machine is needed for us to confidently answer questions like "is this MIR transform correct" or "are we correctly translating MIR to LLVM IR". In other words, as-is this RFC robs us of the ability to precisely reason about MIR, which is inacceptable. This isn't even properly discussed as a drawback, and no attempt is made at sketching what the Abstract Machine for Rust could look like with externref. You can look at MiniRust as a basis if you want to get an idea for what an Abstract Machine for current Rust could look like. (The RFC talks about the fact that this breaks type system assumptions, but that's only a small fraction of the problem. We've had RFCs that break type system assumptions before. This RFC breaks Abstract Machine assumptions. We've never had an RFC anything like this.)

Usually the approach to represent "external resources" in a language like Rust is something similar to file descriptors: a number into a table that is managed outside the realm of the language and that we can only talk about indirectly. The RFC mentions tables but only as "third-party" options. But a table does not have to be third-party, the intrinsics required for that could be in libcore. Given the gigantic cost of the proposed solution, the RFC needs to spend a comparable gigantic amount of space and effort explaining why the table solution, even if it became a first-party concept with good language and library integration, is so bad that we have to undertake the biggest change of the language since Rust 1.0 in order to avoid it.

View changes since the review


In `extern "C"` (and other ABIs), `externref` passes as a single Wasm externref value. It is FFI-safe by construction. improper_ctypes treats it as a known-good type. Variadic positions are rejected (no memory representation).

## Drawbacks

@RalfJung RalfJung Jul 29, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There 's a major drawback that's not discussed enough IMO: none of the existing language and library ecosystem works for this, meaning it will be a major pain to do anything with these types. You cannot return them from Result functions, you cannot use them in async blocks/functions, you cannot use == on them, you cannot newtype them... this RFC is proposing to introduce a fundamentally new kind of thing to Rust only to then make that thing a complete nightmare to use for anything.

The result of this will not feel like "Rust supports externref now". It will feel like "externref has been bolted on to Rust in a poorly integrated unergonomic way".

View changes since the review

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It's an FFI feature primarily, yes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Would it help if we formally restricted externref to only extern "C" and extern block functions? That would still provide the major benefit IMO.

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.

I don't think that makes a difference. As long as values of this type are passing through Rust code, we need a full semantics to understand what is happening.

It's an FFI feature primarily, yes.

As I've suggested elsewhere, please please provide a full fleshed-out example so that those of us who don't know Wasm can fully grok your use-case. (Take all the time you need, don't rush it)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You could skip all of the language changes if externref naturally decayed to an index within a table when it needs to be stored to memory. This needs runtime support. Similar to panic handlers, the compiler would depend on externally provided table allocation and free functions.

Otherwise I don't think this RFC will make any reasonable progress.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You could skip all of the language changes if externref naturally decayed to an index within a table when it needs to be stored to memory.

I think this is the third time that something like that was proposed -- I proposed it on Zulip. I do think that having the Rust type be an index into a table except when passed by value in function args/return values (maybe limited to extern "C") is a good idea, and that LLVM can help with optimizing to remove redundant table allocations/reads/writes.

@RalfJung RalfJung Jul 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would it help if we formally restricted externref to only extern "C" and extern block functions? That would still provide the major benefit IMO.

One can define and call extern "C" functions from Rust so on its own that doesn't really help.

I think this is the third time that something like that was proposed -- I proposed it on Zulip. I do think that having the Rust type be an index into a table except when passed by value in function args/return values (maybe limited to extern "C") is a good idea, and that LLVM can help with optimizing to remove redundant table allocations/reads/writes.

Yeah that makes sense: have this behave like a normal type with a special ABI. ABIs already involve special concepts we cannot represent in Rust, like registers. Having a type that's a table index normally but a bare externref on the ABI will stretch the limits of our already overstretched ABI handling in rustc, but at least that is "just" an implementation concern.

In the Rust Wasm ecosystem today, toolchains face a number of hard limitations with regard to externref interoperability:

- **Third-party Wrappers are Required**: Without native support in Rust, a third-party wrapper technique must be used instead. Typically, this involves creating a WebAssembly table, inserting the `externref` into that table, then using a numeric index to represent all foreign objects (exposed as `JsValue` in wasm-bindgen). Projects like wasm-bindgen are forced to fill this for functionality that could be in core.
- **Interoperability is Harmed**: A consequence of third party wrappers being required is harming interoperability. Sharing any JS value between separate Rust crates requires a shared representation of the table. wasm-bindgen's `JsValue` does not interoperate with other custom table wrapping systems. This in turn makes specific toolchain foreign wrappers "viral" and results in toolchain silos for all `wasm-*` targets, harming the entire Rust Wasm ecosystem.

@RalfJung RalfJung Jul 29, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

wasm-bindgen's JsValue does not interoperate with other custom table wrapping systems.

So let's define a standard table system for Rust that's used across the ecosystem, and let's design it in a way that it can be picked up by other languages as well (or can be adjusted to eventually achieve interop with other languages, once anyone else is interested).

View changes since the review

}
```

`externref` is severely restricted, because WebAssembly reference types cannot exist in linear memory. It may only appear as:

@juntyr juntyr Jul 29, 2026

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.

While the proposed design would mirror externref exactly, I think a non-zero cost wrapper could work better. The wrapper would still be special in that any stores or loads to memory would be translated into stores and loads into a Rust-owned table. As an optimisation, a pass could then look for cases where an externref in fact only passed as a function parameter and remove the table calls. In that case, Rust users could use an almost-normal opaque type, and cases that are written to comply with externref's usage rules would be optimised to compile to the same code as using this restricted proposal

View changes since the review

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

In this scenario, how would you distinguish between the opaque form, and actually getting the index of an &externref on an FFI boundary? Or would externref mean different things depending on usage site? How to categorize those cases and avoid &&externref being amgiuous?

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.

My idea would be that externref in Rust-only code is effectively a table index, but that at FFI boundaries (when defining or calling extern "C" functions), the actual WASM externref is passed, with table index conversions happening right at the boundary. A reasonably clever optimisation pass could then see that

unsafe extern "C" {
    fn create_ref() -> externref;
    fn use_ref(v: externref);
}

pub fn run() {
    unsafe {
        let r = create_ref();
        use_ref(r);
    }
}

which would lower to a externref -> table insert -> table extract -> externref sequence, could be replaced by just passing the externref directly.

## Prior art
[prior-art]: #prior-art

- Clang / LLVM `__externref_t` (since LLVM 15): identical position semantics, with this RFC as a port of its rules to Rust's type system. Emscripten supports it in `EM_JS` and exports.

@ds84182 ds84182 Jul 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If anything, I don't think Clang should be a role model. __externref_t is a non-standard extension to C and C++ implemented in Clang to handle a bespoke target feature. Other compilers, like GCC and MSVC, do not have this (or any type like this).

More generally, I do not think languages should change at such a core level to support something extremely niche like this. WebAssembly has so many oddities that have to be papered over due to fundamental issues in its design. Those oddities should be dealt with by the compiler and/or runtime.

View changes since the review

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.

9 participants