Skip to main content

Module link_plan

Module link_plan 

Source
Expand description

Build the linker invocation for a hot-patch dylib by editing the captured fat-build linker call (see I4g-X1 whisker-linker-shim) as little as possible.

§Why we don’t construct linker args from scratch

cargo + rustc + the platform’s clang/gcc driver assemble a large, fragile argv: sysroot, target triple, -arch, NDK search paths, framework directories, OS-version-min flags, -Wl,… directives, sometimes a custom -fuse-ld=…. These shift across:

  • macOS major versions (sysroot path, -platform_version)
  • Xcode releases (-isysroot, framework dirs)
  • Android NDK r24/r25/r26 (CRT layout, libc++.a, libunwind.a)
  • glibc CSU layout (crt1.o vs Scrt1.o, libc_nonshared.a)
  • rustc’s choice of linker driver (cc, clang, lld)

Re-deriving any of these is a long-tail of papercuts. So: capture the fat-build linker invocation verbatim (X1) and edit only the parts a hot-patch must change. Same principle as build_obj_plan does for rustc.

§What we change

  1. Drop object/archive inputs (.o, .rlib, .a, .so, .dylib). The fat build linked the entire workspace; the patch only needs the freshly-rebuilt object.
  2. Drop -o <path> and the existing -shared (we re-add both).
  3. Drop -undefined <action> (the separated macOS form) so we can deterministically set dynamic_lookup.
  4. Drop --version-script=<path> and --no-undefined-version. The fat build’s version-script enumerates thousands of Rust-mangled symbols (rustc auto-generates one for both dylib and cdylib). Re-applying it to a patch dylib that only defines the one changed function makes the linker emit version script assignment ... failed: symbol not defined for every absent symbol — fatal under --no-undefined-version. The patch dylib’s default visibility (everything global) is the right behaviour: subsecond::apply_patch reads the patch’s .dynsym looking for the changed function’s mangled name.
  5. Append:
    • -shared
    • OS-specific “unresolved is fine” directive:
      • macOS: -Wl,-undefined,dynamic_lookup
      • Linux/Android: -Wl,--unresolved-symbols=ignore-all
    • any caller-supplied extra objects (typically the stub.o produced by crate::hotpatch::create_undefined_symbol_stub). The stub defines every host symbol the patch refers to as a tiny ARM64 trampoline branching to that symbol’s runtime address, computed from the device’s reported subsecond::aslr_reference(). Linking it in this slot means the patch has no DT_NEEDED back-edge to the host, and no dlopen-time symbol resolution to perform — which avoids the Android linker-namespace + RTLD_LOCAL corner cases the previous “back-edge to host dylib” scheme tripped over.
    • the new object path
    • -o <output>

Everything else — -isysroot, -arch, -target, -L, -l, -rpath, -Wl,…, -F, -framework, -fuse-ld=…, -mmacosx-version-min=… — is preserved verbatim.

The “unresolved is fine” directive is the load-bearing trick that makes hot-patch dylibs small and fast: every symbol the patch references but doesn’t define (e.g. an unmodified whisker::println) is left as an undefined-symbol marker, and subsecond::apply_patch resolves it against the original binary’s symbol table at swap-in time. So the patch dylib holds only the changed function bodies, not their entire transitive call graph.

Structs§

LinkPlan
Result of build_link_plan.

Enums§

LinkerOs
Which OS-specific “unresolved-is-fine” directive to emit. Android uses the same lld defaults as Linux for our purposes.

Functions§

build_link_plan
Re-shape a captured linker invocation into the link step of a hot-patch build. See module docs for the rationale.
linker_os_for_host
Pick the LinkerOs that matches the host we’re building on. cfg!-based — at runtime we know which compiled-in branch ran. For cross-target hot-patch (e.g. macOS host → Android device), callers should pass the target OS explicitly rather than rely on this convenience.