Skip to content

feat(rust) add native link option - #1685

Open
BjornTheProgrammer wants to merge 2 commits into
bytecodealliance:mainfrom
BjornTheProgrammer:wit-native
Open

feat(rust) add native link option#1685
BjornTheProgrammer wants to merge 2 commits into
bytecodealliance:mainfrom
BjornTheProgrammer:wit-native

Conversation

@BjornTheProgrammer

Copy link
Copy Markdown

Addresses bytecodealliance/wit-bindgen#1062. Currently the Rust bindings stub with unreachable!() when compiling to wasm. This PR makes bindings usable on native targets, so people can test and run component code in ordinary native environments.

This is a new approach to what was initially done in #1565.

The reason this PR needs to exist is described by a comment in the original PR

This PR despite making the possible stubs very modular, suffers from a lot of issues, I'm going to close this in favor of another PR that I've been making. The idea is a bit different, and slightly more complex then just calling a host function, since that would require the host binary to be compiled with "-rdynamic" on linux, and it wouldn't even work on Windows and MacOS.

To solve this issue I've added a register function which can add a pointer to the host function. This fixes the above issue.

Testing

  • Codegen tests have been added.
  • End-to-end validation via native-wit hosting native plugins in Pumpkin.

The Pumpkin PR is interesting because the approach shows how one can make very minimal changes and get wasmtime and native runtimes working at the same time.

@alexcrichton

Copy link
Copy Markdown
Member

Could you address the question I had in the other PR? This seems like a lot to take on in terms of maintainership here because only codegen is tested, not any runtime parts, and since that comment the component model has continued to add features like async which I'm not sure how would map to native counterparts.

@BjornTheProgrammer

Copy link
Copy Markdown
Author

Sure! I'll respond to the previous comment.

Thanks for the PR, and I'm trying to page this all back in and understand the motivations/intentions here. Correct me if I'm wrong, but the high level idea is to be able to satisfy, on non-wasm platforms, wit-bindgen-generated bindings with a native implementation. Is that correct?

Yes. This has been demonstrated in the Pumpkin PR I shared. It allows for native and wasm targets to be compiled interchangeably with no code changes from the plugin developer.

If so, I'm second-guessing this a bit in the sense of why go through ABI-marshalling at all? For example if you're calling a WIT function it seems like it'd be best to just turn around and immediately call a native function rather than serializing to some ABI-based representation just to deserialize and call a host function. Does that make sense to fit in here at all? Or am I perhaps missing something? (I could very well be)

The reason a direct native call doesn't work here is that the guest is compiled separately from the host and loaded with dlopen. Rust has no stable ABI, so passing native types like String or Vec across that boundary, potentially across different rustc versions, or from a non-Rust host won't work. The canonical ABI is already implemented by wit-bindgen and tapping into that is easy. We wouldn't really have to "serialize/deserialize." Nothing is encoded to a buffer. Scalars pass as-is and strings cross as pointer+length, so simple calls are effectively direct calls already.

Reusing the existing ABI also keeps the change small. The existing codegen already emits the full ABI representation on native targets (the unreachable!() stubs sit after all of that), so this PR just swaps the stub for a dispatch through a registered function pointer and hex-encodes the symbol names. Only around +130 lines of actual generator code. Most of that is the register hooks, which exist so the host doesn't have to re-export every import symbol into the dlopen'd library (the -rdynamic problem) and can implement only the subset of imports it needs. And because it's the same underlying C ABI emission, async, resources, and every other WIT construct should work without a parallel code path. Which is why I didn't really see the need to test the runtime part, since whatever ABI works on wasm should just work with native.

A direct-call mode (traits with idiomatic signatures) is imaginable, but I don't think it fits wit-bindgen's shape. There's no language-neutral contract for it to target, so it would be a separate parallel bindings mode per generator, none of which could interoperate across the dlopen or language boundary. The canonical ABI is the one representation every generator here already shares. Which is what lets, e.g., a C++ host load a Rust plugin under this scheme. Theoretically this could be added to the other generators as well.

@tschneidereit

Copy link
Copy Markdown
Member

@cpetig I know you've done similar work, so it'd be great if you could weigh in here as well.

@cpetig

cpetig commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Wow, good to see growing interest in applying the component model to native binaries.

