Tcl NgspiceTclBridge package (v0.1)

Notes and internalsTop, Main, Index

This part is mostly for me (and other contributors) to quick recall many implementation details and decisions. All information provided here could be found in source code comments, but in more spreaded form.

How instance subcommands are implementedTop, Main, Index

Each simulator instance in ngspicetclbridge is represented by a dedicated Tcl command object that acts as a handle for interacting with that specific ngspice context. This provides natural Tcl semantics such as:

set s [::ngspicetclbridge::new /path/to/libngspice.so]
$s init
$s command "bg_run"
$s waitevent bg_running -n 2

Every $s ... invocation calls into the same underlying C function, InstObjCmd, which dispatches individual subcommands.

Command creationTop, Main, Index

When the package initializes, Ngspicetclbridge_Init() registers the constructor command:

::ngspicetclbridge::new

implemented by the C function NgSpiceNewCmd(). When this command is invoked, it:

1. Allocates and zero-initializes an NgSpiceContext structure.

2. Initializes all mutexes, condition variables, message queues (MsgQueue msgq, MsgQueue capq), and data buffers (DataBuf prod, DataBuf pend).

3. Loads the ngspice shared library dynamically using PDl_OpenFromObj(), resolving function pointers such as ngSpice_Init, ngSpice_Command, ngGet_Vec_Info, etc.

4. Creates a unique Tcl command (e.g., ::ngspicetclbridge::s1) via Tcl_CreateObjCommand(), associating the new NgSpiceContext * as its ClientData.

5. Registers a delete procedure (InstDeleteProc) so that when the command is deleted (via $s destroy or interpreter shutdown), the associated context is safely torn down.

The resulting Tcl command becomes the primary interface for controlling the simulator instance.

Subcommand dispatchingTop, Main, Index

The C entry point for instance methods is:

static int InstObjCmd(ClientData cdata, Tcl_Interp *interp, Tcl_Size objc, Tcl_Obj *const objv[])

Here, cdata is the NgSpiceContext * pointer for this instance. The dispatcher parses objv[1] as the subcommand name (e.g., "command", "vectors", etc.) and executes the corresponding branch of code.

Each subcommand performs specific work, typically involving interaction with the ngspice API or the context’s internal data structures. For example:

The dispatcher uses structured error handling: most subcommands validate argument counts and options before invoking any ngspice function, returning descriptive Tcl errors via Tcl_SetObjResult().

Thread safety and state transitionsTop, Main, Index

ngspice commands may not be executed safely during certain background-thread transitions. To prevent unsafe API access, the context maintains a state machine (NgState) with states such as:

Before any ngspice command is executed, InstObjCmd checks this state under ctx->bg_mu. If ngspice is starting or stopping its background thread, commands are not executed immediately but queued for later execution once the state stabilizes.

Deferred command queueTop, Main, Index

Commands issued while ngspice is in a transitional state (STARTING_BG or STOPPING_BG) are enqueued through:

EnqueuePending(ctx, cmd, do_capture);

This stores the command string and capture flag in a linked list protected by ctx->cmd_mu. Once ngspice reports that the background thread has started or stopped, BGThreadRunningCallback() calls:

FlushPending(ctx);

which replays all queued commands through ngSpice_Command() safely on the main thread.

This mechanism ensures no commands are lost or executed while ngspice’s internal state is inconsistent, eliminating race conditions between Tcl commands and asynchronous background transitions.

Capture modeTop, Main, Index

For commands executed in capture mode, the bridge uses ctx->cap_active and ctx->capq to store output lines printed by ngspice’s SendCharCallback during the command’s lifetime. Once the command returns, captured lines are collected into a Tcl list and returned to the caller along with the return code.

Command deletion and cleanupTop, Main, Index

When a user calls $s destroy or deletes the instance command, Tcl invokes the registered delete procedure:

static void InstDeleteProc(void *cdata)

This initiates a full teardown:

1. Sets ctx->destroying = 1 to block further callbacks.

2. Stops or waits for the background thread to end cleanly.

3. Sends ngSpice_Command("quit") unless the shutdown was unsafe.

4. Waits for ControlledExitCallback() to signal completion.

5. Deletes pending events referencing this context.

6. Schedules deferred destruction via Tcl_EventuallyFree(ctx, InstFreeProc).

InstFreeProc() then performs the final cleanup: releasing Tcl objects, freeing message queues, finalizing mutexes, and optionally unloading the shared library if it’s safe to do so.

SummaryTop, Main, Index

The combination of per-instance Tcl command objects, subcommand dispatching, and deferred command buffering provides a robust interface between Tcl and ngspice. Tcl scripts can issue commands synchronously while ngspice runs asynchronously in the background, and all interactions remain thread-safe and deterministic even during start, stop, and teardown transitions.

How sync callbacks from Ngspice is implementedTop, Main, Index

ngspicetclbridge connects the asynchronous world of ngspice with Tcl by registering a set of C callback functions that ngspice calls whenever important events occur during simulation. These callbacks serve as the bridge between ngspice’s internal simulation threads and Tcl’s cooperative event loop. Because Tcl is not thread-safe, none of these callbacks interact with the interpreter directly — instead, they enqueue Tcl events to be processed later on the main thread.

Registration of callbacksTop, Main, Index

The binding between ngspice and our bridge is established by calling ngSpice_Init() with a set of function pointers and a user data pointer (ctx):

int rc = ctx->ngSpice_Init(
    SendCharCallback,
    SendStatCallback,
    ControlledExitCallback,
    SendDataCallback,
    SendInitDataCallback,
    BGThreadRunningCallback,
    ctx
);

The prototype from the ngspice shared library is:

int ngSpice_Init(
    SendChar*,
    SendStat*,
    ControlledExit*,
    SendData*,
    SendInitData*,
    BGThreadRunning*,
    void* user
);

The final void *user parameter is an opaque pointer passed back to every callback invocation; we use it to carry a pointer to our NgSpiceContext. This allows each callback to know which simulator instance it belongs to, even when multiple instances are loaded simultaneously.

Callback invocation modelTop, Main, Index

ngspice runs its own simulation threads internally, so callbacks can fire at any time — during steady-state DC/AC/transient analysis, at startup, or when shutting down. The order and timing depend entirely on the simulation type and ngspice’s internal scheduler. Common callback types include:

Callback Function Trigger Condition / Purpose
SendCharCallback Emits text lines printed by ngspice (stdout/stderr).
SendStatCallback Emits status messages such as “tran simulation complete”.
ControlledExitCallback Invoked when ngspice is about to terminate, either via “quit” or internal exit.
SendDataCallback Called each time a new data point (vector values) is available during simulation.
SendInitDataCallback Called once at the start of a run, delivering vector metadata (vecinfoall).
BGThreadRunningCallback Indicates that ngspice’s background thread has started or stopped.

Thread safety and queueing behaviorTop, Main, Index

Each callback runs on a non-Tcl thread owned by ngspice, meaning direct interaction with Tcl objects or interpreter state would be unsafe. To handle this, each callback function performs the following steps:

1. Check for teardown: If ctx->destroying is set, the callback immediately returns — it never queues new events during or after teardown.

2. Acquire context mutex: Locks such as ctx->mutex or specialized ones like ctx->bg_mu are used to update shared state safely.

3. Write shared data: Depending on the callback type, the callback may:

4. Signal condition variables: The callback calls Tcl_ConditionNotify() on ctx->cond, ctx->bg_cv, or ctx->exit_cv to wake threads waiting in Tcl commands like waitevent or during shutdown.

5. Queue a Tcl event: Finally, it calls NgSpiceQueueEvent(ctx, callbackId, ctx->gen), which allocates an NgSpiceEvent and schedules it in the Tcl event queue via Tcl_QueueEvent. Tcl will later call NgSpiceEventProc() on the interpreter thread to process the event.

Controlled flow of dataTop, Main, Index

Data-oriented callbacks (SendInitDataCallback and SendDataCallback) are slightly special:

Both callbacks only store the data temporarily — they never construct Tcl objects directly. The transformation into Tcl dictionaries and lists happens later, inside NgSpiceEventProc(), which runs safely on the Tcl thread.

Background thread lifecycle callbacksTop, Main, Index

The BGThreadRunningCallback provides reliable notification of when the simulation thread starts or stops. It updates ctx->bg_started and ctx->bg_ended under ctx->bg_mu, adjusts the ctx->state (STARTING_BG, BG_ACTIVE, STOPPING_BG, IDLE), and signals ctx->bg_cv so that other parts of the system can wait for these transitions. If the state change makes it safe to execute deferred commands, it calls FlushPending(ctx) to send any commands that were queued during startup or shutdown.

Generation and stale-event handlingTop, Main, Index

Each callback carries a generation number (ctx->gen) that uniquely identifies a simulation run. Every time a new SendInitDataCallback is fired, the generation counter increments. All subsequent callbacks within that run inherit the same generation value. When Tcl later processes events via NgSpiceEventProc, any event with a mismatched generation number is ignored — protecting against stale callbacks from a previous run that might reference freed memory.

Controlled exit coordinationTop, Main, Index

The ControlledExitCallback is the mechanism ngspice uses to indicate that it is shutting down cleanly. It sets ctx->exited = 1, signals ctx->exit_cv, and queues a CONTROLLED_EXIT event. This synchronization is crucial for teardown safety: InstDeleteProc() waits on ctx->exit_cv to ensure all ngspice activity has ceased before freeing memory.

Safety and teardown integrationTop, Main, Index

All callbacks are designed to be idempotent and safe to call during teardown:

Together, these mechanisms ensure that ngspice’s asynchronous notifications are translated into Tcl events in a fully thread-safe, deterministic manner — without ever touching Tcl state from non-Tcl threads and without risking race conditions or double frees during instance shutdown.

How event queue interaction is implementedTop, Main, Index

The interaction between ngspice’s asynchronous callbacks and Tcl’s single-threaded event loop is the core of how ngspicetclbridge maintains safe communication between both worlds. ngspice runs its own internal threads and can call back into user code at arbitrary times, so a direct call into Tcl would violate thread safety. Instead, every callback from ngspice queues a lightweight Tcl_Event that is later processed on the main Tcl thread.

Event structure definitionTop, Main, Index

Each pending event from ngspice is represented by a custom structure that embeds a Tcl event header:

typedef struct {
    Tcl_Event header;          /* must be first for Tcl to recognize it */
    NgSpiceContext *ctx;       /* simulator instance that owns this event */
    int callbackId;            /* numeric identifier for callback type */
    uint64_t gen;              /* generation counter (run ID) */
} NgSpiceEvent;

The header.proc field (set when the event is queued) points to the function that Tcl will invoke when it processes this event — in our case, NgSpiceEventProc(). The other fields are used to identify which ngspice instance and which callback the event corresponds to, and to ensure that events from a previous run (“generation”) are discarded safely once a new run starts.

Event allocation and queuingTop, Main, Index

Whenever an ngspice callback is triggered (for example SendDataCallback, SendInitDataCallback, ControlledExitCallback, or BGThreadRunningCallback), it calls:

static void NgSpiceQueueEvent(NgSpiceContext *ctx, int callbackId, uint64_t gen)

This function allocates a new NgSpiceEvent via ckalloc(), sets its header.proc = NgSpiceEventProc, fills in the ctx, callbackId, and current generation gen, and finally calls:

Tcl_QueueEvent((Tcl_Event *)e, TCL_QUEUE_TAIL);

to place it at the end of Tcl’s event queue. Tcl guarantees that the event will be processed on the thread that created the interpreter, regardless of which ngspice thread scheduled it.

Because the callback may race with instance deletion, each queued event increases the lifetime of its context using:

Tcl_Preserve((ClientData)ctx);

which prevents Tcl from freeing NgSpiceContext until all events referencing it have been processed.

Event processingTop, Main, Index

Once Tcl’s event loop reaches the queued item, it calls:

static int NgSpiceEventProc(Tcl_Event *ev, int flags)

Inside this function, we unpack the event, verify that the NgSpiceContext still exists, and check whether the event’s gen field matches the current generation counter in the context. If not, the event is stale and immediately discarded to avoid processing old data from previous runs.

Depending on the callbackId, the procedure performs different tasks:

Once processed, the event always returns 1 to indicate it should be removed from Tcl’s event queue.

Event deletion and resource balancingTop, Main, Index

Tcl supports an optional “delete procedure” for custom events, allowing cleanup of resources that were preserved when the event was queued. ngspicetclbridge uses:

static int DeleteNgSpiceEventProc(Tcl_Event *evPtr, ClientData cd)

which checks if the event belongs to the same context and, if so, performs:

Tcl_Release((ClientData)e->ctx);

balancing the Tcl_Preserve() from NgSpiceQueueEvent(). This ensures that a context is only freed after all its events are gone from Tcl’s queue, even if they were never processed because the interpreter exited or the instance was deleted early.

Generation handling and safetyTop, Main, Index

The generation number (ctx->gen) plays a critical role in keeping old events from corrupting state after a new run starts. Each time SendInitDataCallback fires, the bridge increments ctx->gen. Every event carries a snapshot of that number, and when NgSpiceEventProc() runs, it compares it to the current value. If they don’t match, the event is silently ignored.

This mechanism prevents dangerous use-after-free situations where an asynchronous callback could deliver data from a previously freed buffer (e.g., InitSnap or DataBuf). Only events belonging to the current run are processed; all older generations are discarded.

Synchronization guaranteesTop, Main, Index

Together, this ensures that ngspice’s asynchronous event model integrates cleanly into Tcl’s cooperative scheduler, without risking memory corruption, premature frees, or cross-thread Tcl calls.

How thread safety is ensuredTop, Main, Index

Thread safety in ngspicetclbridge is achieved through a combination of fine-grained mutexes, conditional variables, and a strict rule that all Tcl interactions happen only from the thread that created the NgSpiceContext.

Each NgSpiceContext has its own set of synchronization primitives:

Mutex / Condition Purpose
mutex Protects shared state such as msgq, capq, vectorData, vectorInit, and event counters.
cmd_mu Guards the pending command queue used during transitional states (like bg_run start/stop).
bg_mu Synchronizes access to background-thread state variables (bg_started, bg_ended, state).
exit_mu Protects exited flag and coordinates teardown with ControlledExitCallback.
cond Signals changes in event counters and is used by waitevent to sleep until events fire.
bg_cv Used by BGThreadRunningCallback to notify that background thread has started or ended.
exit_cv Used to signal that ngspice has exited, waking InstDeleteProc.

ngspice callbacks are invoked from its internal simulation threads, never from Tcl. These threads are not allowed to manipulate Tcl objects directly; instead, they enqueue Tcl events via Tcl_QueueEvent. The event structure (NgSpiceEvent) includes a pointer to the NgSpiceContext, a callback ID, and a generation counter to ensure stale events are discarded safely.

Data flow always follows the same synchronization pattern:

Background thread control (bg_run, bg_halt, etc.) uses explicit state transitions (NgState) guarded by bg_mu. Any Tcl command issued while ngspice is in a transitional state (STARTING_BG or STOPPING_BG) is deferred through EnqueuePending(), preventing concurrent access to ngspice API functions. Deferred commands are flushed later from the main Tcl thread after the background thread state becomes stable.

The destroying flag acts as a global fence against late or concurrent operations. Once set, all callbacks, deferred commands, and waitevents are short-circuited. Final cleanup (InstFreeProc) only runs after all callbacks have ceased and all condition variables are finalized, ensuring no dangling activity can touch freed memory.

This model isolates all ngspice background activity from Tcl evaluation, guaranteeing thread safety while still allowing asynchronous simulation and command queuing to coexist without locking the Tcl interpreter.

How data processing is implementedTop, Main, Index

ngspicetclbridge maintains two complementary data paths for handling simulation results from ngspice:

Both paths ultimately present Tcl-side data in native structures — dictionaries and lists of numeric values — but their lifetimes, timing, and ownership differ.

Overview of the data flowTop, Main, Index

1. ngspice calls SendInitDataCallback() → bridge creates and fills InitSnap with vector metadata.

2. Tcl event is queued (SEND_INIT_DATA) → later processed by NgSpiceEventProc(), which converts metadata into the Tcl dictionary ctx->vectorInit.

3. ngspice calls SendDataCallback() → bridge appends numeric samples to ctx->prod (DataBuf).

4. Tcl event is queued (SEND_DATA) → later processed by NgSpiceEventProc(), which transfers rows from ctx->prod into the Tcl dictionary ctx->vectorData.

5. Once processed, the internal buffers are cleared, leaving the Tcl objects as the authoritative copy of simulation results.

This staged handoff model allows ngspice’s worker threads to push raw data asynchronously without ever touching Tcl-managed memory, and Tcl to consume data safely on its main thread.

Asynchronous access structuresTop, Main, Index

ngspice provides a runtime API for querying vector values at any point using ngGet_Vec_Info(). The structure returned is defined as:

typedef struct vector_info {
    char *v_name;
    int v_type;
    short v_flags;
    double *v_realdata;
    ngcomplex_t *v_compdata;
    int v_length;
} vector_info, *pvector_info;

The v_flags field encodes the type of the vector (real or complex) and other display or accumulation properties. If the VF_COMPLEX flag is set, values are stored in an array of ngcomplex_t structures, each containing {cx_real, cx_imag} pairs. Otherwise, they are plain doubles in v_realdata.

The Tcl subcommand asyncvector reads these structures directly by calling ctx->ngGet_Vec_Info(), then returns a simple Tcl list:

Because this function directly queries ngspice’s in-memory storage, it does not depend on the event loop or any intermediate buffering. It is safe to call between runs, though the vector content reflects ngspice’s internal state at the time of the query.

Synchronous access structures (event-driven)Top, Main, Index

When simulations run in the background (bg_run), ngspice periodically calls SendDataCallback() to deliver updated data for all vectors in the current plot. The callback receives a vecvaluesall structure:

typedef struct vecvalues {
    char* name;
    double creal;
    double cimag;
    NG_BOOL is_scale;
    NG_BOOL is_complex;
} vecvalues, *pvecvalues;

typedef struct vecvaluesall {
    int veccount;
    int vecindex;
    pvecvalues *vecsa;
} vecvaluesall, *pvecvaluesall;

For each data point (or “row”), the callback iterates through all vectors, constructs lightweight DataCell entries, and appends them to a DataRow inside the DataBuf buffer owned by ctx. The bridge stores only primitive doubles and small strings — it does not allocate Tcl objects here.

The complementary initialization callback, SendInitDataCallback(), provides metadata for these vectors before any data points are sent. It receives a vecinfoall structure describing all available vectors and fills a corresponding InitSnap structure:

typedef struct {
    int veccount;
    struct {
        char *name;
        int number;
        int is_real;
    } *vecs;
} InitSnap;

This snapshot serves as the source for ctx->vectorInit once the event is processed on the Tcl side.

Internal buffering and memory layoutTop, Main, Index

The in-memory storage for raw data uses nested structures designed for efficient append and event-driven transfer:

typedef struct {
    char *name;
    int is_complex;
    double creal, cimag;
} DataCell;

typedef struct {
    int veccount;
    DataCell *vecs;
} DataRow;

typedef struct {
    DataRow *rows;
    size_t count, cap;
} DataBuf;

Memory is dynamically resized as new data arrives. Once the Tcl event handler (NgSpiceEventProc) transfers the buffered rows into the Tcl dictionary, DataBuf_Free() clears the contents to release memory and reset the counters.

Conversion into Tcl objectsTop, Main, Index

When Tcl processes the SEND_INIT_DATA or SEND_DATA events, it transforms the internal C buffers into Tcl data structures:

The conversion always occurs under ctx->mutex to prevent concurrent modification during background activity.

Data lifecycle and cleanupTop, Main, Index

This ensures that every simulation run starts with a clean state and that all memory ownership is consistent between Tcl and ngspice.

Thread safety in data handlingTop, Main, Index

Together, these layers implement a robust, zero-copy, thread-safe data pipeline from ngspice’s internal solver to Tcl-accessible variables, capable of handling both real-time streaming and post-run inspection scenarios.

How message queue is implementedTop, Main, Index

ngspicetclbridge provides a unified and thread-safe mechanism for collecting text output and diagnostic messages emitted by ngspice during simulation. This includes lines printed to ngspice’s stdout/stderr streams, status messages, and optionally captured command output when -capture mode is active.

OverviewTop, Main, Index

Each NgSpiceContext maintains two independent message queues:

Both queues use the same lightweight data structure MsgQueue, defined as:

typedef struct {
    char **items;
    size_t count;
    size_t cap;
} MsgQueue;

The queue maintains a dynamically allocated array of C strings and tracks both the number of stored messages (count) and the total allocated capacity (cap). Each new message is appended as a heap-allocated copy, and capacity grows exponentially when needed.

Message production (ngspice callbacks)Top, Main, Index

ngspice produces text output through callbacks SendCharCallback and SendStatCallback. These callbacks are invoked asynchronously from ngspice’s internal threads and may fire at any moment. To safely handle these messages:

1. The callback locks ctx->mutex to synchronize access to shared state.

2. The callback calls QueueMsg(ctx, line) which appends the given text to ctx->msgq.

3. If ctx->cap_active is set (i.e., a $s command -capture ... is in progress), the same line is also appended to ctx->capq for later retrieval by the capturing command.

4. The callback signals Tcl_ConditionNotify(&ctx->cond) to wake any threads waiting in $s waitevent or $s messages -wait.

5. Finally, the callback unlocks ctx->mutex before returning.

The QueueMsg() function is responsible for ensuring capacity growth, copying the message, and maintaining null-termination of the array. It performs no Tcl operations, keeping it completely thread-safe.

Message consumption (Tcl commands)Top, Main, Index

On the Tcl side, messages are retrieved by calling $s messages. This subcommand locks the same ctx->mutex, iterates over the msgq entries, and returns them as a Tcl list:

$s messages
→ { "# circuit initialized" "# transient analysis running" "# background thread running started" ... }

Optionally, $s messages -clear clears the message queue after reading, allowing the caller to poll for new messages incrementally.

Because msgq is append-only and all reads occur under the same mutex, there is no race condition between message production and consumption. Each message remains valid until explicitly cleared or freed.

Capture mode (-capture)Top, Main, Index

The capture mechanism allows a single $s command call to return the exact lines ngspice printed while executing a specific command. This is handled transparently inside the command branch:

1. Before invoking ngSpice_Command(), the bridge:

2. ngspice executes the command, possibly emitting messages via SendCharCallback. 3. Each emitted line is recorded in both msgq (global) and capq (local). 4. After the command finishes, the bridge:

5. The Tcl result is a dictionary:

{ rc <integer> output { "<line1>" "<line2>" ... } }

where rc is the return code from ngspice and output is the captured text.

This dual-queue approach ensures that capture mode never interferes with the persistent global message stream, while still providing deterministic per-command output isolation.

Clearing and freeing queuesTop, Main, Index

Both message queues can be cleared or destroyed via:

void MsgQ_Clear(MsgQueue *q);
void MsgQ_Free(MsgQueue *q);

The queues are cleared at multiple stages:

Synchronization and safety guaranteesTop, Main, Index

SummaryTop, Main, Index

The message queue subsystem provides a clean, thread-safe bridge between ngspice’s asynchronous textual output and Tcl’s event-driven interface. It ensures:

How dynamic library loading and portability is implementedTop, Main, Index

ngspicetclbridge is designed to operate seamlessly across all major platforms supported by Tcl — primarily Linux, macOS, and Windows — using a unified abstraction layer for dynamic library management. Because the simulator backend (ngspice) is distributed as a shared library (libngspice.so, libngspice.dylib, or ngspice.dll), the bridge must dynamically load it at runtime, resolve function symbols, and safely unload it during shutdown.

Abstracted portability layerTop, Main, Index

All platform differences in dynamic library handling are encapsulated in a small portability layer consisting of three functions and an opaque handle type:

typedef void *PDlHandle;

PDlHandle PDl_OpenFromObj(Tcl_Interp *interp, Tcl_Obj *pathObj);
void *PDl_Sym(PDlHandle handle, const char *sym);
void PDl_Close(PDlHandle handle);

Internally, this layer maps directly to the platform’s native API:

This makes the rest of the code platform-agnostic. The bridge never calls dlopen or LoadLibrary directly — only through this wrapper.

Unicode and Tcl path integrationTop, Main, Index

A key feature of the wrapper is that it accepts a Tcl path object (Tcl_Obj *pathObj) instead of a C string. This allows proper handling of Unicode and cross-platform path semantics:

This guarantees that paths with non-ASCII characters (e.g., C:/ユーザー/ngspice.dll) are handled correctly on all platforms.

Function symbol resolutionTop, Main, Index

After opening the shared library, NgSpiceNewCmd() uses PDl_Sym() to resolve all required function symbols dynamically. Each symbol pointer is stored in the instance’s NgSpiceContext:

ctx->ngSpice_Init      = PDl_Sym(handle, "ngSpice_Init");
ctx->ngSpice_Command   = PDl_Sym(handle, "ngSpice_Command");
ctx->ngGet_Vec_Info    = PDl_Sym(handle, "ngGet_Vec_Info");
...

If any symbol lookup fails, the bridge immediately closes the library and returns a descriptive Tcl error, preventing partially initialized instances. This allows compatibility with multiple ngspice builds that may export slightly different symbol sets.

Safe unloading and crash preventionTop, Main, Index

When an instance is destroyed (InstDeleteProcInstFreeProc), the bridge normally calls PDl_Close() to unload the ngspice library. However, under certain conditions (e.g., if ngspice has left background threads running or its shutdown callbacks are incomplete), unloading can cause segmentation faults. To handle this safely:

This design sacrifices a small amount of memory for guaranteed stability during process termination.

Platform-specific nuancesTop, Main, Index

Windows:

macOS:

Linux / BSD:

SummaryTop, Main, Index

The DLL loading layer provides:

This allows ngspicetclbridge to load any compatible ngspice shared library at runtime, regardless of platform or path encoding, ensuring consistent behavior and portability across Tcl environments.