Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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 an extern prototype 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 input arguments 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 a chandle lives 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.

Two blocks inside one simulator process — a SystemVerilog testbench and a C/C++ implementation — connected by a bidirectional direct-call edge labelled svdpi.h.SystemVerilog Testbenchimport "DPI-C" … · export "DPI-C" …C / C++ Implementation#include <svdpi.h>direct call acrossthe boundary(svdpi.h)12
Figure 1 — SystemVerilog and C share one simulator process. import lets SV call into C; export lets C call back into SV. The svdpi.h header defines the calling convention and helper types.

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:

FeatureDPI-CPLI / VPI
Syntax effortOne import declarationRegistration tables, handle traversal, callbacks
PerformanceNear-native function-call speedSlow — heavyweight callback mechanism
Type safetyCompiler-checked at elaborationRuntime errors only
Simulator DB accessNone by default (context grants it)Full simulation-database access
PortabilityIEEE 1800 standard — same code everywhereVPI 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
/* 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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;
endfunction

On the C side, forward-declare the exported symbols and call them like ordinary C functions:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
/* 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).

SystemVerilogC / C++Notes
bytechar / int8_t8-bit signed
shortintshort / int16_t16-bit signed
intint / int32_t32-bit signed — the most common scalar
longintlong long / int64_t64-bit signed
realdoubleIEEE 754 double precision
shortrealfloatIEEE 754 single precision
bitsvBit2-state 1-bit
logic / regsvLogic4-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
stringconst char*null-terminated; input only for imported functions
chandlevoid*opaque C pointer — see §7
voidvoidreturn type of tasks and void functions

The six pairs that cover ~90% of real DPI-C code: intint32_t, realdouble, stringconst char*, chandlevoid*, byteint8_t, longintint64_t. Packed vectors are the one case that needs the svdpi.h helper types:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#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.

Three-stage chandle lifecycle: Create returns a non-null handle, Use is repeated any number of times, Destroy frees the memory and nulls the handle.Createh = c_create(); //non-nullUse (any number oftimes)c_write(h, data); v =c_read(h);Destroyc_free(h); h = null;12
Figure 2 — the chandle lifecycle. Create returns a non-null handle; use it any number of times; destroy frees the C memory. Skipping destroy leaks C heap the simulator never warns about.

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#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 calls svGetScope(), 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.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 SV

8. 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
/* 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;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# 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.c

Expected output (the all-zeros case is deterministically 0x00; the second value follows from the CRC-8/0x07 algorithm):

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./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's real.
  • 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

1. Forgetting #include <svdpi.h> in the C file

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ 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.

2. Type mismatch between the SV declaration and the C signature

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import "DPI-C" function longint c_fn(input int x);   // expects int64_t return
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int c_fn(int x) { return x; }    // ❌ returns int32_t — silent corruption

Fix: match every type exactly. intint32_t, longintint64_t, realdouble. 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.

3. Using an output port in an imported function

Symptom: Compile error — "DPI import function cannot have output/inout ports".

Cause: DPI imported functions accept only input ports. Outputs require a DPI task.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ 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).

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

4. Calling C through a null chandle

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
chandle h;            // null
c_use(h);             // ❌ C dereferences null → segfault

Fix: always create before use; guard with if (h !== null).

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

5. Not freeing C memory (chandle leak)

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
final begin
  if (ring_h !== null) ring_free(ring_h);
end

Guardrail: every chandle-returning create has a matching destroy; pair them at design time like malloc/free.

6. Exported function not declared automatic

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
export "DPI-C" function sv_fn;
function int sv_fn(int a); ... endfunction      // ❌ static lifetime

Fix: declare it function automatic.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
function automatic int sv_fn(int a); ... endfunction

Guardrail: 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).

Related material elsewhere in the curriculum:

13. Quick Reference

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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

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.