Skip to content

runtime: ext staticlibs bundle perry-runtime and shadow perry-stdlib's real symbols with its no-op stubs (node:http servers get no tokio reactor) #6314

Description

@proggeramlug

Split out of #6303 / #6313, which fixed the semantic half of this. This is the structural half, and it is pre-existing (it reproduces with or without #6313).

The invariant that is being violated

perry-runtime/src/stdlib_stubs.rs defines no-op #[no_mangle] stubs for stdlib symbols — js_stdlib_init_dispatch, js_ws_*, fetch, readline — on this stated assumption:

When perry-stdlib IS linked, its real implementations are used instead (the linker picks stdlib over runtime since only one is ever linked).

That assumption is false. The perry-ext-* crates are crate-type = ["staticlib", "rlib"], so each one bundles the perry-runtime rlib objects (including the stdlib_stubs codegen unit) into its libperry_ext_*.a. And perry compile links the ext archives before stdlib/runtime (prefer_well_known_before_stdlib). So the bundled copy's no-op stub wins the link.

Symptom

js_stdlib_init_dispatch resolves to perry-runtime's no-op stub, perry-stdlib's real dispatch init never runs, spawn_blocking_with_reactor is never registered — and any node:http server dies on its first accept:

$ ./server_bin
[perry] warning: `js_stdlib_init_dispatch` is a no-op stub on this platform —
                 stdlib dispatch symbol from perry-stdlib not linked into this binary (runtime-only build)
listening
thread '<unnamed>' panicked at crates/perry-ext-http-server/src/server.rs:910:
  there is no reactor running, must be called from the context of a Tokio 1.x runtime
thread caused non-unwinding panic. aborting.
$ curl localhost:3111/     -> status=000

Repro is a 3-liner:

import * as http from 'http';
const srv = http.createServer((_q: any, s: any) => s.end('hello'));
srv.listen(3111, () => console.log('listening'));

Why it is usually masked

strip_bundled_runtime_from_well_known_lib is supposed to remove the bundled perry_runtime-* members from the ext archive. Its Rule 1 matches members by name containment against stdlib's archive:

.filter(|c| stdlib_members.iter().any(|s| s.contains(c.as_str())))

A perry-runtime built with a different feature set gets a different codegen-unit hash, so the member name never appears in stdlib's list, the candidate set is empty, and the strip silently returns the archive unchanged.

That means the strip only works when the ext archive and stdlib happened to be built in the same cargo invocation (feature unification ⇒ one perry-runtime unit ⇒ matching hashes). A coherent whole-workspace build masks the bug completely; the release build does not:

# .github/workflows/release-packages.yml
cargo build --profile dist ... -p perry-runtime -p perry-runtime-static   # unit A
cargo build --profile dist ... -p perry-stdlib  -p perry-stdlib-static    # unit B
for d in crates/perry-ext-*; do
  cargo build --profile dist ... -p "$name"                               # unit C  <-- diverges
done

Three invocations, three different perry-runtime feature resolutions.

Possible fixes (needs a maintainer call on the build graph)

  1. Make strip-dedup's Rule 1 hash-insensitive — strip a bundled perry_runtime-* member iff every symbol it defines is also defined by stdlib (Rule 2 already computes stdlib_defined). Strictly more robust than name containment, and it is the property we actually want.
  2. Build every shipped archive in one cargo invocation so feature unification yields a single perry-runtime unit. Careful: the union would then include external-ws-symbols / external-fetch-symbols, which suppress the stubs in the shipped libperry_runtime.a — that changes what a program links when it uses WebSocket without an ext archive, so it needs checking.
  3. Stop bundling perry-runtime into the ext staticlibs — have them reference the symbols as undefined externs (the perry-ffi pattern) and let the final link resolve them. Architecturally the cleanest and it kills the whole duplicate-runtime class, but perry-ext-http uses a lot of Rust-ABI perry-runtime surface (gc::js_shadow_frame_push, arena::pointer_in_nursery, ClosureHeader, Promise, …), so it is a real refactor.

#6313 takes none of these — it only guarantees the bundled copy is not a semantically degraded subset (so str.replace(re, fn) is correct whichever copy wins). The stub shadowing above is orthogonal and still open.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions