Skip to content

feat(libSceAmpr): implement __cxa_call_unexpected - #6024

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
KernelPanicNT:feat/libsceampr-cxa-call-unexpected
Oct 17, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
KernelPanicNT:feat/libsceampr-cxa-call-unexpected

Conversation

@KernelPanicNT

@KernelPanicNT KernelPanicNT commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What

Implements __cxa_call_unexpected (NID usKbuvy2hQg) in libSceAmpr. Until now it was a throwing int (void) stub, added by @Pouare514 with the Deathloop loader stubs in #407 (merged); the NID first appeared in their missing-imports list #272, which they closed themselves without merging and without a stated reason. This PR replaces that stub.

Compilers emit calls to it from the landing pad of a function whose dynamic exception specification (throw(...)) an exception violates. It takes the in-flight exception's unwind header and never returns.

Source of the behaviour. No console measurement is involved. The model is LLVM libcxxabi 18.1.8 (tag llvmorg-18.1.8, commit 3b5b5c1), together with what this emulator's libc already exports:

  • cxa_personality.cpp L1171-L1211: [[noreturn]] void __cxa_call_unexpected(void* arg). A null header is passed to call_terminate (L329-L340), whose __cxa_begin_catch (cxa_exception.cpp L445-L448) reads the header's exception_class without a null check, so libcxxabi does not define the null case. Otherwise it calls __cxa_begin_catch, takes the unexpected handler stored in the exception header (or std::get_unexpected() for a foreign exception), and calls std::__unexpected with it.

  • cxa_exception.cpp L209-L214, called by __cxa_throw at L279: the header's handlers are std::get_unexpected() and std::get_terminate() at throw time. cxa_handlers.cpp L32-L37: std::__unexpected calls the handler and aborts if it returns.

  • cxa_default_handlers.cpp L88-L99: the default unexpected handler calls std::terminate(). After __cxa_begin_catch has pushed the native exception onto the caught stack (cxa_exception.cpp L470-L477), std::terminate() runs the terminate handler saved in that exception's header, and only otherwise the current one (cxa_handlers.cpp L72-L93).

  • In AnyPS5, nothing can change the unexpected handler:

    So the handler is always the default one, and the libcxxabi path reduces to: begin the catch, then run the terminate handler saved when the exception was thrown.

  • libc's existing __cxa_call_terminate: __cxa_begin_catch, then Terminate() with the current global handler, as AnyPS5's own std::terminate() does. So the result matches libcxxabi except that the current terminate handler runs instead of the one saved in the header when the exception was thrown (Runtime.cpp L283 records it). The two differ only if std::set_terminate was called between the throw and the call to __cxa_call_unexpected. The debt entry records this. For a null header this emulator's libc __cxa_begin_catch calls Terminate() itself; that is AnyPS5 behaviour, not something libcxxabi specifies.

That the console's runtime is libcxxabi-compatible here is assumed, not checked.

Changes:

  • __cxa_call_unexpected_nid_postfix gets the [[noreturn]] void (void*) signature and forwards to libc's __cxa_call_terminate_nid_postfix. This follows the earlier libSceAmpr exports that call into libc: feat(libSceAmpr): implement Dinkumware thread helpers #3080 (wcstombs_s through wcsrtombs_s, threads through pthread), feat(libSceAmpr): implement the std::_Pad thread launch helpers #3743 (std::_Pad through _Mtx/_Cnd) and feat(libSceAmpr): implement std::_Throw_Cpp_error #3679 (std::_Throw_Cpp_error through libc's exception runtime).
  • guest_ampr_threads (Linux) gains TestCallUnexpected. It uses the forked-child pattern of feat(libSceAmpr): implement the std::_Pad thread launch helpers #3743's GuestSystemErrorCode, which becomes TerminateExitCode and takes the terminate handler to install so that both tests share it: the child installs a guest terminate handler with the exported std::set_terminate, and the new handler exits with a code that encodes __cxa_current_exception_type().
    • A guest exception, built with __cxa_allocate_exception and __cxa_init_primary_exception, is passed by its unwind header. The handler runs with that exception caught (exit 42).
    • A null header runs the handler with no current exception (exit 43).
    • Exit 2 would mean the call returned.
  • The libSceAmpr technical debt entry records the model, the terminate-handler difference and the unmodelled case below.

