Part 7 · Advanced & Integration · Intermediate

DPI Pitfalls & Debug

Compile/link flow, symbol issues, time-consuming task rules, thread safety, crash debugging, and DPI vs PLI/VPI.

The compile and link flow

DPI failures cluster at build time. Conceptually every simulator follows the same flow: compile C to position-independent objects, link them into a shared object, and tell the simulator to load that object at elaboration so import symbols resolve.

diagram
DPI BUILD & LOAD FLOW [C] [SV]

  model.c ──► gcc -c -fPIC ──► model.o ─┐
  shim.c  ──► gcc -c -fPIC ──► shim.o  ─┼─► gcc -shared ──► libmodel.so
                                        ┘
  tb.sv (import "DPI-C" ...) ──► compile/elaborate
                                        │
            simulator loads libmodel.so │  (-sv_lib libmodel /
                                        ▼   -dpicpath / vendor switch)
                  symbol lookup: "checksum_add" found? ── yes ─► run
                                        │
                                        no
                                        ▼
                  elaboration error: unresolved DPI import

Symbol and name issues

  • C++ name mangling — a model built as C++ exports mangled symbols; the SV import looks for the plain C name. Wrap every DPI-visible function in extern "C" { ... }.

  • Name mismatch — the C symbol must match the SV import name exactly (or the renamed form import "DPI-C" c_name = function ...).

  • Missing -fPIC — the shared link fails, or worse, silently picks up a stale .so from a previous build. Clean builds matter.

  • Stale shared object — the simulator loads the .so it finds on its path; after editing C, confirm the timestamp of what was actually loaded.


Runtime rules: time and threads

Time-consuming imported tasks

C code itself can never consume simulation time — there is no way to wait on the scheduler from C. An imported task may appear to take time only because it calls an exported SV task that blocks; control flows C → exported SV task → @(posedge clk) → back to C when the wait completes. Imported functions may not do even that — any call chain that could block must be declared task on both the import and the export.

Thread safety

If the C model spawns its own threads (pthreads, OpenMP), those threads must never call exported SV functions or any svdpi API — the simulator kernel is single-threaded from DPI's perspective, and foreign-thread calls corrupt scheduler state. The safe pattern: worker threads write results into a C-side queue, and the simulator thread drains the queue on its next import call.

c
/* SAFE: foreign thread → queue → simulator thread */
static result_q q;                 /* mutex-protected C queue */

void* worker(void* arg) {          /* pthread — NO sv calls here */
    result r = heavy_compute(arg);
    q_push(&q, r);                 /* C-side queue only */
    return 0;
}

void poll_results(void)            /* context import, sim thread */
{
    result r;
    while (q_try_pop(&q, &r))
        sv_result_ready(r.id, r.val);   /* export — safe HERE */
}

Debugging crashes at the boundary

  1. Reproduce with the simulator under a debugger (gdb on the simulator binary, or the vendor's C-debug switch) — DPI frames show up as ordinary C stack frames.

  2. Suspect ownership first: dangling open-array pointers, stored string pointers, and frees across the boundary cause most segfaults — usually delayed, far from the bug.

  3. Check prototype agreement: an SV import with int where C expects long long corrupts the stack silently on some ABIs; diff the import against the C signature argument by argument.

  4. Bisect the boundary: replace the C body with a stub that logs arguments and returns a constant — if the crash vanishes, the bug is C-side; if not, the call itself (types/scope) is wrong.

  5. Build the C side with -g -fsanitize=address for unit tests outside the simulator — ASan catches the ownership bugs DPI makes hard to see.

Interview angle: DPI vs PLI/VPI

A standard senior-interview question. The answer: PLI/VPI is a callback-and-handle API for introspecting the simulation itself — walking the design hierarchy, reading arbitrary nets, registering value-change callbacks; it is powerful, verbose, and slow per call. DPI is a function-call binding — near-zero overhead, plain prototypes on both sides, but no design introspection at all. Use DPI to run software (models, codecs, utilities); use VPI when a tool must discover or instrument the design dynamically (waveform tools, custom linters). In modern verification, DPI covers 95% of needs.

Key takeaways

  • Build C with -fPIC into a shared object and confirm the simulator loads the fresh one — most 'DPI bugs' are build bugs.

  • extern "C" on every DPI-visible function when compiling as C++ — mangled names never resolve.

  • Only the simulator thread may touch svdpi APIs or exports; foreign threads communicate through C-side queues.

  • DPI = fast function binding to run software; VPI = slow handle API to introspect the design — know the one-liner.

Common pitfalls

  • Unresolved import at elaboration after a C edit — stale .so on the load path, not a code bug.

  • Blocking call chain through an imported function instead of a task — illegal; declare task end to end.

  • Calling an export from a pthread the C model spawned — scheduler corruption, intermittent crashes.

  • Debugging a delayed segfault at the crash site instead of auditing pointer lifetimes at the boundary — the bug is upstream.