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.
Continue learning
Related tutorials
- Related topic
Package Declaration & Usage
SystemVerilog package — the named compilation scope that bundles parameters, types, functions, tasks, and classes for sharing across modules, interfaces, and classes. The :: scope-resolution operator, what is legal vs illegal inside a package, the function automatic discipline, and the canonical protocol-package and verification-utility patterns that all production VIPs (including UVM) are built on.
- Related topic
import — Explicit & Wildcard
SystemVerilog import — the shorthand for using package members without the pkg_name:: prefix. The two forms behave very differently: explicit (import pkg::item) verifies the name at the import line and errors on conflict with locals; wildcard (import pkg::*) registers a deferred search path and silently shadows under local declarations. Module-header vs body placement, nested-import non-export, name-resolution priority, and the UVM-style three-tier import pattern.
Standards & specifications
- Governing standard
- IEEE Std 1800 (SystemVerilog)(opens IEEE in a new tab)
Defines SystemVerilog language semantics — syntax, data types, scheduling and the behaviour a conforming simulator must produce. It does not define tool-specific synthesis support or vendor methodology.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the SystemVerilog curriculum.
