SystemVerilog · Module 17
Direct Programming Interface (DPI-C)
SystemVerilog DPI-C — the standard interface for calling C/C++ from SystemVerilog and exposing SV back to C in the same simulator process. import vs export, the full SV↔C data-type mapping, the chandle type for C object lifetimes, pure vs context functions, a complete CRC-8 reference-model example, and the compile/link flow on VCS, Questa, and Xcelium.
Module 17 · Page 17.1
The Direct Programming Interface (DPI-C) is how SystemVerilog and C/C++ talk to each other inside a single simulator process. You import a C function and call it like any SV function; you export an SV function and call it from C. There is no socket, no message queue, no PLI/VPI boilerplate — both languages share one address space and the call crosses the boundary as a near-native function call. This lesson covers the two directions (import / export), the exact data-type mapping the LRM defines, the chandle type that lets SV hold an opaque pointer to a C object, the pure / context qualifiers that control optimisation and kernel access, and a complete CRC-8 reference-model example with the compile/link commands for the three major simulators. DPI-C is specified in IEEE 1800-2017 Chapter 35, and the C header svdpi.h ships with every compliant tool.
1. Engineering Problem — Why Call C at All
You are building a testbench for a CRC engine and need a golden reference model to check the DUT against. Your firmware team already ships a battle-tested CRC implementation in C, used in production. Rewriting it in SystemVerilog means two copies of the same algorithm that can silently drift apart — a divergence that survives simulation and surfaces as a tape-out bug. The right move is to reuse the C model directly and make it the scoreboard's reference.
Before DPI-C, the only bridge was the Verilog PLI/VPI API — registration tables, callback functions, handle traversal, hundreds of lines of C boilerplate just to pass two integers across. It was slow, error-prone, and most engineers avoided it. DPI-C, added in IEEE 1800, replaces all of that with a single declaration:
// Tell SV this function is implemented in C — that's the entire bridge.
import "DPI-C" function int compute_crc8(input int data, input int poly);
// Now call it exactly like any SystemVerilog function.
int result = compute_crc8(payload, 8'h07);No registration tables, no callbacks. The simulator links the compiled C object into the simulation process and resolves the symbol like any C linker would. The same mechanism unlocks a whole class of reuse: math libraries (math.h), file/format parsers, compression and crypto libraries (zlib, OpenSSL), SystemC co-simulation glue, and performance-critical testbench code that runs millions of times per simulation and is 10–100× faster in C than in SV.
2. Mental Model — One Process, Two Languages, a Direct Call
The picture every engineer carries:
SystemVerilog and C run in the same simulator process and share the same memory. DPI-C is not a network call or an RPC — it is a direct function call across a language boundary.
import "DPI-C"is a compile-time promise to the SV compiler ("this function exists in C; trust me, I'll link it"), exactly like anexternprototype in a C header.export "DPI-C"is the reverse: it makes an SV function callable from C. At elaboration the compiler records the declaration; at simulation startup the simulator links the C shared library and resolves the symbols.
Four invariants this picture preserves:
import= SV calls C;export= C calls SV. Import is by far the common case (reference models, math, parsers). Export is for when a C model needs to trigger SV behaviour or read simulation state.- The signature is a contract, checked at the boundary. The SV declaration and the C definition must agree on return type, argument types, and order. A mismatch is silent data corruption, not always a tool error — the discipline is "match every type exactly."
- Functions can't consume time or have output ports; tasks can. An imported DPI function takes only
inputarguments and cannot advance simulation time (no#delay, no@event). If you need outputs or time, import a DPI task. - C owns its own memory. SV's garbage collector does not manage the C heap. Anything
malloc'd in C and handed back as achandlelives until C frees it — leaks are invisible to the simulator.
3. Visual Explanation — The Boundary Inside One Process
Both sides live in the same process; the DPI mechanism, declared through svdpi.h, is the direct call across the language boundary.
The two directions of the boundary:
import — SV calls C
A function or task implemented in C, declared in SV with import "DPI-C". The most common case — reference models, math libraries, file parsers, performance-critical code.
export — C calls SV
A function or task defined in SV, made callable from C with export "DPI-C". Used when a C model must trigger SV behaviour or read simulation state.
Compared with the legacy interface, the DPI-C advantage is decisive:
| Feature | DPI-C | PLI / VPI |
|---|---|---|
| Syntax effort | One import declaration | Registration tables, handle traversal, callbacks |
| Performance | Near-native function-call speed | Slow — heavyweight callback mechanism |
| Type safety | Compiler-checked at elaboration | Runtime errors only |
| Simulator DB access | None by default (context grants it) | Full simulation-database access |
| Portability | IEEE 1800 standard — same code everywhere | VPI is standard; PLI is legacy |
4. import — Calling C from SystemVerilog
An import declaration tells the SV compiler a function or task is implemented in C. The compiler generates the calling-convention glue; you write nothing beyond the declaration on either side.
// Form 1 — same name in SV and C
import "DPI-C" function int c_add(input int a, input int b);
import "DPI-C" function void c_log(input string msg);
// Form 2 — different SV name, mapped to the C function name
import "DPI-C" function int sv_crc32 = compute_crc32(input int data, input int poly);
// ^ SV name ^ C function name
// Form 3 — import a task (tasks CAN have output ports)
import "DPI-C" task c_model_step(input int clk_cycles, output int status);Reading the anatomy of a single import line, left to right: import (the body is not in SV) · "DPI-C" (the layer identifier — always this exact string) · function (returns a value, no time consumption; use task for outputs/time) · the return type (must match C exactly) · the name (shared with C unless you use the sv_name = c_name form) · port directions (functions allow only input) · the argument list (types and order must match C; names may differ).
A complete import example, both sides:
// tb.sv
import "DPI-C" function int c_add(input int a, input int b);
import "DPI-C" function void c_log(input string s);
module dpi_demo;
int result;
initial begin
result = c_add(10, 20);
$display("Sum = %0d", result); // Sum = 30
c_log("Sim started");
end
endmodule/* dpi_funcs.c — signatures MUST match the SV import declarations */
#include <svdpi.h>
#include <stdio.h>
int c_add(int a, int b) {
return a + b;
}
void c_log(const char* s) {
printf("[C] %s\n", s);
}5. export — Calling SystemVerilog from C
An export declaration makes an SV function or task callable from C. C initiates the call and control transfers into the SV simulation kernel.
// Export — same name in SV and C
export "DPI-C" function sv_multiply;
function automatic int sv_multiply(int a, int b);
return a * b;
endfunction
// Export with a different C-visible name
// SV name: sv_checksum C name: calc_checksum
export "DPI-C" function calc_checksum = sv_checksum;
function automatic int sv_checksum(int data);
return data ^ 32'hA5A5A5A5;
endfunctionOn the C side, forward-declare the exported symbols and call them like ordinary C functions:
/* caller.c */
#include <svdpi.h>
#include <stdio.h>
/* Forward-declare the SV-exported functions */
extern int sv_multiply(int a, int b);
extern int calc_checksum(int data); /* C-side alias for sv_checksum */
void c_run_model(void) {
int product = sv_multiply(6, 7); /* calls back into SV */
int checksum = calc_checksum(0xFF);
printf("product=%d checksum=%d\n", product, checksum);
}6. Data-Type Mapping — SV ↔ C
Every type crossing the boundary needs an unambiguous mapping, defined by the LRM. Always #include <svdpi.h> in the C file — it defines the helper types (svBit, svLogic, svBitVecVal, svLogicVecVal, chandle).
| SystemVerilog | C / C++ | Notes |
|---|---|---|
byte | char / int8_t | 8-bit signed |
shortint | short / int16_t | 16-bit signed |
int | int / int32_t | 32-bit signed — the most common scalar |
longint | long long / int64_t | 64-bit signed |
real | double | IEEE 754 double precision |
shortreal | float | IEEE 754 single precision |
bit | svBit | 2-state 1-bit |
logic / reg | svLogic | 4-state 1-bit (0/1/X/Z) |
bit [N-1:0] | svBitVecVal* | array of 32-bit words; ≤32 bits fits in one word |
logic [N-1:0] | svLogicVecVal* | aval/bval word pairs for the 4-state value |
string | const char* | null-terminated; input only for imported functions |
chandle | void* | opaque C pointer — see §7 |
void | void | return type of tasks and void functions |
The six pairs that cover ~90% of real DPI-C code: int↔int32_t, real↔double, string↔const char*, chandle↔void*, byte↔int8_t, longint↔int64_t. Packed vectors are the one case that needs the svdpi.h helper types:
import "DPI-C" function void c_invert(
input bit [31:0] data_in,
output bit [31:0] data_out
);
module test;
bit [31:0] in = 32'hF0F0_F0F0;
bit [31:0] out;
initial begin
c_invert(in, out);
$display("%h", out); // 0f0f0f0f
end
endmodule#include <svdpi.h>
/* bit[31:0] is ≤ 32 bits → one svBitVecVal word (uint32_t) */
void c_invert(const svBitVecVal* data_in, svBitVecVal* data_out) {
data_out[0] = ~data_in[0];
}7. The chandle Type — Holding a C Object
chandle is a SystemVerilog type added specifically for DPI-C. It holds an opaque pointer to a C object (void* on the C side). SV cannot dereference or inspect it — it can only store it and pass it back to C on a later call. That is exactly enough to manage a C object's lifetime from an SV testbench: create it, use it across many calls, then destroy it.
The rules: a chandle initialises to null; it can only be assigned the return value of a DPI import function (or null); it cannot appear in constraints, randomisation, or packed arrays. The classic create / use / destroy pattern:
import "DPI-C" function chandle ring_create(input int depth);
import "DPI-C" function void ring_push(input chandle h, input int v);
import "DPI-C" function int ring_pop(input chandle h);
import "DPI-C" function void ring_free(input chandle h);
module tb;
chandle ring_h; // null initially
initial begin
ring_h = ring_create(8);
ring_push(ring_h, 42);
ring_push(ring_h, 99);
$display("pop=%0d", ring_pop(ring_h)); // pop=42
ring_free(ring_h);
ring_h = null; // good habit — avoids stale-handle reuse
end
endmodule#include <svdpi.h>
#include <stdlib.h>
typedef struct {
int* data;
int depth, head, tail;
} RingBuf;
void* ring_create(int depth) {
RingBuf* r = malloc(sizeof(RingBuf));
r->data = calloc(depth, sizeof(int));
r->depth = depth;
r->head = r->tail = 0;
return r; // void* → chandle in SV
}
void ring_push(void* h, int v) {
RingBuf* r = (RingBuf*)h;
r->data[r->tail++ % r->depth] = v;
}
int ring_pop(void* h) {
RingBuf* r = (RingBuf*)h;
return r->data[r->head++ % r->depth];
}
void ring_free(void* h) {
RingBuf* r = (RingBuf*)h;
free(r->data); free(r);
}7.1 pure vs context — Optimisation and Kernel Access
Every imported function falls into one of three categories. Declaring the right one lets the simulator optimise aggressively — or grants the C code access to the simulation kernel.
- Regular (default, no keyword). The C function may have side effects (global state, file I/O) but does not need the simulation kernel. The simulator calls it as-is.
pure. The function is side-effect-free and deterministic — same inputs always produce the same output, and it touches nothing outside its own stack. The simulator may cache the result and skip redundant calls. Use it for math/bit-manipulation utilities.context. The function needs the simulation kernel — it callssvGetScope(), reads simulation time, or calls back into an exported SV task. The most powerful and most expensive category; required for any C function that re-enters SV.
import "DPI-C" function int c_with_side_effect(input int x); // regular
import "DPI-C" pure function real c_log2(input real x); // cacheable
import "DPI-C" context task c_wait_and_report(input int cycles); // re-enters SV8. Full Working Example — CRC-8 Reference Model
The end-to-end flow: a CRC-8 implemented in C, called from an SV testbench, with the exact compile/link commands for each simulator.
/* crc8.c — CRC-8 with polynomial 0x07 (CCITT) */
#include <svdpi.h>
#include <stdint.h>
unsigned char compute_crc8(const svBitVecVal* bytes, int n) {
uint8_t crc = 0x00;
for (int i = 0; i < n; i++) {
uint8_t b = (bytes[i / 4] >> ((i % 4) * 8)) & 0xFF; // unpack byte i
crc ^= b;
for (int k = 0; k < 8; k++)
crc = (crc & 0x80) ? ((crc << 1) ^ 0x07) : (crc << 1);
}
return crc;
}// tb_crc8.sv — byte → unsigned char, bit[63:0] → svBitVecVal*
import "DPI-C" function byte compute_crc8(
input bit [63:0] bytes,
input int n
);
module tb_crc8;
bit [63:0] payload;
byte crc;
initial begin
payload = 64'h0000_0000_0000_0000;
crc = compute_crc8(payload, 8);
$display("CRC-8(all-0s) = 8'h%02h", crc);
payload = 64'h0102_0304_0506_0708;
crc = compute_crc8(payload, 8);
$display("CRC-8(01..08) = 8'h%02h", crc);
end
endmodule# Synopsys VCS — compile C to a shared lib, then link with -sv_lib
gcc -shared -fPIC -o crc8.so crc8.c -I${VCS_HOME}/include
vcs -sverilog tb_crc8.sv -sv_lib crc8 -o sim_crc8 && ./sim_crc8
# Siemens Questa / ModelSim
vlog -sv tb_crc8.sv
vsim -sv_lib ./crc8 tb_crc8
# Cadence Xcelium — compiles the C directly, no separate gcc step
xrun -sv tb_crc8.sv crc8.cExpected output (the all-zeros case is deterministically 0x00; the second value follows from the CRC-8/0x07 algorithm):
$ ./sim_crc8
CRC-8(all-0s) = 8'h00
CRC-8(01..08) = 8'h...9. Industry Usage — When Engineers Reach for DPI-C
DPI-C is not an academic exercise; these are the recurring on-the-job triggers:
- Reference models. A firmware C model for AES / CRC / FEC becomes the golden reference in the scoreboard — no rewrite, no divergence risk.
- Math & algorithms.
math.h(sin,log,pow), FFTs, fixed/floating-point filters — richer and faster than SV'sreal. - File I/O & parsing. Binary trace files, custom formats, structured report writers — C handles raw I/O far more efficiently than SV system tasks.
- Third-party libraries. zlib, OpenSSL, protocol stacks, vendor emulation models — all ship as C/C++ and bridge in through DPI-C.
- Performance-critical testbench code. Packet generators, scoreboards, hash tables that run millions of times per simulation — often 10–100× faster in C.
- Co-simulation glue. The first layer of an SV↔SystemC, SV↔external-process, or hardware-in-the-loop bridge.
UVM environments use DPI-C the same way — typically a context import for a C reference model invoked from a scoreboard, with the C side calling an exported SV logging function. The mechanism is identical; only the surrounding methodology changes.
10. Debugging Guide — The Mistakes Everyone Makes Once
DPI-C — bugs every engineer hits the first time
Symptom: The C compiler errors on svBit, svLogic, svBitVecVal, or chandle being undefined — or, worse, silently uses the wrong size for a DPI type.
Cause: Those helper types are declared in svdpi.h. Without it, the C file has no definition for the DPI ABI types.
// ❌ svBitVecVal is undefined
void c_invert(const svBitVecVal* in, svBitVecVal* out) { out[0] = ~in[0]; }Fix: make #include <svdpi.h> the first line of every C file that implements or calls DPI functions.
Guardrail: treat svdpi.h as mandatory boilerplate for DPI C files, exactly like uvm_macros.svh for UVM components.
Symptom: No tool error, but the values are wrong — the simulator reads the wrong bytes across the boundary.
Cause: The SV declares function longint c_fn(input int x) but the C writes int c_fn(int x) — a 64-bit return read from a 32-bit value. The signature is a contract; breaking it corrupts data silently.
import "DPI-C" function longint c_fn(input int x); // expects int64_t returnint c_fn(int x) { return x; } // ❌ returns int32_t — silent corruptionFix: match every type exactly. int↔int32_t, longint↔int64_t, real↔double. Use <stdint.h> types in C to be explicit.
Guardrail: review the SV declaration and the C signature side by side; they are one contract in two files.
Symptom: Compile error — "DPI import function cannot have output/inout ports".
Cause: DPI imported functions accept only input ports. Outputs require a DPI task.
// ❌ functions cannot have output ports
import "DPI-C" function void c_fn(output int result);Fix: change function to task — tasks support output and inout (and null return).
import "DPI-C" task c_fn(output int result);Guardrail: "need more than one value back, or need to consume time?" → DPI task, not function.
Symptom: Segmentation fault inside the simulator, often with a cryptic stack trace.
Cause: chandle h; is null. Calling c_use(h) before h = c_create() passes a null pointer that C dereferences.
chandle h; // null
c_use(h); // ❌ C dereferences null → segfaultFix: always create before use; guard with if (h !== null).
h = c_create();
if (h !== null) c_use(h);Guardrail: treat a chandle like a C pointer — never dereference (use) it until it has been assigned a created object.
Symptom: Memory climbs across a long test or a regression of many tests; the simulator never warns.
Cause: A C object malloc'd and returned as a chandle lives until C frees it. SV's garbage collector does not manage the C heap.
Fix: write a c_free() / c_destroy() import and call it in a final block or at end-of-test.
final begin
if (ring_h !== null) ring_free(ring_h);
endGuardrail: every chandle-returning create has a matching destroy; pair them at design time like malloc/free.
Symptom: Reentrant calls from C return wrong results, intermittently, with no error.
Cause: A static-lifetime exported function shares one copy of its locals. Reentrant C calls corrupt that storage.
export "DPI-C" function sv_fn;
function int sv_fn(int a); ... endfunction // ❌ static lifetimeFix: declare it function automatic.
function automatic int sv_fn(int a); ... endfunctionGuardrail: every exported subprogram is automatic — make it a lint rule.
11. Q & A
12. Cross-References & What's Next
This lesson covered the DPI-C boundary: import/export, the SV↔C type mapping, chandle lifetimes, pure/context, and the compile/link flow. It opens Module 17 (Advanced Topics).
- Previous: 16.4 — Package Dependencies & Compilation Order — the build-order discipline; DPI C objects join the same file-list/compile flow as a
-sv_libshared library. - Module index: Module 17 — Advanced Topics — the advanced-feature arc.
Related material elsewhere in the curriculum:
- Package Declaration & Usage — the
importkeyword here is unrelated to package import; this page'simport "DPI-C"is the C-bridge form. - What UVM Is — and What It Is Not — UVM scoreboards routinely wrap a C reference model through a
contextDPI import.
13. Quick Reference
// ── Import a C function (SV calls C) ────────────────────────────────
import "DPI-C" function int c_func(input int a, input int b);
// ── Import with alias (different SV name vs C name) ─────────────────
import "DPI-C" function int sv_name = c_name(input int x);
// ── Import a task (can have output ports / consume time) ────────────
import "DPI-C" task c_task(input int x, output int y);
// ── pure: deterministic, side-effect-free → cacheable ───────────────
import "DPI-C" pure function real c_sqrt(input real x);
// ── context: needs sim kernel / calls back into SV ──────────────────
import "DPI-C" context task c_ctx_task(input int x);
// ── Export a function (C calls SV) — ALWAYS automatic ───────────────
export "DPI-C" function sv_func;
function automatic int sv_func(int a); return a * 2; endfunction
// ── chandle — opaque C pointer (void* in C) ─────────────────────────
import "DPI-C" function chandle c_create();
import "DPI-C" function void c_destroy(input chandle h);
chandle h = c_create(); // null until assigned
c_destroy(h); h = null; // pair every create with a destroy
// ── Type mapping cheat sheet ────────────────────────────────────────
// int↔int32_t · longint↔int64_t · real↔double · byte↔int8_t
// string↔const char* (input only) · chandle↔void* · bit[N]↔svBitVecVal*
// ── Always #include <svdpi.h> in every DPI C file ───────────────────14. Summary
DPI-C is the standard, near-zero-overhead bridge between SystemVerilog and C/C++ inside one simulator process. import "DPI-C" lets SV call C (reference models, math, parsers, performance-critical code); export "DPI-C" lets C call SV. The declaration is a contract — return type, argument types, and order must match the C definition exactly, because a mismatch is silent data corruption rather than a guaranteed tool error.
The mechanics that make it work: imported functions are input-only and time-free, while tasks allow output/inout and (with context) time and SV callbacks; the type mapping is defined by the LRM, with svdpi.h supplying the helper types for packed vectors and chandle; chandle holds an opaque void* so SV can own a C object's create/use/destroy lifecycle — and, because SV's GC never touches the C heap, every create needs a matching destroy; pure unlocks call-caching for deterministic functions while context grants kernel access for the C that re-enters SV. The CRC-8 example shows the whole flow, and the build differs only by simulator (-sv_lib on VCS/Questa, direct .c on Xcelium) while the DPI source stays identical. DPI-C is a verification interface — never synthesizable, never in a waveform — and it is the reason a battle-tested C model can serve as the golden reference your scoreboard checks against.