Skip to content

extern "C" API lets C++/OpenVDB exceptions escape across the FFI boundary -> std::terminate (uncatchable from non-C++ bindings) #27

Description

@Borderliner

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions