Skip to content

fix(android): make the NDK build compile and target the API floor - #20

Merged
Lynxifyy merged 2 commits into
mainfrom
lynxifyy/berryapi-android-build-fix
Nov 10, 2026
Merged

Lynxifyy merged 2 commits into
mainfrom
lynxifyy/berryapi-android-build-fix

Conversation

@Lynxifyy

Copy link
Copy Markdown
Owner

What

Two defects kept tools/build-android.sh from producing a library. Both are invisible to the host build (it never set the flag and never compiled for Android), so they only showed up on the first real on-device build.

Why

  • Exceptions. COMMON_FLAGS passed -fno-exceptions, but include/berry/hook_chain.h wraps each chain handler in try/catch to isolate a faulting handler. The translation unit failed with cannot use 'try' with exceptions disabled. Exceptions are now enabled, matching the host build, so the fault-isolation path compiles.
  • backtrace(3). core/lifecycle/crash.cpp guarded the unwinder on __linux__, but on Android it is only declared from API 33 while the build targets API 24, so the symbol was undeclared. The guard now requires glibc, or a non-Android Linux, or Android API >= 33; below that the crash report still carries the signal, the fault address and the last log line.

Evidence

tools/build-android.sh now emits libBerryAPI.so (AArch64) exporting the full ABI-8 C surface (BERRY_SetWorldSeam / BERRY_GetReplayApi / BERRY_GetGovernorApi and the struct-size/abi-version queries).

build-android: 103 C++ units, 7 Ed25519, 2 LZ4, 78 mbedTLS and 49 Luau units from tools/sources.mk
build-android: OK -> build-android/arm64-v8a/libBerryAPI.so
  Class:   ELF64
  Type:    DYN (Shared object file)
  Machine: AArch64

Verified locally with NDK 28.2.13676358; Dobby is built from the vendored source.

Lynxifyy and others added 2 commits November 10, 2026 03:00
Two defects kept tools/build-android.sh from producing a library, both only
visible on the on-device build (the host build never set the flag and never
compiled for Android):

- COMMON_FLAGS passed -fno-exceptions, but include/berry/hook_chain.h wraps each
  chain handler in try/catch to isolate a faulting handler. The translation unit
  failed with "cannot use 'try' with exceptions disabled". Exceptions are now
  enabled to match the host build, so the fault-isolation path compiles.

- core/lifecycle/crash.cpp guarded backtrace(3) on __linux__, but on Android it
  is only declared from API 33 while the build targets API 24, so the symbol was
  undeclared. The guard now requires glibc, or a non-Android Linux, or Android
  API >= 33; below that the crash report still carries the signal, the fault
  address and the last log line.

Verified: tools/build-android.sh now emits libBerryAPI.so (AArch64) exporting
the full ABI-8 C surface (BERRY_SetWorldSeam / BERRY_GetReplayApi /
BERRY_GetGovernorApi and the struct-size/abi-version queries).
)

* feat(cosmetic): add a C ABI for cosmetics and the player-model hook

The cosmetics system was C++ only, so a host that dlopen's libBerryAPI.so - a
launcher's native preloader - could not drive it without linking the whole C++
surface. berry/cosmetic_c.h closes that gap the same way berry/replay_c.h did
for the replay: a plain C struct of function pointers obtained from
BERRY_GetCosmeticApi(), with a size/abi_version guard the host checks before
calling a pointer, and a named status on every call that can fail.

The surface covers reload_packs, the catalog (list/registry_count), the local
loadout (equip/unequip/equipped/clear/canonical), presets and stats. It also
carries the player-model plumbing: set_player_model_hook installs the detour on
the host's per-build player-render target and binds a host-supplied bone reader,
which is what makes a cosmetic resolve to a real rig. Without it the render path
fails closed and draws nothing.

BerryConfigC grew cosmetic_pack_directory and cosmetic_asset_directory, so a
host can point the client at its packs through the C ABI; the global
BERRY_ABI_VERSION moves 8 -> 9 (the world-seam layout is unchanged, sizeof stays
112). BerryClient gains the public ReloadCosmeticPacks / InstallPlayerModelHook
/ ClearPlayerModelHook that both the attach path and the C ABI use, so the two
cannot drift.

Verified: make test green (incl. the new tests/test_cosmetic_c.cpp and the
extended tests/dlopen_test.c); the five BERRY_* symbols export in the shared
library.

* fix(seam): give the world seam its own ABI version

The world seam reported the global BERRY_ABI_VERSION, so any config-struct growth
moved the seam's ABI number too and the preloader (which pins
kHostSeamAbiVersion) would refuse a seam whose layout had not changed. The seam
struct now carries BERRY_SEAM_ABI_VERSION, an independent number bumped only when
BerrySeamC itself changes. Its value stays 8, so the preloader's pinned 8 keeps
matching; the global version is free to grow for config-only reasons.

* fix(cosmetic): hook a real function in the C ABI test, not a stack int

The player-model hook test passed `int hookTarget = 0` as the detour target.
The shim decodes the target's prologue as x86-64 code, and reading past the
four-byte stack variable is a stack-buffer-overflow that ASan aborts on. It
also depended on an anonymous mapping being within a rel32 of the detour and
trampoline, which the shim's jmp needs.

Hook a real function in this module instead. The prologue is a normal
function, the distance is guaranteed to fit, and the test now actually
installs and clears the hook rather than skipping on a refused target.

* fix(mbedtls): drop MBEDTLS_AESCE_C; the unit it needs is not vendored

The vendored config enabled MBEDTLS_AESCE_C, but only the trimmed client stack
is vendored and library/aesce.c is not one of its units. aes.c guards its
Armv8-A fast path on that option, so an arm64 build referenced the
mbedtls_aesce_* symbols with nothing to satisfy them: the arm64 test job failed
to link, and the shipping .so would carry the same unresolved symbols (a jump
to address 0 the first time AES ran on an arm64 device with the crypto
extension). The portable AES in aes.c is used instead.

tools/check-mbedtls.py now keeps the option off, and PROVENANCE.md records why.

Also stop the check-sigcheck self-test from tripping over the compiler's own
bytes: the pattern is the 16-byte ELF identification header, not the bare
4-byte magic, which clang embeds as an immediate in its generated loader checks
and so appears twice in a clang-built binary.

* fix(ci): green the clang and sanitize host jobs

Three independent defects, each of which the g++ job either masked or never
reached:

- check-sigcheck pinned the ELF OSABI byte (e_ident[7]) to 00, but the g++
  toolchain writes 03 (GNU/Linux). Wildcard that byte so the 16-byte
  identification pattern matches either toolchain.
- LanCosmeticTransport::Magic() returned cosmeticfrag::kMagic, a 4-byte array
  with no terminator; the test builds a std::string from it, which clang read
  past the end. Return a NUL-terminated static, as the sibling transports do.
- test_packet_capture called its fake receive function directly, so clang
  inlined the body and the call never reached the patched prologue. Mark it
  noinline and reach it through a volatile pointer.
- async.http_get cast the raw double timeout to long before the call, so a NaN
  argument was UB at the call site even though HttpGetBlocking clamps. Guard
  the cast.

---------

Co-authored-by: Lynxifyy <297587416+Lynxifyy@users.noreply.github.com>
@Lynxifyy
Lynxifyy merged commit 0faff5c into main Nov 10, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant