extern "C" API lets C++/OpenVDB exceptions escape across the FFI boundary → std::terminate (uncatchable from non-C++ bindings)
Affected version: PicoGK-v2.2.0 (PicoGKRuntime, commit 0f26321). Reported by PicoPie, a Pythonic (ctypes) binding of PicoGK.
Summary
The C ABI in Source/PicoGKLibrary.cpp exposes ~173 extern "C" PICOGK_API functions, none of which catch C++ exceptions. When an internal operation (OpenVDB, the STL, etc.) throws, the exception propagates out of an extern "C" function — which is undefined behaviour and in practice calls std::terminate, aborting the whole host process. From any non-C++ binding (Python ctypes/cffi, Rust FFI, etc.) this is uncatchable: there is no C++ stack to unwind into, so the host app dies instead of getting an error it can handle.
Examples that abort the process today
- A CSG boolean / second implicit intersect on a grid that is no longer a valid level set → OpenVDB throws
ValueError("expected grid A outside value > 0…").
- An out-of-range VDB field index → the accessors use
std::vector::at(), which throws std::out_of_range.
- (Before the sibling narrow-band issue is fixed) an implicit intersect at a fine voxel size throws the same OpenVDB
ValueError.
In each case a C# caller can try/catch, but a C-ABI caller cannot — the throw crosses extern "C" and terminates.
Suggested fix
Wrap every PICOGK_API body in a try/catch that records the error and returns a type-appropriate sentinel, and expose a get/clear-last-error pair so bindings can poll after each call and raise their own exception. Sketch:
static thread_local std::string g_lastError;
extern "C" int g_lastErrorFlag = 0;
PICOGK_API void PicoGK_SetError(const char* psz) { g_lastError = psz ? psz : "unknown"; g_lastErrorFlag = 1; }
PICOGK_API int PicoGK_nGetLastError(char* psz, int nMax) { /* copy g_lastError out */ }
#define PICOGK_TRY g_lastErrorFlag = 0; try {
#define PICOGK_CATCH(sentinel) } \
catch (const std::exception& e) { PicoGK_SetError(e.what()); return sentinel; } \
catch (...) { PicoGK_SetError("unknown native error"); return sentinel; }
…with the sentinel chosen per return type (void→;, bool→false, handle/int→0, float→NaN). The binding reads g_lastErrorFlag after each call (cheap; no extra native call) and raises. This turns any native error — even ones not anticipated — into an ordinary catchable exception instead of a process abort, with no behavioural change for the C# wrapper.
Context
We currently apply exactly this transform to the pinned runtime at build time in PicoPie so a fuzz campaign of degenerate inputs (NaN/inf, empty grids, repeated CSG) never aborts the process. We'd much rather this live upstream. Note try/catch does not cover hard faults (a nullptr/NaN reaching native math can still SIGSEGV, and a far-away query point can hang) — those are best guarded at the binding boundary — but converting the large class of thrown C++ exceptions into catchable errors is the high-value change.
Happy to send a PR (we have the build-time transform already).
Reported by PicoPie — a Pythonic binding of PicoGK.
extern "C"API lets C++/OpenVDB exceptions escape across the FFI boundary →std::terminate(uncatchable from non-C++ bindings)Affected version: PicoGK-v2.2.0 (
PicoGKRuntime, commit0f26321). Reported by PicoPie, a Pythonic (ctypes) binding of PicoGK.Summary
The C ABI in
Source/PicoGKLibrary.cppexposes ~173extern "C"PICOGK_APIfunctions, none of which catch C++ exceptions. When an internal operation (OpenVDB, the STL, etc.) throws, the exception propagates out of anextern "C"function — which is undefined behaviour and in practice callsstd::terminate, aborting the whole host process. From any non-C++ binding (Pythonctypes/cffi, Rust FFI, etc.) this is uncatchable: there is no C++ stack to unwind into, so the host app dies instead of getting an error it can handle.Examples that abort the process today
ValueError("expected grid A outside value > 0…").std::vector::at(), which throwsstd::out_of_range.ValueError.In each case a C# caller can
try/catch, but a C-ABI caller cannot — the throw crossesextern "C"and terminates.Suggested fix
Wrap every
PICOGK_APIbody in atry/catchthat records the error and returns a type-appropriate sentinel, and expose a get/clear-last-error pair so bindings can poll after each call and raise their own exception. Sketch:…with the sentinel chosen per return type (
void→;,bool→false, handle/int→0,float→NaN). The binding readsg_lastErrorFlagafter each call (cheap; no extra native call) and raises. This turns any native error — even ones not anticipated — into an ordinary catchable exception instead of a process abort, with no behavioural change for the C# wrapper.Context
We currently apply exactly this transform to the pinned runtime at build time in PicoPie so a fuzz campaign of degenerate inputs (NaN/inf, empty grids, repeated CSG) never aborts the process. We'd much rather this live upstream. Note
try/catchdoes not cover hard faults (anullptr/NaN reaching native math can stillSIGSEGV, and a far-away query point can hang) — those are best guarded at the binding boundary — but converting the large class of thrown C++ exceptions into catchable errors is the high-value change.Happy to send a PR (we have the build-time transform already).
Reported by PicoPie — a Pythonic binding of PicoGK.