relon_codegen_llvm/wasm_link.rs
1//! S3.X wasm32 link step: turn an LLVM-emitted **relocatable** wasm
2//! object (`\0asm` with a `linking` custom section, undefined symbols,
3//! no exports / no memory) into an **instantiable** wasm module.
4//!
5//! `LlvmAotEvaluator::emit_object_for_target(.., CodegenTarget::Wasm32)`
6//! writes a relocatable object — the LLVM WebAssembly backend emits the
7//! same object-file shape `clang -c --target=wasm32` produces. wasmtime
8//! cannot instantiate that directly; it needs the linker pass that
9//! materialises the `memory`, the `globals` (stack pointer), and the
10//! function `export`s. We shell out to `wasm-ld` for that, mirroring how
11//! a `clang --target=wasm32` toolchain finishes the build.
12//!
13//! `wasm-ld` is the LLVM linker shipped with the `lld` package; we probe
14//! the common binary names (`wasm-ld`, `wasm-ld-NN`). The relocatable
15//! wasm object format is stable across recent LLVM majors, so a system
16//! `wasm-ld-17` happily links an LLVM-18-emitted object.
17
18use std::path::Path;
19use std::process::Command;
20
21use crate::error::LlvmError;
22
23/// Candidate `wasm-ld` binary names, most-specific first. The LLVM-18
24/// build the emitter uses doesn't ship `wasm-ld` in `/usr/lib/llvm-18`
25/// on every distro, but the wasm object format is forward/back-compatible
26/// across these majors for the link step.
27const WASM_LD_CANDIDATES: &[&str] = &[
28 "wasm-ld",
29 "wasm-ld-18",
30 "wasm-ld-17",
31 "wasm-ld-19",
32 "wasm-ld-16",
33];
34
35/// Locate a usable `wasm-ld` on `PATH`. Returns the binary name (for
36/// `Command::new`) or `None` when no candidate responds to `--version`.
37pub fn find_wasm_ld() -> Option<String> {
38 for name in WASM_LD_CANDIDATES {
39 let ok = Command::new(name)
40 .arg("--version")
41 .output()
42 .map(|o| o.status.success())
43 .unwrap_or(false);
44 if ok {
45 return Some((*name).to_string());
46 }
47 }
48 None
49}
50
51/// Link a relocatable wasm object (`obj_path`) into an instantiable
52/// wasm module written to `out_path`, exporting `entry_symbol` and the
53/// linear `memory`.
54///
55/// Flags:
56/// - `--no-entry`: there is no `_start` / `main`; the module is a
57/// library whose entry is the exported relon symbol.
58/// - `--export=<entry_symbol>` + `--export=__heap_base`: surface the
59/// relon entry (and the heap base, useful for the buffer-arena
60/// handshake) to the host.
61/// - `--allow-undefined`: tolerate unresolved imports (e.g. a future
62/// WASI host fn) — they become wasm `import`s the host satisfies.
63/// - `--export-memory` (implicit default) yields the `memory` export
64/// wasmtime reads for the arena handshake.
65pub fn link_wasm_object(
66 obj_path: &Path,
67 out_path: &Path,
68 entry_symbol: &str,
69) -> Result<(), LlvmError> {
70 let ld = find_wasm_ld().ok_or_else(|| {
71 LlvmError::Codegen(
72 "wasm-ld not found on PATH (install `lld` / `wasm-ld`); required to link the \
73 relocatable wasm32 object into an instantiable module"
74 .into(),
75 )
76 })?;
77 let output = Command::new(&ld)
78 .arg("--no-entry")
79 .arg("--allow-undefined")
80 .arg(format!("--export={entry_symbol}"))
81 // `__heap_base` is a synthetic global the linker emits marking
82 // the first byte past the static data; the buffer-arena
83 // handshake lays its `ArenaState` + arena there. Harmless export
84 // for the fast path (no consumer reads it).
85 .arg("--export=__heap_base")
86 .arg(obj_path)
87 .arg("-o")
88 .arg(out_path)
89 .output()
90 .map_err(|e| LlvmError::Codegen(format!("spawn {ld}: {e}")))?;
91 if !output.status.success() {
92 return Err(LlvmError::Codegen(format!(
93 "{ld} failed ({}):\n{}",
94 output.status,
95 String::from_utf8_lossy(&output.stderr)
96 )));
97 }
98 Ok(())
99}