I have a long living fork of wit-bindgen at https://github.com/cpetig/wit-bindgen/ which adds

  • hex encoding of the colorful wasm-link names (as native symbols are more constrained on ":/[]" characters) - should become standardized in something akin to Add BuildTargets.md WebAssembly/component-model#378
  • removing the unreachable!
  • introducing a symmetric ABI mode which enables directly connecting exports to imports (needs one renamed when importing and exporting the same interface from the same module).
    • a matching test runner for native with symmetric ABI covering a large fraction of the runtime tests
    • this also covers streams and futures with a matching runtime library for POSIX.
    • a dll import linking stub generator which removes the need to link component dlls in dependency graph order
  • some more unrelated niceties (including a now unmaintained mode to connect native canonical components by a "host" mesh, wamr host code)

The symmetric ABI is what makes the real difference in usability as the (also mental) overhead for (maintaining) a host mode (mesh) code generator is significant.

I guess we should have a discussion about standardizing native names in a BuildTarget.md, potentially about symmetric ABI and then merge our efforts. I simply dispersed myself in too many parallel activities over the past year to get it approved and merged upstream until now. Honestly the pain was too low to make this high priority.

@cpetig cpetig left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I feel that the amount of code changes can be reduced by making the native symbol name generation and target-conditional linker attributes the default behavior. This is how I would solve this.

Comment thread crates/guest-rust/src/lib.rs Outdated
/// // still does; use `type_section_suffix` to tell them apart. See
/// // `wit_bindgen_rust::Opts::link_native_symbols` for the full list of
/// // symbols a host can expect.
/// link_native_symbols: true,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(written before I saw that your code is likely the same when the native flag is turned on)

I think my approach of using conditional compilation to choose between both naming schemes at compilation time of identical generated code is preferable. See e.g. https://github.com/cpetig/wit-bindgen/blob/work-in-progress/crates/cpp/tests/symmetric_future/future/src/future_world.rs#L17-L24 for an example.

Comment thread crates/rust/src/interface.rs Outdated
/// `#`, `[` and `]` characters that canonical names contain. Names that
/// survive encoding unchanged (`$root` exports, for instance) are emitted
/// once with no `cfg` rather than twice.
fn core_export_symbols(&self, export_name: &str) -> Vec<(&'static str, String)> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ok, I see, this is additive, so the generated code is unchanged in the default case.

I will leave this decision to Alex, but for my branch I decided that it should become the default, simplifying the generation logic and making the flag unnecessary. (It only adds visual clutter to the generated code, no runtime effects)

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.

I pushed some changes that now make it the default as well.

@BjornTheProgrammer

Copy link
Copy Markdown
Author

@cpetig thank you for the review!

I hadn't realized you had a fork tackling this issue as well, and I would love to combine our work. We're already aligned on naming (the hex encoding this PR moves into wit-bindgen-core is the same scheme your C++ generator uses).

I've also taken your suggestion. Native symbol name generation and target-conditional linker attributes are now the default behavior, and the link_native_symbols option is gone entirely. Exports are emitted once with paired cfg_attr(target_arch = "wasm32", ...) export names. The one place I deliberately differ from your fork is imports. Instead of undefined externs, the old unreachable!() slot is filled with a dispatch through a _wit_bindgen_register(func: unsafe extern "C" fn(...)) hook. That keeps the default free for existing users (bindings crates still build and link everywhere on host targets. No import stub libraries needed, and calling an unregistered import aborts with a message, same as the stub did), and it's what makes runtime-discovered plugins work: a dlopen'd cdylib can't resolve import symbols against the host executable without -rdynamic on Linux, and can't on Windows at all, which is why I abandoned #1565. The hooks are convention-agnostic. They just carry a core-signature function pointer, so a symmetric mode could register through the same mechanism later.

On the symmetric ABI, I had considered something similar for this PR, but I was worried about whether something requiring maintenance at that scale would be accepted, which is why I settled on the existing wasm32 ABI. It also helped that wasmtime's bindgen existed as a reference for making a fully working native host runtime (native-wit). A lot of our changes are shared regardless. Would it make sense to land this PR as the common substrate (standardized names, no stubs, a portable dispatch mechanism), and then adopt the symmetric ABI as an opt-in mode once the BuildTargets.md discussion stabilizes? Happy to help draft the naming section there. The encoding and hook naming in this PR could serve as input.

@BjornTheProgrammer

Copy link
Copy Markdown
Author

Hmm seems like a single unit test fails due to a naming conflict after my changes. I just had an idea how to fix this that might make naming resolution never a problem and also simplify the rest of the code. I'll probably push some changes tomorrow, but for now I would love to hear your input!

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.

4 participants