From f4db0a969c2a6631aefb350d7bc25ff4f900cc67 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Tue, 7 Jul 2026 18:27:05 -0700 Subject: [PATCH 1/2] Emit the emscripten entry point as `__main_argc_argv` The `wasm32-unknown-emscripten` target uses the argc/argv form of main but emitted the entry point under the raw name `main`. That links on the default entry path (emscripten calls it via `callMain`), but fails on entry paths that reference the mangled symbol directly from wasm. In particular `-sPROXY_TO_PTHREAD` links `crt1_proxy_main.o`, whose call chain bottoms out at `__main_argc_argv`, so linking a Rust binary fails with: ``` wasm-ld: error: crt1_proxy_main.o: undefined symbol: main ``` This sets `entry_name = "__main_argc_argv"` so the entry point is emitted under the C-ABI name emscripten's crt/libc expects, and then works in both cases. The JS-visible name of the entry point stays `_main`, since emscripten bridges `_main` to the wasm `__main_argc_argv` export internally (whereas it wouldn't for `___main_argc_argv`). --- compiler/rustc_codegen_ssa/src/back/linker.rs | 17 ++++++- .../spec/targets/wasm32_unknown_emscripten.rs | 7 +++ .../wasm-emscripten-main-symbol/main.rs | 1 + .../wasm-emscripten-main-symbol/rmake.rs | 45 +++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 tests/run-make/wasm-emscripten-main-symbol/main.rs create mode 100644 tests/run-make/wasm-emscripten-main-symbol/rmake.rs diff --git a/compiler/rustc_codegen_ssa/src/back/linker.rs b/compiler/rustc_codegen_ssa/src/back/linker.rs index 8c20db5791774..2eac97f3c4a47 100644 --- a/compiler/rustc_codegen_ssa/src/back/linker.rs +++ b/compiler/rustc_codegen_ssa/src/back/linker.rs @@ -1286,9 +1286,24 @@ impl<'a> Linker for EmLinker<'a> { self.cc_arg("-s"); + // Emscripten exposes the program entry point under the JS name `_main` + // regardless of the underlying wasm symbol (which is `__main_argc_argv` + // to match Clang's C ABI), bridging the two internally. So the entry + // symbol must be requested as `_main` here rather than as a `_`-prefixed + // form of its wasm name. + let entry_name = self.sess.target.entry_name.as_ref(); let mut arg = OsString::from("EXPORTED_FUNCTIONS="); let encoded = serde_json::to_string( - &symbols.iter().map(|sym| "_".to_owned() + &sym.name).collect::>(), + &symbols + .iter() + .map(|sym| { + if sym.name == entry_name { + "_main".to_owned() + } else { + "_".to_owned() + &sym.name + } + }) + .collect::>(), ) .unwrap(); debug!("{encoded}"); diff --git a/compiler/rustc_target/src/spec/targets/wasm32_unknown_emscripten.rs b/compiler/rustc_target/src/spec/targets/wasm32_unknown_emscripten.rs index 2104286ec8684..d585808b94fff 100644 --- a/compiler/rustc_target/src/spec/targets/wasm32_unknown_emscripten.rs +++ b/compiler/rustc_target/src/spec/targets/wasm32_unknown_emscripten.rs @@ -29,6 +29,13 @@ pub(crate) fn target() -> Target { crt_static_default: true, crt_static_allows_dylibs: true, main_needs_argc_argv: true, + // Emit the entry point under the wasm C-ABI name that Clang uses + // (`__main_argc_argv` for the argc/argv form) rather than a raw `main`. + // This is what emscripten's crt/libc references, and is required for + // entry paths such as `-sPROXY_TO_PTHREAD` whose `crt1_proxy_main` + // links against `__main_argc_argv`. A raw `main` only resolves on the + // JS-driven default entry path and fails to link on the proxied one. + entry_name: "__main_argc_argv".into(), panic_strategy: PanicStrategy::Unwind, no_default_libraries: false, families: cvs!["unix", "wasm"], diff --git a/tests/run-make/wasm-emscripten-main-symbol/main.rs b/tests/run-make/wasm-emscripten-main-symbol/main.rs new file mode 100644 index 0000000000000..f328e4d9d04c3 --- /dev/null +++ b/tests/run-make/wasm-emscripten-main-symbol/main.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/tests/run-make/wasm-emscripten-main-symbol/rmake.rs b/tests/run-make/wasm-emscripten-main-symbol/rmake.rs new file mode 100644 index 0000000000000..03bbdd0bcb62b --- /dev/null +++ b/tests/run-make/wasm-emscripten-main-symbol/rmake.rs @@ -0,0 +1,45 @@ +//@ only-wasm32-unknown-emscripten + +// On wasm the age-old C trick of a `main` that is either `int main(void)` or +// `int main(int, char**)` doesn't work, because wasm requires caller and callee +// signatures to match. The platform ABI (as used by Clang) therefore mangles +// the entry point by signature; the argc/argv form is emitted as +// `__main_argc_argv`. Rust's emscripten target uses the argc/argv form, so its +// entry point must be emitted under that name rather than as a raw `main`. +// +// This matters beyond cosmetics: emscripten's own crt references +// `__main_argc_argv` directly on some entry paths (notably `crt1_proxy_main.o`, +// used by `-sPROXY_TO_PTHREAD`), which fail to link against a raw `main`. +// +// The JS-visible name of the entry point stays `_main`; emscripten bridges that +// to the wasm `__main_argc_argv` export internally. + +use run_make_support::{rfs, rustc, wasmparser}; +use wasmparser::ExternalKind; + +fn main() { + rustc().input("main.rs").target("wasm32-unknown-emscripten").output("main.js").run(); + + let file = rfs::read("main.wasm"); + let mut entry = None; + let mut has_plain_main = false; + for payload in wasmparser::Parser::new(0).parse_all(&file) { + if let wasmparser::Payload::ExportSection(s) = payload.unwrap() { + for export in s { + let export = export.unwrap(); + match export.name { + "__main_argc_argv" => entry = Some(export.kind), + "main" => has_plain_main = true, + _ => {} + } + } + } + } + + assert_eq!( + entry, + Some(ExternalKind::Func), + "the emscripten entry point must be exported as the wasm C-ABI symbol `__main_argc_argv`", + ); + assert!(!has_plain_main, "a raw `main` symbol must not be exported on wasm"); +} From 15f98292d6f292682ab12d326ce5af25f9da1ada Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Wed, 8 Jul 2026 09:51:20 -0700 Subject: [PATCH 2/2] update comments --- compiler/rustc_codegen_ssa/src/back/linker.rs | 6 +++--- .../src/spec/targets/wasm32_unknown_emscripten.rs | 10 ++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/compiler/rustc_codegen_ssa/src/back/linker.rs b/compiler/rustc_codegen_ssa/src/back/linker.rs index 2eac97f3c4a47..e24e7bad2b945 100644 --- a/compiler/rustc_codegen_ssa/src/back/linker.rs +++ b/compiler/rustc_codegen_ssa/src/back/linker.rs @@ -1288,9 +1288,9 @@ impl<'a> Linker for EmLinker<'a> { // Emscripten exposes the program entry point under the JS name `_main` // regardless of the underlying wasm symbol (which is `__main_argc_argv` - // to match Clang's C ABI), bridging the two internally. So the entry - // symbol must be requested as `_main` here rather than as a `_`-prefixed - // form of its wasm name. + // per the wasm C ABI in the tool-conventions BasicCABI spec), bridging + // the two internally. So the entry symbol must be requested as `_main` + // here rather than as a `_`-prefixed form of its wasm name. let entry_name = self.sess.target.entry_name.as_ref(); let mut arg = OsString::from("EXPORTED_FUNCTIONS="); let encoded = serde_json::to_string( diff --git a/compiler/rustc_target/src/spec/targets/wasm32_unknown_emscripten.rs b/compiler/rustc_target/src/spec/targets/wasm32_unknown_emscripten.rs index d585808b94fff..bda2681605814 100644 --- a/compiler/rustc_target/src/spec/targets/wasm32_unknown_emscripten.rs +++ b/compiler/rustc_target/src/spec/targets/wasm32_unknown_emscripten.rs @@ -29,12 +29,10 @@ pub(crate) fn target() -> Target { crt_static_default: true, crt_static_allows_dylibs: true, main_needs_argc_argv: true, - // Emit the entry point under the wasm C-ABI name that Clang uses - // (`__main_argc_argv` for the argc/argv form) rather than a raw `main`. - // This is what emscripten's crt/libc references, and is required for - // entry paths such as `-sPROXY_TO_PTHREAD` whose `crt1_proxy_main` - // links against `__main_argc_argv`. A raw `main` only resolves on the - // JS-driven default entry path and fails to link on the proxied one. + // Use the wasm C-ABI entry name from the tool-conventions BasicCABI + // spec rather than a raw `main`, as referenced by emscripten's crt/libc. + // Required for entry paths like `-sPROXY_TO_PTHREAD`, whose + // `crt1_proxy_main` links against `__main_argc_argv`. entry_name: "__main_argc_argv".into(), panic_strategy: PanicStrategy::Unwind, no_default_libraries: false,