Skip to content

Shared C shim + error/handle model ​

The Node and WASM backends share the same “CSPICE integration semantics” via a shared C shim package:

  • Package: packages/backend-shim-c/

The shim is the canonical place where we decide:

  • how SPICE errors are captured and turned into “JS-shaped” errors
  • what an opaque “handle” means for pointer-backed objects (cells/windows)
  • which assumptions are process-global (and therefore must be serialized)

What lives in packages/backend-shim-c/ ​

  • Public headers (stable ABI):
    • packages/backend-shim-c/include/tspice_backend_shim.h
  • Internal helpers:
    • packages/backend-shim-c/include/tspice_error.h (small header-only helper for writing error strings)
  • Implementation:
    • packages/backend-shim-c/src/errors.c
    • packages/backend-shim-c/src/handle_validation.c
    • packages/backend-shim-c/src/domains/*.c

The exported functions follow a consistent naming pattern (tspice_*) and are designed to be callable from:

  • a Node native addon (C++/N-API)
  • an Emscripten module (WASM)

How the shim is consumed ​

Node backend (native addon) ​

The Node backend compiles the shim directly into the addon.

  • Build definition: packages/backend-node/native/binding.gyp
    • includes ../../backend-shim-c/src/**/*.c as sources
    • adds ../../backend-shim-c/include to include paths

This ensures the Node addon and the WASM module share identical “C-level” semantics.

WASM backend (Emscripten) ​

The WASM build includes the shim as a single translation unit:

  • packages/backend-wasm/emscripten/tspice_backend_wasm_wrapper.c

That file #includes the shim .c sources directly so the Emscripten build can compile one wrapper while still reusing the shared shim implementation.

Error model ​

Every shim function follows the same basic pattern:

  • returns 0 on success
  • returns non-zero on failure
  • writes a NUL-terminated error string into an err buffer (char* err, int errMaxBytes)

Key pieces:

  • tspice_init_cspice_error_handling_once() (in src/errors.c) configures CSPICE globally:

    • erract_c("SET", 0, "RETURN") so SPICE routines return control instead of aborting
    • errprt_c("SET", 0, "NONE") so CSPICE doesn’t print directly to stdout/stderr
  • When a SPICE call fails, tspice_get_spice_error_message_and_reset() captures:

    • SHORT message (getmsg_c("SHORT", ...))
    • LONG message (getmsg_c("LONG", ...))
    • quick trace (qcktrc_c(...))

    …stores them in process-global buffers, and calls reset_c().

Those structured buffers can be retrieved later via:

  • tspice_get_last_error_short()
  • tspice_get_last_error_long()
  • tspice_get_last_error_trace()

This lets higher-level code attach structured error fields without re-parsing a formatted string.

Handle model (cells/windows) ​

Some parts of the backend contract expose “handles” that are not CSPICE integer file handles, but opaque references to heap-allocated C structs (notably SpiceCell / SpiceWindow).

In the shim:

  • allocation functions return an opaque numeric handle (a uintptr_t cast of the pointer)
  • src/handle_validation.c maintains a process-global registry of live handles
  • every use site validates membership to prevent use-after-free

Important constraints:

  • the registry is intentionally simple and not thread-safe
  • callers must serialize access (Node does this with a global mutex; WASM is single-threaded)

Why this layer matters ​

Keeping the shim shared is how we keep Node and WASM behavior aligned:

  • consistent error capture + reset semantics
  • consistent handle validation + “expired handle” failures
  • one place to encode process-global assumptions (which would otherwise drift between backends)