Skip to content

Harden the C ABI: catch C++ exceptions instead of terminating the process (#27) - #29

Open
Borderliner wants to merge 1 commit into
leap71:mainfrom
Borderliner:harden/never-abort-c-abi
Open

Harden the C ABI: catch C++ exceptions instead of terminating the process (#27)#29
Borderliner wants to merge 1 commit into
leap71:mainfrom
Borderliner:harden/never-abort-c-abi

Conversation

@Borderliner

Copy link
Copy Markdown

Addresses #27 — harden the C ABI so a thrown C++/OpenVDB exception becomes a catchable error in the caller instead of a process abort.

Problem

Every extern "C" PICOGK_API function can let a C++ exception propagate across the FFI boundary. That's undefined behaviour and in practice calls std::terminate, killing the host process. A C# caller can try/catch; a C-ABI binding (Python ctypes, Rust FFI, …) cannot — there's no C++ frame to unwind into. Examples that abort today: a CSG/second implicit intersect on a non-level-set grid (OpenVDB ValueError), an out-of-range VDB field index (std::vector::atstd::out_of_range).

Change (Source/PicoGKLibrary.cpp only)

  • Add a small infra block: PicoGK_SetError / PicoGK_nGetLastError and an exported g_pkLastErrorFlag, plus PICOGK_GUARD_TRY / PICOGK_GUARD_CATCH(sentinel) macros.
  • Wrap the body of each of the 173 PICOGK_API functions in try/catch, returning a type-appropriate sentinel (void;, boolfalse, handle/int→0, floatNaN). A binding clears the flag on entry, reads it after each call, and raises.

The diff is large but entirely mechanical — the reviewable core is the ~40-line infra block at the top and the uniform two-line wrap repeated per function. It's produced by an idempotent script, so it can be re-generated for future runtime versions rather than hand-maintained.

Properties

  • No behavioural change for calls that don't throw (flag cleared on entry, set only in a catch).
  • The C# wrapper is unaffected (it never reads the flag).
  • Does not cover hard faults (SIGSEGV from e.g. a NaN reaching native math) or hangs — those are best guarded at the binding boundary; this converts the large class of thrown exceptions.

Verification

This exact transform is what PicoPie applies to the pinned runtime at build time; it compiles and ships on Linux, macOS, and Windows in PicoPie's wheel CI, and a subprocess fuzz campaign of degenerate inputs (NaN/inf, empty grids, repeated CSG) runs with zero process aborts.

Open to adjustment

Happy to tailor the approach to your preferences — e.g. making the last-error string thread_local for multi-threaded API use (currently a single file-scope global), the symbol names, or limiting the scope to the throw-prone subset rather than all functions.


Reported & implemented via PicoPie, a Pythonic binding of PicoGK.

…cess

Every extern "C" PICOGK_API function can currently let a C++/OpenVDB
exception propagate across the FFI boundary, which is UB and in practice
calls std::terminate and aborts the host process -- uncatchable from
non-C++ bindings (Python ctypes, Rust FFI, ...). See leap71#27.

Wrap each PICOGK_API body in a try/catch that records a last-error string,
sets a flag, and returns a type-appropriate sentinel (void -> ;,
bool -> false, handle/int -> 0, float -> NaN), and add PicoGK_SetError /
PicoGK_nGetLastError plus an exported g_pkLastErrorFlag so a binding can
poll after each call and raise its own exception.

No behavioural change for calls that don't throw (the flag is cleared on
entry and set only on exception). Does not cover hard faults (SIGSEGV) or
hangs -- those remain the binding's responsibility.

Generated mechanically (idempotent) so it can be re-applied across runtime
versions; in PicoPie it currently runs as a build-time transform.

Addresses leap71#27.
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