Not modelled: a replaced unexpected handler that throws, which in libcxxabi is then checked against the violated exception specification (rethrow, or std::bad_exception). It cannot be reached while no std::set_unexpected exists. The debt entry says so, so that adding set_unexpected later revisits this function.

Progress (tools/progress.py on the branch base e27ff7c): system libraries 3269/3679 → 3270/3679; libSceAmpr 136/144 → 137/144.

Tested

Head 5b7e3aea8b38e3f627366f53e0cb92757c18760e, base e27ff7c545cb5ac669612567d853956ffa02ebe6.

  • Linux (Ubuntu 24.04, GCC 14.2.0, Ninja, Release, BUILD_TESTING=ON, AMD EPYC 7763 (GitHub Codespaces), tests run natively): guest_ampr_threads_tests passes in 5 of 5 runs, the test executable being run directly five times.
  • Negative control: keep the tests and replace only core/libs/prx/libSceAmpr/Export.cpp with the base version: guest_ampr_threads_tests fails with requirement failed at line 515. Restoring the PR source and rebuilding, guest_ampr_threads_tests passes in 5 of 5 runs again.
  • Windows, full Build workflow on my fork (run 38736530145, attempt 2; attempt 1 stopped at configure when the runner could not download FFmpeg), this head built together with the updated heads of feat(libc): scan 16-bit wide conversions in sscanf and vsscanf on Linux #6017, feat(libc): scan 16-bit wide conversions in fscanf on Linux #6018 and feat(libSceAmpr): implement __cxa_call_unexpected #6024 and merged into the fork's main at b08db6a093f440af697d71ea907cec5f675fcdc5, WinLibs MinGW-w64 GCC 15.2.0, Release, BUILD_TESTING=ON: 1278/1279 tests pass, including guest_ampr_threads. The one failure, prx_thread_local_destructors, reports a thread_local destructor in libSceAgcDriver (traceDrawKeyLookup), which main gained in de2ddda the same day; this PR does not touch that library. The relinker (Linux, Windows, macOS), register-context and write-watch jobs of the same run pass.
  • Conventions checker and git diff --check pass. The Linux job of the Build workflow now targets the project's self-hosted runner, so it was not run on the fork; no title was run.

Duplicate and overlap check

  • I searched open, merged and closed PRs for __cxa_call_unexpected, cxa_call_unexpected and the NID usKbuvy2hQg. The only hits are feat(libs): Deathloop loading support (Ampr, NetCtlApDialog + stubs) #407 (merged; added the throwing stub) and docs(nids): announce deathloop-ppsa01670 missing imports #272 (closed unmerged by its author with no stated reason; a list of missing imports), both by @Pouare514. No open PR implements the function or names it.
  • No open PR touches core/libs/prx/libSceAmpr/Export.cpp, core/libs/tests/GuestAmprThreads.cpp or docs/dev/debt/libSceAmpr.md, so there is no textual-conflict risk. The libc exception runtime is only called, not changed.
  • The branch is one commit based on e27ff7c, so it merges without conflicts.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes (Claude)

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Progress report (2e5dd5a → 5b7e3ae)

📈 System libraries: 88.72% (+0.02%, +1 implemented functions)

✅ 1 implemented
Library Name
libSceAmpr __cxa_call_unexpected_nid_postfix

__cxa_call_unexpected takes the in-flight exception's unwind header and
never returns, as in libcxxabi. No std::set_unexpected is exported and
the exception header's unexpected handler is never set, so the handler
is always the default one, which calls std::terminate. It therefore
forwards to libc's __cxa_call_terminate, which begins the catch and runs
the current terminate handler; a null header terminates in libc's
__cxa_begin_catch. libcxxabi's std::terminate would instead run the
terminate handler saved in the exception header at throw time; the two
differ only if std::set_terminate was called in between, which the debt
entry records.

The test shares the forked-child helper of the std::_Pad test, which now
takes the terminate handler to install.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 6044 core/libs/tests/GuestAmprThreads.cpp
PR 6447 core/libs/prx/libSceAmpr/Export.cpp

@adriadam10

Copy link
Copy Markdown
Collaborator

Checked build and ctest (37/37 in scope, guest_ampr_threads passes), and the new test fails with the old Export.cpp. The forwarding to libc's __cxa_call_terminate matches the libcxxabi path given that no unexpected handler can be set, and the terminate-handler difference is recorded in the debt entry. Merging.

@adriadam10
adriadam10 merged commit 30dadc2 into boykopovar:main Oct 17, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants