Notes and internals¶
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 implemented¶
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 creation¶
When the package initializes, Ngspicetclbridge_Init() registers the constructor command:
::ngspicetclbridge::new
implemented by the C function NgSpiceNewCmd(). When this command is invoked, it:
Allocates and zero-initializes an
NgSpiceContextstructure.Initializes all mutexes, condition variables, message queues (
MsgQueue msgq,MsgQueue capq), and data buffers (DataBuf prod,DataBuf pend).Loads the ngspice shared library dynamically using
PDl_OpenFromObj(), resolving function pointers such asngSpice_Init,ngSpice_Command,ngGet_Vec_Info, etc.Creates a unique Tcl command (e.g.,
::ngspicetclbridge::s1) viaTcl_CreateObjCommand(), associating the newNgSpiceContext *as itsClientData.Registers a delete procedure (
InstDeleteProc) so that when the command is deleted (via$s destroyor interpreter shutdown), the associated context is safely torn down.
The resulting Tcl command becomes the primary interface for controlling the simulator instance.
Subcommand dispatching¶
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:
command ?-capture? string - Sends arbitrary ngspice commands via
ngSpice_Command(). If-captureis used, temporarily enables output capturing inctx->capq, then returns a dictionary withrcandoutputfields.circuit list - Sends a complete circuit deck to ngspice via
ngSpice_Circ().waitevent - Blocks until an ngspice event occurs or a timeout expires, returning a result dictionary.
vectors, initvectors, messages, eventcounts - Return or clear data structures stored in the context.
plot, asyncvector, isrunning, abort, destroy - Perform specialized inspection or control operations on the current simulation session.
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 transitions¶
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:
NGSTATE_IDLE- safe to send commandsNGSTATE_STARTING_BG- ngspice background thread startingNGSTATE_BG_ACTIVE- background thread runningNGSTATE_STOPPING_BG- halting background threadNGSTATE_DEAD- teardown in progress
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 queue¶
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 mode¶
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 cleanup¶
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:
Sets
ctx->destroying = 1to block further callbacks.Stops or waits for the background thread to end cleanly.
Sends
ngSpice_Command("quit")unless the shutdown was unsafe.Waits for
ControlledExitCallback()to signal completion.Deletes pending events referencing this context.
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.
Summary¶
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 implemented¶
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 callbacks¶
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 model¶
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 |
|---|---|
|
Emits text lines printed by ngspice (stdout/stderr). |
|
Emits status messages such as “tran simulation complete”. |
|
Invoked when ngspice is about to terminate, either via “quit” or internal exit. |
|
Called each time a new data point (vector values) is available during simulation. |
|
Called once at the start of a run, delivering vector metadata ( |
|
Indicates that ngspice’s background thread has started or stopped. |
Thread safety and queueing behavior¶
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:
Check for teardown: If
ctx->destroyingis set, the callback immediately returns - it never queues new events during or after teardown.Acquire context mutex: Locks such as
ctx->mutexor specialized ones likectx->bg_muare used to update shared state safely.Write shared data: Depending on the callback type, the callback may:
Append text to
ctx->msgq(forSendCharandSendStat);Update counters in
ctx->evt_counts;Store new vector data rows in
ctx->prodor initialization snapshots inctx->init_snap.
Signal condition variables: The callback calls
Tcl_ConditionNotify()onctx->cond,ctx->bg_cv, orctx->exit_cvto wake threads waiting in Tcl commands likewaiteventor during shutdown.Queue a Tcl event: Finally, it calls
NgSpiceQueueEvent(ctx, callbackId, ctx->gen), which allocates anNgSpiceEventand schedules it in the Tcl event queue viaTcl_QueueEvent. Tcl will later callNgSpiceEventProc()on the interpreter thread to process the event.
Controlled flow of data¶
Data-oriented callbacks (SendInitDataCallback and SendDataCallback) are slightly special:
SendInitDataCallbackis invoked once per run, delivering metadata for all vectors. It allocates and fills anInitSnapstructure inside the context, which contains vector names, indices, and type information (real/complex).SendDataCallbackis invoked repeatedly as the simulation progresses, delivering numerical data points in the form ofvecvaluesall. The callback copies the numeric samples into theDataBufstructure (ctx->prod) for deferred consumption by Tcl when the event is processed.
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 callbacks¶
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 handling¶
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 coordination¶
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 integration¶
All callbacks are designed to be idempotent and safe to call during teardown:
If the global heap has been marked poisoned (
g_heap_poisoned), callbacks return immediately.The
destroyingflag prevents late enqueues during deletion.All shared data structures (
msgq,capq,prod,init_snap) are protected by their corresponding mutexes.Each queued event uses
Tcl_Preserve()/Tcl_Release()pairing so that no context is freed while events remain in Tcl’s queue.
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 implemented¶
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 definition¶
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 queuing¶
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 processing¶
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:
SEND_INIT_DATA: transfers initialization metadata from
ctx->init_snapintoctx->vectorInit(a Tcl dictionary mapping vector names to{number N real 0/1}), resetsctx->vectorData, and frees the snapshot.SEND_DATA: moves data rows from
ctx->prod(filled in the ngspice thread) into the Tcl dictionaryctx->vectorData, appending each vector’s numeric or complex values. The buffer is then cleared.CONTROLLED_EXIT: marks
ctx->exited = 1and signalsctx->exit_cvto wake any waiters.BG_THREAD_RUNNING: updates state flags, signals
ctx->bg_cv, and may callFlushPending()to run any commands queued during background thread transitions.All others: increment their respective event counters and append textual messages to
ctx->msgq.
Once processed, the event always returns 1 to indicate it should be removed from Tcl’s event queue.
Event deletion and resource balancing¶
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 safety¶
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 guarantees¶
Each event handler acquires and releases
ctx->mutexas needed when touching shared data.Message and data queues are thread-safe thanks to this locking discipline.
All Tcl object reference counts (
Tcl_IncrRefCount/Tcl_DecrRefCount) are adjusted only on the Tcl thread.
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 ensured¶
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 |
|---|---|
|
Protects shared state such as |
|
Guards the pending command queue used during transitional states (like |
|
Synchronizes access to background-thread state variables ( |
|
Protects |
|
Signals changes in event counters and is used by |
|
Used by |
|
Used to signal that ngspice has exited, waking |
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:
The ngspice callback thread acquires the relevant mutex, writes to the shared structure (e.g.,
msgq,prod, orevt_counts), and releases it.It then queues an event to the Tcl thread.
The Tcl thread later processes the event, safely reading from the same structures under lock.
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 implemented¶
ngspicetclbridge maintains two complementary data paths for handling simulation results from ngspice:
Synchronous (event-driven) data, delivered through callbacks (
SendInitDataCallbackandSendDataCallback) and stored inside internal buffers (DataBuf,InitSnap).Asynchronous (on-demand) data, retrieved by Tcl commands such as
asyncvectororvectors, which query ngspice directly viangGet_Vec_Info()or similar functions.
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 flow¶
ngspice calls
SendInitDataCallback()→ bridge creates and fillsInitSnapwith vector metadata.Tcl event is queued (
SEND_INIT_DATA) → later processed byNgSpiceEventProc(), which converts metadata into the Tcl dictionaryctx->vectorInit.ngspice calls
SendDataCallback()→ bridge appends numeric samples toctx->prod(DataBuf).Tcl event is queued (
SEND_DATA) → later processed byNgSpiceEventProc(), which transfers rows fromctx->prodinto the Tcl dictionaryctx->vectorData.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 structures¶
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:
Real vectors as a flat list of doubles.
Complex vectors as a list of
{re im}pairs.
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)¶
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 layout¶
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;
Each DataCell holds one scalar value (real or complex) for a single vector at a single timestep.
Each DataRow groups all
DataCellcorresponding to one simulation step.DataBuf acts as a growable ring buffer accumulating new
DataRowuntil they are consumed.
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 objects¶
When Tcl processes the SEND_INIT_DATA or SEND_DATA events, it transforms the internal C buffers into Tcl
data structures:
``SEND_INIT_DATA``: Creates or replaces
ctx->vectorInitwith a dictionary:tcl { vectorName {number N real 0|1} ... }This defines vector order and type information for the upcoming run.``SEND_DATA``: Appends to
ctx->vectorData, a dictionary mapping each vector name to a list of numeric values:tcl { V(out) {0.0 0.1 0.2 ...} I(R1) {0.0 0.0 0.0 ...} ... }Complex values are represented as{re im}pairs. Once the transfer completes,ctx->prodis cleared.
The conversion always occurs under ctx->mutex to prevent concurrent modification during background activity.
Data lifecycle and cleanup¶
At the start of each simulation,
vectorDataandvectorInitare cleared, and a new generation number (ctx->gen) is incremented.Any pending data buffers or old Tcl objects from previous runs are discarded.
At the end of a run,
DataBufandInitSnapare freed.During teardown (
InstDeleteProc), all remaining data buffers are released to prevent leaks.
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 handling¶
SendDataCallback()andSendInitDataCallback()run on ngspice’s thread; they lockctx->mutexwhile modifying shared buffers.Tcl-side handlers (
NgSpiceEventProc()) also lock the same mutex when consuming the data.This producer–consumer pattern ensures no race conditions between ngspice data production and Tcl data consumption.
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 implemented¶
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.
Overview¶
Each NgSpiceContext maintains two independent message queues:
``msgq`` - the global asynchronous message queue, which accumulates all text lines and status messages emitted by ngspice during normal operation. These messages are accessible to Tcl via the
$s messagessubcommand and are used by commands such aswaiteventor logging facilities to display simulation output.``capq`` - the temporary capture queue, which is used exclusively during the execution of
$s command -capture .... Whilecap_activeis set, any message that would normally go intomsgqis duplicated intocapqas well. This allows Tcl to capture and return ngspice’s textual output specific to a single command invocation, without interfering with the ongoing asynchronous log.
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)¶
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:
The callback locks
ctx->mutexto synchronize access to shared state.The callback calls
QueueMsg(ctx, line)which appends the given text toctx->msgq.If
ctx->cap_activeis set (i.e., a$s command -capture ...is in progress), the same line is also appended toctx->capqfor later retrieval by the capturing command.The callback signals
Tcl_ConditionNotify(&ctx->cond)to wake any threads waiting in$s waiteventor$s messages -wait.Finally, the callback unlocks
ctx->mutexbefore 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)¶
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)¶
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:
Before invoking
ngSpice_Command(), the bridge:
Locks
ctx->mutexClears
ctx->capqusingMsgQ_Clear()Sets
ctx->cap_active = 1Unlocks
ctx->mutex
ngspice executes the command, possibly emitting messages via
SendCharCallback.Each emitted line is recorded in both
msgq(global) andcapq(local).After the command finishes, the bridge:
Locks
ctx->mutexagainDisables capture (
ctx->cap_active = 0)Copies messages from
capqinto a Tcl list (outList)Clears
capqUnlocks the mutex
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 queues¶
Both message queues can be cleared or destroyed via:
void MsgQ_Clear(MsgQueue *q);
void MsgQ_Free(MsgQueue *q);
MsgQ_Clear()frees individual strings and resetscountto zero, but retains allocated capacity.MsgQ_Free()releases both the strings and the queue’s internal array, used during context teardown.
The queues are cleared at multiple stages:
At simulator initialization (empty start).
After each
$s messages -clearcall.After each capture-mode command completes.
During instance deletion and final free (
InstFreeProc).
Synchronization and safety guarantees¶
All modifications of either
msgqorcapqoccur underctx->mutex.The queues store only heap-allocated strings - no Tcl objects or shared buffers - making them completely safe to access from ngspice threads.
During teardown (
ctx->destroying = 1), callbacks stop queueing new messages immediately.All pending messages are safely freed during
InstFreeProc().
Summary¶
The message queue subsystem provides a clean, thread-safe bridge between ngspice’s asynchronous textual output and Tcl’s event-driven interface. It ensures:
reliable, lossless accumulation of ngspice messages,
isolated per-command capture without data loss,
and safe concurrent access across threads and teardown phases.
How dynamic library loading and portability is implemented¶
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 layer¶
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:
On POSIX systems (Linux, macOS):
PDl_OpenFromObj()callsdlopen(),PDl_Sym()callsdlsym(),PDl_Close()callsdlclose().On Windows:
PDl_OpenFromObj()callsLoadLibraryW(),PDl_Sym()callsGetProcAddress(),PDl_Close()callsFreeLibrary().
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 integration¶
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:
On Windows, Tcl internally stores paths as UTF-16, so
PDl_OpenFromObj()extracts the wide-string form and passes it toLoadLibraryW()directly, avoiding encoding errors or locale dependence.On POSIX systems, the path is converted to UTF-8 and passed as a regular C string to
dlopen().
This guarantees that paths with non-ASCII characters (e.g., C:/ユーザー/ngspice.dll) are handled correctly on all
platforms.
Function symbol resolution¶
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 prevention¶
When an instance is destroyed (InstDeleteProc → InstFreeProc), 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:
A flag
ctx->skip_dlcloseis set if teardown was not clean (e.g., controlled exit didn’t complete).In such cases,
InstFreeProc()skipsPDl_Close()and leaks the handle intentionally, avoiding unsafe deallocation while still allowing the Tcl interpreter to exit normally.
This design sacrifices a small amount of memory for guaranteed stability during process termination.
Platform-specific nuances¶
Windows:
Uses
LoadLibraryW()for Unicode safety.Relies on system DLL search path rules; Tcl’s
file normalizeis used to avoid relative path ambiguity.Threaded interaction with ngspice’s background thread is identical to POSIX, as all synchronization uses Tcl’s cross-platform
Tcl_MutexandTcl_Condition.
macOS:
Uses
dlopen()withRTLD_NOW | RTLD_GLOBALto ensure ngspice’s internal dependencies are resolved immediately..dylibsuffixes are automatically accepted alongside.so.
Linux / BSD:
Standard ELF dynamic linking via
dlopen(); no special handling required.
Summary¶
The DLL loading layer provides:
Transparent, cross-platform dynamic linking without preprocessor ifdefs in the main code.
Full Unicode path support using Tcl’s path abstractions.
Safe error handling and controlled unloading semantics.
Isolation of platform differences behind three simple functions.
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.
Copyright (c) George Yashin