. "$BST_LIB" # BST-TIMEOUT: 300 # six execs of small wasm modules; the runner's own default # # of 60 is enough on an idle guest and not on a loaded one, # # and 300 is what the ticket gates and the nightly pass, so # # this marker costs them nothing. # # libffi 3.4.6 (poky meta/recipes-support) on wasm32-hwjs -- A LIBRARY THAT # LINKS AND ABORTS, and this suite exists to keep that statement true in both # directions. # # WHAT IS SHIPPED. libffi's machine-dependent layer is a stub on this arm: # upstream's wasm32 backend is Emscripten-only (it reaches an indirect call # through the JS runtime's dynCall and builds a closure by compiling a fresh # module and growing the function table), and HardwareJS offers no such # substrate. Everything else is upstream's portable core, unmodified and # genuinely working -- the type descriptors, the struct-layout algorithm, the # cif validator, the closure allocator and the raw API. # # SO THERE ARE TWO KINDS OF CASE HERE AND THEY PULL IN OPPOSITE DIRECTIONS. # # 1. The working half must answer CORRECTLY. A library that packaged the # right symbols and laid structures out wrongly would satisfy every # consumer's configure and corrupt every consumer's data, and nothing # downstream would notice, so the layout arithmetic is pinned hardest. # # 2. The stubbed half must FAIL, LOUDLY. The cases named `abort-*` below are # RED WITNESSES: each fails if the abort does NOT happen. That inversion # is the point of them. A future real backend cannot slip past this suite # unnoticed -- it turns these cases red on the day it lands, which is # exactly when someone should be reading this file. # # EVERY EXPECTED VALUE IS PINNED FROM AN x86-64 CONTROL, not guessed: the same # libffi 3.4.6 tarball, configured with this recipe's own switches # (--disable-builddir --disable-exec-static-tramp, static), built natively with # gcc 13.3.0 against glibc 2.39, and driven through the SAME probe source on # 2026-08-24. Where a case says "control", that is what it means. # # THREE THINGS DIFFER FROM THE CONTROL AND EACH IS A PROPERTY OF THE ARM, not a # defect. They are asserted at their wasm32 values with the control's recorded # beside them, because an unexplained difference is how a real regression gets # read as "oh, that one is just different": # # - pointer size, and ffi_type_pointer with it: 4 here, 8 on x86-64. # - FFI_DEFAULT_ABI: FFI_WASM32 (1) here, FFI_UNIX64 (2) on x86-64 -- two # unrelated enums, so only the wasm32 value means anything below. # - FFI_TYPE_COMPLEX: fatal here and accepted there. x86-64 has a complex # lowering; this arm has none, and upstream's own prep_cif.c answers that # with a bare abort() -- guarded on FFI_TARGET_HAS_COMPLEX_TYPE, which # wasm32 does not define -- BEFORE this port's machdep is reached, so # there is no status for a caller to read and no message on the way out. # It is therefore a red witness (`abort-complex` below) rather than a # status case, and the probe does not prepare one: doing so killed the # probe and took its other thirty cases with it, which is how this was # found. # # Two families are absent from ffi.h on this arm entirely, and an x86-64 header # declares both -- which is why the ABI manifest below is shorter than a native # one and why neither shows up as a missing symbol: # # - ffi_type_complex_float/_double/_longdouble, gated on # FFI_TARGET_HAS_COMPLEX_TYPE, which wasm32 does not define; # - ffi_call_go and ffi_prep_go_closure, gated on FFI_GO_CLOSURES, which # wasm32/ffitarget.h leaves commented out. LX=/usr/libexec/libffi-tests ABI=$LX/ffi-abi.txt # --------------------------------------------------------- the deliverable --- # Named by path rather than probed with `require`: libffi ships no program, so # there is no tool whose absence could produce a skip, and an image that failed # to install the archive must go RED rather than quiet. check libffi/archive test -f /usr/lib/libffi.a check libffi/header test -f /usr/include/ffi.h check libffi/ffitarget test -f /usr/include/ffitarget.h check libffi/pkgconfig test -f /usr/lib/pkgconfig/libffi.pc # THE VERSION A CONSUMER CONFIGURES AGAINST. glib's meson asks for # `dependency('libffi', version: '>=3.0.0')` and every other consumer asks # something similar, so the number pkg-config reports is load-bearing: a .pc # that reported nothing, or reported the wrong version, fails those configures # with a message about libffi being missing rather than about the version. if command -v pkg-config >/dev/null 2>&1; then check_eq libffi/modversion 3.4.6 "$(pkg-config --modversion libffi 2>&1)" # -lffi must be advertised AND must resolve; the resolving half is # 99-pkgconfig-link-surface.test's whole subject, so this only pins the name. check_out libffi/pc-libs "-lffi" pkg-config --libs libffi # A consumer asks pkg-config where the header is. Here the answer is the # compiler's own default include directory, so pkg-config strips it and # --cflags is legitimately EMPTY; "include" was the x86-64 control's answer, # whose prefix is not /usr. Pin what makes the empty answer correct, so a # .pc that lost its Cflags line -- or points somewhere the compiler does not # search -- is still a red. check_eq libffi/pc-includedir /usr/include "$(pkg-config --variable=includedir libffi 2>&1)" check_eq libffi/pc-cflags "" "$(pkg-config --cflags libffi 2>&1)" else bst_skip libffi/modversion "pkg-config not installed on this image" bst_skip libffi/pc-libs "pkg-config not installed on this image" bst_skip libffi/pc-cflags "pkg-config not installed on this image" bst_skip libffi/pc-includedir "pkg-config not installed on this image" fi # ------------------------------------------------------------ ABI surface --- # ffi.h declares 33 names on this arm and libffi.a defines every one of them -- # the recipe proves that with llvm-nm at build time and fails the build if one # is missing, which is a stronger place for that check than here. # # What this suite can add is the SECOND fact, the one the archive cannot show: # five of those names are present and NOT LINKABLE. closures.c takes the # FFI_MMAP_EXEC_WRIT path here, and raw_api.c and java_raw_api.c build their # closure trampolines over an alloca'd argument array; in all three the address # escapes to an opaque sink, hwjs-cc's coroutinize pass REFUSEs the function, # and wasmld FATALs the link of any program reaching it (hwjs-cc#15, open). # Nothing about that is libffi's doing or this port's -- it is upstream code # this port did not touch, refused by the platform. # # The manifest and the shipped libffi-abi-check come from the same generator run # but are different artifacts, so comparing their counts catches a packaging # step that shipped a stale one. check libffi/abi-manifest test -f "$ABI" if [ ! -f "$ABI" ]; then bst_notok libffi/abi-linkable "no manifest at $ABI" bst_notok libffi/abi-unlinkable "no manifest at $ABI" elif [ ! -x $LX/libffi-abi-check ]; then bst_notok libffi/abi-linkable "$LX/libffi-abi-check is not installed" bst_notok libffi/abi-unlinkable "no check program to compare the manifest with" else want=$(awk '$3=="linkable"' "$ABI" | wc -l | tr -d ' ') aout=$($LX/libffi-abi-check 2>&1); arc=$? if bst_execfail "$aout"; then bst_unrun libffi/abi-linkable "$aout" else afield() { printf '%s\n' "$aout" | awk -v t="$1" '$1==t {print $2; exit}'; } check_eq libffi/abi-linkable "$want" "$(afield R0-abi-linkable)" check_eq libffi/abi-nonnull "$want" "$(afield R0-abi-nonnull)" check_eq libffi/abi-rc 0 "$arc" check_eq libffi/abi-unlinkable-count 5 "$(afield R0-abi-unlinkable)" fi # THE UNLINKABLE SET IS PINNED BY NAME, not merely counted. Five names, three # causes, one defect -- and the three are in different files, so hwjs-cc#15 # could plausibly be fixed for one shape before the others and shrink this set # by two rather than by five. Naming them means that shows up here as a # specific red rather than as a count that drifted. # # This case goes RED when the platform is FIXED. That is intended: the entry # points then rejoin the checked surface and this port's exception table, its # marker and this assertion all come out together. The build-time negative # link probe fires first and says the same thing. check_eq libffi/abi-unlinkable \ 'ffi_closure_alloc ffi_prep_java_raw_closure ffi_prep_java_raw_closure_loc ffi_prep_raw_closure ffi_prep_raw_closure_loc' \ "$(awk '$3=="unlinkable"{printf "%s%s", s, $1; s=" "} END{print ""}' "$ABI" | tr ' ' '\n' | sort | tr '\n' ' ' | sed 's/ $//')" fi # ---------------------------------------------------------------- the core --- # libffi-probe drives the portable half in process. Absent probe is a RED, not # a skip: it ships from the same recipe as the archive, so its absence means the # port did not install what it built. if [ ! -x $LX/libffi-probe ]; then bst_notok libffi/probe-present "$LX/libffi-probe is not installed -- libffi-tests missing from the image" else pout=$($LX/libffi-probe 2>&1); prc=$? if bst_execfail "$pout"; then bst_unrun libffi/probe-run "$pout" else field() { printf '%s\n' "$pout" | awk -v t="$1" '$1==t {print $2; exit}'; } line() { printf '%s\n' "$pout" | awk -v t="$1" '$1==t {$1=""; sub(/^ /,""); print; exit}'; } check_eq libffi/probe-rc 0 "$prc" case "$pout" in *ALL-LIBFFI-OK*) bst_ok libffi/probe-complete ;; *) bst_notok libffi/probe-complete "no ALL-LIBFFI-OK: $(_bst_1line "$pout")" ;; esac # --- the arm's own constants ------------------------------------------- # FFI_DEFAULT_ABI is FFI_WASM32 (1). This is the whole ticket in one # number: ffitarget.h describes FFI_WASM32 as "raw, no structures, varargs, # or closures (not implemented!)" and makes it the default whenever # __EMSCRIPTEN__ is undefined, which here it always is. The control reads 2 # (FFI_UNIX64) from an unrelated enum. check_eq libffi/default-abi 1 "$(field R0-default-abi)" # 0..3: FFI_FIRST_ABI, FFI_WASM32, FFI_WASM32_EMSCRIPTEN, FFI_LAST_ABI. check_eq libffi/abi-range '0..3' "$(field R0-abi-range)" check_eq libffi/sizeof-ptr 4 "$(field R0-sizeof-ptr)" # control: 8 check_eq libffi/closures 1 "$(field R0-closures)" # --- the exported type descriptors ------------------------------------- # Three fields each -- size/alignment/type-code -- and they are the entire # input to the layout algorithm, so a wrong one here is a wrong answer # everywhere downstream. Every fixed-width entry matches the control # exactly; only `pointer` differs, and it differs by the word size. for t in void:1/1/0 uint8:1/1/5 sint8:1/1/6 uint16:2/2/7 sint16:2/2/8 \ uint32:4/4/9 sint32:4/4/10 uint64:8/8/11 sint64:8/8/12 \ float:4/4/2 double:8/8/3 longdouble:16/16/4; do n=${t%%:*}; want=${t#*:} check_eq "libffi/type-$n" "$want" \ "$(printf '%s\n' "$pout" | awk -v n="$n" '$1=="R1-type" && $2==n{print $3}')" done # pointer is 4/4/14 here and 8/8/14 on the control -- the word size, and the # only descriptor difference between the two arms check_eq libffi/type-pointer '4/4/14' \ "$(printf '%s\n' "$pout" | awk '$1=="R1-type" && $2=="pointer"{print $3}')" # --- ffi_prep_cif: the call every consumer makes ------------------------ # 0 is FFI_OK. A stub that refused FFI_DEFAULT_ABI would fail here and # break every consumer at configure time rather than at call time, which is # the failure this port exists to avoid. check_eq libffi/prep-status 0 "$(field R2-prep-status)" check_eq libffi/prep-nargs 3 "$(field R2-nargs)" check_eq libffi/prep-rtype 11 "$(field R2-rtype)" # nfixedargs is the wasm32 arm's FFI_EXTRA_CIF_FIELDS member; the control # has no such field and prints n/a. A backend that left it unset would hand # the caller whatever the stack held. check_eq libffi/prep-nfixedargs 3 "$(field R2-nfixedargs)" check_eq libffi/prep-void-status 0 "$(field R2b-void-status)" check_eq libffi/prep-void-nfixedargs 0 "$(field R2b-nfixedargs)" # --- ffi_prep_cif_var: the ordering hazard ------------------------------ # The variadic path runs ffi_prep_cif_machdep_var FIRST and # ffi_prep_cif_machdep after it, so nfixedargs must SURVIVE the second call. # A stub written as `return FFI_OK` passes every other case in this file and # fails this one, which is why it is here. check_eq libffi/var-status 0 "$(field R3-var-status)" check_eq libffi/var-nargs 4 "$(field R3-nargs)" check_eq libffi/var-nfixedargs 2 "$(field R3-nfixedargs)" # --- struct layout: the portable core's real work ----------------------- # {uint8, uint32, uint8, double}. Padding comes from each member's own # alignment, so this is arch-invariant for fixed-width members and MUST # equal the control, which read the same. check_eq libffi/offsets-status 0 "$(field R4-offsets-status)" check_eq libffi/offsets '0|4|8|16 size=24 align=8' "$(line R4-offsets)" # all-one-byte members: no padding anywhere, and the case where an # implementation that always rounded up to the word would show check_eq libffi/offsets-packed '0|1|2 size=3 align=1' "$(line R4b-offsets)" # NESTED: the inner struct's COMPUTED alignment, not its first member's, # decides where it lands. An implementation that flattened the element list # would put the third member at 24 by a different route and still pass # everything above; it could not reproduce inner=16/8. check_eq libffi/offsets-nested '0|8|24 size=32 align=8 inner=16/8' "$(line R5-offsets)" check_eq libffi/struct-return-status 0 "$(field R5b-struct-return-status)" check_eq libffi/struct-return-size '8 align=4' "$(line R5b-struct-size)" # --- the refusals ------------------------------------------------------- # A stub is only predictable if it says no in the same places a real backend # would. 2 is FFI_BAD_ABI, 1 is FFI_BAD_TYPEDEF. # # The ABI bounds are refused by the PORTABLE CORE before this port sees # them, and read the same on the control. check_eq libffi/reject-abi-first 2 "$(field R6-abi-first)" check_eq libffi/reject-abi-last 2 "$(field R6-abi-last)" check_eq libffi/reject-abi-bogus 2 "$(field R6-abi-bogus)" # FFI_TYPE_COMPLEX is not prepared from inside the probe on this arm: it # is fatal here (see the header), so the probe prints the reason in the # place the control prints a status, and `abort-complex` below is what # actually witnesses the death. Asserted rather than allowed to vanish -- # an absent pair would read the same as a probe that lost them. check_eq libffi/complex-rtype-unprobed 'n/a-aborts-upstream' "$(field R6-complex-rtype)" check_eq libffi/complex-arg-unprobed 'n/a-aborts-upstream' "$(field R6-complex-arg)" # --- the closure allocator, which is NOT reached ------------------------ # ffi_closure_alloc works as C and is packaged, and it is unlinkable here # (hwjs-cc#15, above). A probe that called it would not link, so every # case in this file would be lost -- the probe records the fact instead, # and this asserts the record so an R7 that quietly disappeared from the # probe would be a red rather than an absence nobody notices. check_eq libffi/closure-alloc-unlinkable 'not-linkable-hwjs-cc-15' \ "$(field R7-closure-alloc)" # --- the raw API -------------------------------------------------------- # No consumer on this image today; asserted because it is PACKAGED, and a # symbol that ships is a symbol someone can call. The round trip exercises # the same word-size arithmetic ffi_raw_size reports, so a wrong # FFI_SIZEOF_ARG shows up as a corrupted value rather than only as a wrong # total. check_eq libffi/raw-roundtrip 'deadbeef|-2.5|ptr-ok' "$(line R9-roundtrip)" fi fi # ------------------------------------------------------- THE RED WITNESSES --- # Each of the two programs below is SUPPOSED TO DIE. The cases fail if it does # not: a build where ffi_call started returning would satisfy an assertion that # only looked for the abort message, and returning is precisely the failure mode # the stub exists to rule out. # # They are two programs and not one because closures fail for a different reason # from calls -- a closure needs a NEW function to exist at run time, not merely a # call whose type is chosen at run time -- and a real backend could plausibly # land one before the other. A single program asserting both would stop at the # first abort and never reach the second. # # The message text is an INTERFACE, asserted verbatim. A future real backend # removes these calls; it does not reword them. witness() { _id=$1; _prog=$2; _msg=$3; _reached=$4; _staged=$5 if [ ! -x "$_prog" ]; then bst_notok "libffi/abort-$_id-present" "$_prog is not installed" bst_notok "libffi/abort-$_id-message" "no program to run" bst_notok "libffi/abort-$_id-died" "no program to run" return fi bst_ok "libffi/abort-$_id-present" _out=$("$_prog" 2>&1); _rc=$? if bst_execfail "$_out"; then bst_unrun "libffi/abort-$_id-message" "$_out" bst_unrun "libffi/abort-$_id-died" "$_out" return fi # the program got as far as the entry point under test -- so a red below is # about the abort and never about a cif or an allocation that failed first case "$_out" in *"$_staged"*) bst_ok "libffi/abort-$_id-staged" ;; *) bst_notok "libffi/abort-$_id-staged" "never reached $_staged: $(_bst_1line "$_out")" ;; esac # the documented diagnostic, verbatim case "$_out" in *"$_msg"*) bst_ok "libffi/abort-$_id-message" ;; *) bst_notok "libffi/abort-$_id-message" "want:'$_msg' got:'$(_bst_1line "$_out")'" ;; esac # THE RED WITNESS: the line past the entry point must never appear case "$_out" in *"$_reached"*) bst_notok "libffi/abort-$_id-died" \ "the entry point RETURNED -- the stub is no longer aborting: $(_bst_1line "$_out")" ;; *) bst_ok "libffi/abort-$_id-died" ;; esac # ...and it must not have exited successfully either, which is the same # statement made where a caller in a script would read it if [ "$_rc" -ne 0 ]; then bst_ok "libffi/abort-$_id-rc" else bst_notok "libffi/abort-$_id-rc" "exited 0 -- a caller would read that as success" fi } witness call $LX/libffi-abort-call \ 'libffi-wasm32-hwjs: ffi_call: unimplemented (runtime call generation unavailable); aborting' \ 'REACHED-AFTER-FFI-CALL' 'PREP-OK' # The closure witness stages on PREP-OK rather than on an allocation: its # ffi_closure is static storage, because ffi_closure_alloc cannot be linked # here (hwjs-cc#15) and a witness that does not link witnesses nothing. witness closure $LX/libffi-abort-closure \ 'libffi-wasm32-hwjs: ffi_prep_closure_loc: unimplemented (runtime call generation unavailable); aborting' \ 'RETURNED-FROM-PREP-CLOSURE' 'PREP-OK' # The third witness does NOT go through witness(), because the thing it # witnesses is the absence of a message. This death is upstream's, not this # port's: prep_cif.c answers FFI_TYPE_COMPLEX with a bare abort() on an arm # without FFI_TARGET_HAS_COMPLEX_TYPE, before this port's machdep runs. So the # case that would assert a diagnostic asserts SILENCE instead -- a consumer # carrying a complex descriptor over from another target gets no status and no # line, and that is the fact worth pinning. The day this arm gains complex # support the program stops linking (its own #error) and the probe's guard # restores the status cases. if [ ! -x $LX/libffi-abort-complex ]; then bst_notok libffi/abort-complex-present "$LX/libffi-abort-complex is not installed" bst_notok libffi/abort-complex-staged "no program to run" bst_notok libffi/abort-complex-died "no program to run" bst_notok libffi/abort-complex-silent "no program to run" bst_notok libffi/abort-complex-rc "no program to run" else bst_ok libffi/abort-complex-present cout=$($LX/libffi-abort-complex 2>&1); crc=$? if bst_execfail "$cout"; then bst_unrun libffi/abort-complex-staged "$cout" bst_unrun libffi/abort-complex-died "$cout" bst_unrun libffi/abort-complex-silent "$cout" bst_unrun libffi/abort-complex-rc "$cout" else case "$cout" in *PREP-COMPLEX-STAGED*) bst_ok libffi/abort-complex-staged ;; *) bst_notok libffi/abort-complex-staged "never reached the cif: $(_bst_1line "$cout")" ;; esac # THE RED WITNESS: ffi_prep_cif must never return for a complex type here case "$cout" in *RETURNED-FROM-PREP-CIF-COMPLEX*) bst_notok libffi/abort-complex-died \ "ffi_prep_cif RETURNED for a complex type -- complex has an answer on this arm now, so retire the witness, the probe guard and this block together: $(_bst_1line "$cout")" ;; *) bst_ok libffi/abort-complex-died ;; esac # ...and it says nothing on the way out, unlike this port's own aborts case "$cout" in *libffi-wasm32-hwjs*) bst_notok libffi/abort-complex-silent \ "a diagnostic appeared -- this death is upstream's bare abort() and carries none, so something else aborted: $(_bst_1line "$cout")" ;; *) bst_ok libffi/abort-complex-silent ;; esac if [ "$crc" -ne 0 ]; then bst_ok libffi/abort-complex-rc else bst_notok libffi/abort-complex-rc "exited 0 -- a caller would read that as success" fi fi fi