Skip to content

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.direct call across the boundary (svdpi.h)direct call acrossthe boundary…SystemVerilog Testbenchimport "DPI-C" … · export "DPI-C" …C / C++ Implementation#include <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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet