#!/usr/bin/env bash # wasmc++: the REAL C++ cross driver for wasm32-hwjs (musl arm) — # PLAN-CXX-STATUS-CHANNEL Track R §6 item 5 (closes the litb#104 "the C++ # compiler is a symlink to the C driver" gap). # # What it is: clang++ pointed at the staged libc++/libc++abi headers + # bitcode archives (the sysroot build's `make cxx-runtimes`) on top of wasmcc's # proven C substrate. COMPILE emits BITCODE with the pinned clang++ (the # HwjsEHPreLower + HwjsCoroutinize passes run ONCE over the merged module # inside wasmld — per-TU invoke/landingpad IR is exactly their input). # LINK delegates to wasmcc's coro link arm (crt/RT-provenance/pool/thunks # machinery inherited verbatim) with the C++ pieces injected: # - every C++/C source on the link line is pre-compiled HERE with the # C++ flag set (wasmcc would C-compile it with -fno-exceptions); # - the §5.2 matcher/terminus TU (cxa_hwjs_status_channel.cpp.o) rides # as an explicit ROOT — wasmld's need-only pool pull runs BEFORE the # plugin compile, but the references to __hwjs_cxa_type_match / the # strong __hwjs_uncaught_unwinding are created BY the pre-lowering # DURING that compile, so an archive-resident copy is never pulled # (and the RT's weak __hwjs_uncaught_unwinding would silently win); # - libc++.a + libc++abi.a are appended (default `-lc++ -lc++abi` # semantics; explicit -lc++/-lc++abi/-lstdc++ are folded into that). # # EXCEPTIONS ARE ON, ALWAYS (plan N1): C++ exceptions propagate on the hwjs # status channel (STATUS-CHANNEL-CONTRACT §5.2) after HwjsEHPreLowerPass # lowers invoke/landingpad/resume to status checks — no libunwind, no # _Unwind_* two-phase ABI, no LSDA (plan D5). -fno-exceptions is REFUSED # loudly: a -fno-exceptions frame between a raise and its handler would # skip its cleanups silently (the I3 silent-transit shape), and shipping # STL-with-exceptions is the whole point. # # CHECK EMISSION (I3, plan §7): C++ code must be check-emitting, so this # driver defaults HWJS_EH_PRELOWER=1 on the substrate of the day (wasmld # loads the pre-lower plugin and its never-silent SUMMARY guard proves the # pass ran; under HWJS_STATUS_CHANNEL=1 the same pass is loaded by that # knob and the artifact additionally attests hwjs.abi check_emitting). # An explicit HWJS_EH_PRELOWER=0 for a C++ link is refused (gate arms may # acknowledge via WASMLD_HWJS_ALLOW_RED_KNOBS=1 — the house RED-knob # pattern; the E1 mechanism-off twin is built exactly that way). set -euo pipefail HWJS_HOME="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # shellcheck source=libexec/find-llvm.sh source "$HWJS_HOME/libexec/find-llvm.sh" # The sysroot tree, a directory of this checkout. The default was the # checkout ROOT, one level above where the artifacts are, left over from when # a separate repository built the libc trees and always exported the pointer. TOOLS_DIR="${HWJS_TOOLS_DIR:-${BRINTIUM_SYSROOT:-$HWJS_HOME/sysroot}}" HWJS_LLVM_BIN="$(hwjs_llvm_bin "$HWJS_HOME/libexec")" \ || { echo "wasmc++: FATAL: pinned LLVM not found (set WASMLD_HWJS_LLVM_BIN, or build it per toolchain/llvm-from-source.md)." >&2; exit 1; } CLANGXX="$HWJS_LLVM_BIN/clang++" WASMCC="${WASMCXX_CC_DRIVER:-$HWJS_HOME/libexec/brintcc-musl}" # C++ is hwjs-substrate-only on this platform (there is no Asyncify C++ # story): default the coro arm ON. An explicit WASMLD_HWJS=0 is honored # for compile-only introspection but refused for links below. export WASMLD_HWJS="${WASMLD_HWJS:-1}" # ── libc sysroot (same env surface as wasmcc; the default probes this # checkout's musl-src alias first, then the musl BUILD tree — a caller given # only a staged build tree has no musl-src, only build/musl-1.2.5-build) ──── _musl_root="$TOOLS_DIR/musl-src" [[ -f "$_musl_root/include/stdlib.h" ]] || _musl_root="$TOOLS_DIR/build/musl-1.2.5-build" LIBC_INCLUDE="${WACKY_LIBC_INCLUDE:-$_musl_root/include}" LIBC_ARCH_INCLUDE="${WACKY_LIBC_ARCH_INCLUDE:-$_musl_root/arch/wasm32}" LIBC_GENERIC_INCLUDE="${WACKY_LIBC_GENERIC_INCLUDE:-${LIBC_ARCH_INCLUDE%/wasm32}/generic}" LIBC_OBJ_INCLUDE="${WACKY_LIBC_OBJ_INCLUDE:-${LIBC_INCLUDE%/include}/obj/include}" WACKY_LIBC_DIR="${WACKY_LIBC_DIR:-$TOOLS_DIR/build/musl-1.2.5-build/lib}" # ── the staged C++ runtimes (the sysroot build's `make cxx-runtimes`) ──────────── LIBCXX_INC="${WACKY_LIBCXX_INC:-$TOOLS_DIR/build/musl-1.2.5-build/include/c++/v1}" LIBCXX_LIBDIR="${WACKY_LIBCXX_LIBDIR:-$TOOLS_DIR/build/musl-1.2.5-build/lib}" # Kernel UAPI headers (linux/*.h asm/*.h): libc++'s atomic machinery needs # them (linux/futex.h); appended LAST so they can never shadow a libc or # libc++ header. # # UNLIKE THE GLIBC ARM, this one is genuinely optional: a musl C++ line that # never reaches the atomics wait machinery compiles without them, and the # include below is added only when the directory exists. What is NOT optional # is that the pointer be the real one. The value here used to be a path into a # particular distribution's build tree, reached by walking out of the sysroot — # it named nothing after that tree moved, and clang drops an -isystem that is # not there without a word, so a C++ file that did include linux/futex.h # failed as a missing header rather than as a missing input. There is one # pointer at the kernel's headers in this toolchain and this is it. WACKY_UAPI_INCLUDE="${WACKY_UAPI_INCLUDE:-${BRINTIUM_KERNEL_HEADERS:-}}" # ── refusals (loud, value-keyed — never guess) ────────────────────────────── for a in "$@"; do case "$a" in -fno-exceptions) echo "wasmc++: FATAL: -fno-exceptions on a C++ line — exceptions are ON on wasm32-hwjs (they ride the status channel; PLAN-CXX-STATUS-CHANNEL N1). A -fno-exceptions frame between a raise and its handler would skip its cleanups SILENTLY (the I3 silent-transit shape). If a package only builds with exceptions off, that is a finding to report, not a flag to pass." >&2 exit 1 ;; esac done case "${HWJS_EH_PRELOWER:-}" in '') export HWJS_EH_PRELOWER=1 ;; # the check-emitting default (I3) 1) ;; 0) if [[ "${WASMLD_HWJS_ALLOW_RED_KNOBS:-0}" != "1" ]]; then echo "wasmc++: FATAL: HWJS_EH_PRELOWER=0 on a C++ line — without the EH pre-lowering the ported __cxa_throw raises on the status channel and RETURNS into code that never checks it (the demonstrated runtime-RED shape: noreturn-breach violation / I2 abort). C++ through this driver is check-emitting by default (I3). Set WASMLD_HWJS_ALLOW_RED_KNOBS=1 only for a deliberate mechanism-off RED arm." >&2 exit 1 fi echo "wasmc++: NOTE (HWJS_EH_PRELOWER=0, RED arm ACKNOWLEDGED): building WITHOUT the EH pre-lowering — the mechanism-off twin. Non-production." >&2 ;; *) echo "wasmc++: FATAL: HWJS_EH_PRELOWER='${HWJS_EH_PRELOWER}' — this knob is VALUE-keyed (1 = on, 0 = RED arm, unset/empty = default 1); refusing to guess." >&2 exit 1 ;; esac # ── triples + flag sets ───────────────────────────────────────────────────── COMPILE_TRIPLE="wasm32-hwjs-linux-musl" COMPILE_TARGET="--target=$COMPILE_TRIPLE" export WASMLD_HWJS_COMPILE_TARGET="${WASMLD_HWJS_COMPILE_TARGET:-$COMPILE_TRIPLE}" # wasmcc COMMON_ARGS parity minus -fno-exceptions (see header), plus the # C++ posture: -fexceptions -frtti explicit (wasm targets default # exceptions OFF in clang — the flag is load-bearing). COMMON_ARGS=( -nostdlib -nostdlibinc -fno-builtin -fexceptions -frtti -fvisibility=default -fno-pic -fno-PIE -matomics -mbulk-memory -mmutable-globals -msign-ext -mnontrapping-fptoint -ffunction-sections -fdata-sections ) CXX_ISYSTEM_ARGS=( -nostdinc++ ) [[ -d "$LIBCXX_INC" ]] || { echo "wasmc++: FATAL: no staged libc++ headers at $LIBCXX_INC — run 'make -C sysroot cxx-runtimes', or set WACKY_LIBCXX_INC." >&2 exit 1 } CXX_ISYSTEM_ARGS+=( -isystem "$LIBCXX_INC" ) for inc in "$LIBC_ARCH_INCLUDE" "$LIBC_GENERIC_INCLUDE" "$LIBC_OBJ_INCLUDE" "$LIBC_INCLUDE" "$WACKY_UAPI_INCLUDE"; do [[ -d "$inc" ]] && CXX_ISYSTEM_ARGS+=( -isystem "$inc" ) done # ── mode detection (mirror wasmcc) ────────────────────────────────────────── is_link_mode=1 for a in "$@"; do case "$a" in -c|-E|-S|-M|-MM|-fsyntax-only|--version|-v|-dumpversion|-dumpmachine|-print-*) is_link_mode=0 ;; esac done if [[ "$is_link_mode" == "0" ]]; then # ── COMPILE / preprocess / introspection ──────────────────────────────── # Bitcode out on the coro arm (wasmld partitions by magic); mirror # wasmcc's default -o pinning (autotools suffix rules expect .o). emit=() compile=0 have_o=0 src="" prev="" for a in "$@"; do case "$a" in -c) compile=1; [[ "$WASMLD_HWJS" == "1" && "${WASMLD_HWJS_CONFTEST:-0}" == "0" ]] && emit=( -emit-llvm ) ;; # BRINTOS-WORKAROUND: brintos/hwjs-cc#39 revisit # The attached form must count too: missing it synthesises a second # -o after the caller's args, so clang writes ./.o and the # requested path never appears -- silently, exit 0. waf always emits # -o. -o|-o?*) have_o=1 ;; esac if [[ "$prev" != "-o" ]]; then case "$a" in *.c|*.cc|*.cpp|*.cxx|*.C|*.cp|*.c++) src="$a" ;; esac fi prev="$a" done out=() if [[ "$compile" == "1" && "$have_o" == "0" && -n "$src" ]]; then base="${src##*/}"; base="${base%.*}"; out=( -o "${base}.o" ) fi exec "$CLANGXX" \ "$COMPILE_TARGET" \ "${COMMON_ARGS[@]}" \ "${CXX_ISYSTEM_ARGS[@]}" \ "${emit[@]}" \ "$@" \ "${out[@]}" fi # ── LINK ──────────────────────────────────────────────────────────────────── if [[ "$WASMLD_HWJS" != "1" ]]; then echo "wasmc++: FATAL: WASMLD_HWJS=0 on a C++ LINK — there is no non-coro C++ substrate on wasm32-hwjs (the status-channel lowering IS the exception mechanism). Unset WASMLD_HWJS or leave it 1." >&2 exit 1 fi # Pre-compile every C-family source on the link line with the C++ flag set # (wasmcc's own single-step path would C-compile it with -fno-exceptions). # clang++ compiles a .c input as C++ here — the documented g++ behavior. stage="$(mktemp -d "${TMPDIR:-/tmp}/wasmcxx-stage.XXXXXX")" trap 'rm -rf "$stage"' EXIT cxx_flags=() expect="" for a in "$@"; do if [[ -n "$expect" ]]; then [[ "$expect" == "cc" ]] && cxx_flags+=( "$a" ) expect="" continue fi case "$a" in -o|-L) expect="skip" ;; -I|-D|-U|-include|-imacros|-isystem|-idirafter|-iquote|-Xclang) cxx_flags+=( "$a" ); expect="cc" ;; -o*|-L*|-l*|-Wl,*) : ;; -*) cxx_flags+=( "$a" ) ;; *) : ;; esac done link_args=() nostdlibxx=0 srcn=0 for a in "$@"; do case "$a" in -nostdlib|-nodefaultlibs|-nostdlib++) nostdlibxx=1; link_args+=( "$a" ) ;; # this driver OWNS the C++ runtime archives (appended below with the # matcher root); fold explicit spellings into that so the archive # never rides the line twice -lc++|-lc++abi|-lstdc++) : ;; *.c|*.cc|*.cpp|*.cxx|*.C|*.cp|*.c++) [[ -f "$a" ]] || { echo "wasmc++: FATAL: link-line source '$a' does not exist." >&2; exit 1; } srcn=$((srcn + 1)) sb="$(basename "$a")" so="$stage/linksrc-${srcn}-${sb%.*}.o" "$CLANGXX" \ "$COMPILE_TARGET" \ "${COMMON_ARGS[@]}" \ "${CXX_ISYSTEM_ARGS[@]}" \ "${cxx_flags[@]}" \ -emit-llvm -c "$a" -o "$so" \ || { echo "wasmc++: FATAL: single-step C++ compile of link-line source '$a' failed (it must join the merged-module pass pipeline; error above)." >&2; exit 1; } link_args+=( "$so" ) ;; *) link_args+=( "$a" ) ;; esac done CXX_LINK_EXTRAS=() # -fexperimental-library (tzdb/PSTL/syncstream): the staged # libc++experimental.a joins the link whenever it is staged — wasmld's pool # pull is need-only, so non-experimental links are byte-identical. If the # caller ASKED for the experimental library and the archive is not staged, # refuse HERE with the staging recipe instead of leaking "undefined symbol # std::chrono::get_tzdb" at the end of the link (the pre-2026-07-21 "config # that lies" failure mode: __config_site advertised tzdb, nothing shipped it). wants_experimental=0 for a in "$@"; do [[ "$a" == "-fexperimental-library" ]] && wants_experimental=1 done if [[ "$nostdlibxx" == "0" && "${WASMLD_HWJS_CONFTEST:-0}" == "0" ]]; then MATCHER="${WACKY_LIBCXX_MATCHER:-$LIBCXX_LIBDIR/cxa_hwjs_status_channel.cpp.o}" for f in "$MATCHER" "$LIBCXX_LIBDIR/libc++.a" "$LIBCXX_LIBDIR/libc++abi.a"; do [[ -f "$f" ]] || { echo "wasmc++: FATAL: missing staged C++ runtime piece '$f' — run 'make -C sysroot cxx-runtimes'." >&2 exit 1 } done LIBCXX_EXP="$LIBCXX_LIBDIR/libc++experimental.a" if [[ "$wants_experimental" == "1" && ! -f "$LIBCXX_EXP" ]]; then echo "wasmc++: FATAL: -fexperimental-library requested but no staged libc++experimental.a in $LIBCXX_LIBDIR — the headers would compile tzdb/PSTL/syncstream uses that cannot link. Rebuild the runtimes (LIBCXX_ENABLE_EXPERIMENTAL_LIBRARY=ON is the brintos cache default since 2026-07-21) and run 'make -C sysroot cxx-runtimes'." >&2 exit 1 fi CXX_LINK_EXTRAS=( "$MATCHER" ) [[ -f "$LIBCXX_EXP" ]] && CXX_LINK_EXTRAS+=( "$LIBCXX_EXP" ) CXX_LINK_EXTRAS+=( "$LIBCXX_LIBDIR/libc++.a" "$LIBCXX_LIBDIR/libc++abi.a" ) # If a caller turns on the merge DCE (internalize+globaldce pre-pass; # default OFF on the musl arm), the matcher TU's definitions look dead # at DCE time — the references to them are created BY the pre-lowering # during the LATER plugin compile — and would be deleted, leaking # phantom env imports (witnessed on the glibc arm, where DCE defaults # ON). Pin them as DCE roots whenever DCE is requested. if [[ -n "${WASMLD_HWJS_DCE_MERGE:-}" && "${WASMLD_HWJS_DCE_MERGE}" != "0" ]]; then export WASMLD_HWJS_DCE_EXTRA_ROOTS="${WASMLD_HWJS_DCE_EXTRA_ROOTS:+${WASMLD_HWJS_DCE_EXTRA_ROOTS},}__hwjs_cxa_type_match,__hwjs_uncaught_unwinding" fi elif [[ "$nostdlibxx" == "0" ]]; then # conftest fast path: the archives would only be nm'd for the witness # allowlist; hand them over so C++ probes answer honestly. for f in "$LIBCXX_LIBDIR/libc++.a" "$LIBCXX_LIBDIR/libc++abi.a"; do [[ -f "$f" ]] && CXX_LINK_EXTRAS+=( "$f" ) done fi # Delegate the link to wasmcc's coro arm: crt selection, RT provenance, # musl pool, thunks, builtins — all inherited from the ONE proven C lane. exec "$WASMCC" "${link_args[@]}" "${CXX_LINK_EXTRAS[@]}"