Direct Programming Interface (DPI)
Calling C functions from SystemVerilog and SV functions from C — import and export declarations, the pure and context qualifiers, allowed argument types, open arrays, the WYSIWYG argument-passing principle, exported tasks, and the disable protocol for mixed-language call chains.
📋 What DPI Is
The Direct Programming Interface (DPI) lets SystemVerilog and a foreign programming language call each other’s functions directly — without the overhead of the PLI or VPI. SV 3.1 defines the foreign language layer for C only, but the SV side of the interface is language-independent by design.
- Imported function/task — implemented in C, called from SV. Declared with an
import "DPI"statement. - Exported function/task — implemented in SV, called from C. Declared with an
export "DPI"statement.
Calling an imported function looks identical to calling a native SV function. The caller cannot tell whether a function is implemented in SV or C. This makes DPI adoption transparent and avoids learning-curve friction.
context qualifier.
📋 Two Layers
DPI is cleanly separated into two independent layers. The SV layer knows nothing about how C passes arguments or represents memory. The C layer knows nothing about SV scheduling semantics.
SV Layer (this article)
- Import and export declarations
- Argument types, directions, and default values
- pure / context qualifiers
- Calling semantics (WYSIWYG)
- Disable protocol acknowledgement
- Language-independent — identical for any foreign language
C Layer (Annex F)
- Actual argument-passing mechanism (value vs reference)
- Memory layout of SV types in C
- C header types matching SV types
- DPI utility API (
svIsDisabledState, etc.) - Linking conventions
- C-specific — not covered in this article
📋 Global Name Space
Every imported or exported task/function has a global linkage name (a C identifier) that is unique across the entire design. This name space is separate from the SV compilation-unit scope name space.
- Global names must follow C identifier rules: start with a letter or underscore, followed by alphanumeric characters or underscores.
- No overloading is allowed — each global name is unique.
- If the global name would clash with an SV keyword, use an escaped identifier: the leading backslash and trailing space are stripped to form the C linkage name.
- If no explicit global name is given, the SV task/function name is used as the linkage name.
- The same C function can be imported multiple times in different scopes or with different SV names.
// Export with a different C name (SV name: foo+, C name: foo_plus) export "DPI" foo_plus = function \foo+; // Export with the same name on both sides export "DPI" function foo; // Import with a C linkage name different from the SV name import "DPI" init_1 = function void \init[1](); // Import where the C name is an SV keyword — use escaped identifier import "DPI" \begin = function void \init[2]();
📄 Import Declarations
An import declaration has exactly the same structure as an SV function or task prototype — direction, types, and an optional default value for each argument — plus an optional qualifier and an optional C linkage name.
// Basic form — no qualifier, C name same as SV name import "DPI" function void myInit(); // pure: math library function (result depends only on inputs, no side effects) import "DPI" pure function real sin(real); // chandle: opaque pointer to C-allocated memory import "DPI" function chandle malloc(int size); import "DPI" function void free(chandle ptr); // Queue management — two SV names, one C implementation (newQueue) import "DPI" function chandle newQueue(input string name_of_queue); import "DPI" newQueue = function chandle newAnonQueue(input string s=null); // Abstract data structure operations import "DPI" function chandle newElem(bit [15:0]); import "DPI" function void enqueue(chandle queue, chandle elem); import "DPI" function chandle dequeue(chandle queue); // Return a bit vector — packed array up to 32 bits import "DPI" function bit [15:0] getStimulus(); // context: must access SV state (e.g. VPI or exported functions) // Output array: logic[64:1] arr[0:63] import "DPI" context function void processTransaction( chandle elem, output logic [64:1] arr [0:63]); // Imported task: can consume simulation time, can be context import "DPI" task checkResults(input string s, bit [511:0] packet);
📋 pure and context Qualifiers
📋 pure Function Rules
A pure function call may be eliminated by the compiler if its result is not needed, or its cached result reused if the same input values appear again. This enables substantial simulation performance gains.
A pure function must not — directly or indirectly — do any of the following:
- Perform file operations.
- Read or write anything in the broadest sense: I/O, environment variables, OS objects, shared memory, sockets.
- Access any persistent data: global or static C variables.
📋 context Task/Function Rules
A context import receives an implicit scope representing the fully qualified instance name where the import declaration appears. This scope determines which exported SV functions can be called directly. To call exported functions from a different scope, the C code must first change its current scope using DPI utility functions.
- All exported SV tasks and functions are always context — they always need an associated instance context.
- Only context imports can safely call VPI, PLI, or exported SV functions.
- Calling VPI/PLI from a non-context import produces unpredictable behaviour — it can crash if the callee requires a context that was never set.
- Declaring an import as context does not automatically enable VPI or PLI access — the appropriate tool mechanism must still be used to enable those interfaces.
- Declaring context when not needed reduces simulation performance unnecessarily.
📋 Shared Semantic Rules for All Imports
- Imported functions complete instantly and consume zero simulation time — like native SV functions.
- Imported tasks may block and consume simulation time — like native SV tasks.
- input arguments: the C function must not modify them. Changes are invisible outside the function.
- output arguments: initial value inside the C function is undetermined. C must write before reading.
- inout arguments: C can read the initial value and write back. Changes propagate to the caller.
- ref cannot be used in import declarations. The actual pass-by-value or pass-by-reference mechanism is determined entirely by the C layer — transparent to SV.
- Memory management: SV and C each own their own memory. The C code must never free SV-allocated memory and vice versa (unless explicitly passing a pointer back through an imported free function).
- Reentrancy: imported tasks can be simultaneously active in multiple SV threads — C code must be re-entrant (avoid unsafe static variables, use thread-safe library calls).
- C++ exceptions must not propagate across the SV/C boundary. If C++ is used, catch all exceptions before returning to SV; otherwise undefined behaviour results.
📋 Function Result Types
Return types for both imported and exported functions are restricted to small values. The following are the only legal return types:
void,byte,shortint,int,longintreal,shortrealchandle,stringbitandlogic(scalar)- Packed bit arrays up to 32 bits (and types equivalent to packed bit arrays up to 32 bits)
📋 Argument Types
A rich subset of SV types is legal for formal arguments of imported and exported tasks/functions:
- All the types legal as return types (above).
- Packed one-dimensional arrays of
bitandlogic. - Any packed type — since every packed type is eventually equivalent to a one-dimensional packed array.
- Enumeration types — interpreted as their underlying integer type.
- Structs, unpacked arrays, and typedefs built from the above.
Key caveats:
- Class types cannot be passed across the DPI boundary at all.
- The
refdirection is not legal in import declarations. - The memory layout of packed structures and arrays is implementation-dependent — relevant only on the C side.
- Unpacked arrays passed to sized formal arguments may be implementation-dependent; using open arrays avoids this.
📋 Open Arrays
A formal argument is an open array when one or more of its dimensions is left unspecified (written as []). Open arrays let the same C function handle SV arrays of different sizes — the actual dimensions are determined at each call site.
// Fixed formal argument (specific size) logic [31:0] array32x10 [1:10] // fully sized: packed=32, unpacked=[1:10] // Open packed dimension (any packed width accepted) bit[] // open packed, no unpacked // Open unpacked dimension (any number of elements accepted) logic [31:0] array32xN [] // packed=32 fixed, unpacked=open // Open packed dimension, fixed unpacked logic [] arrayNx3 [3:1] // packed=open, unpacked=[3:1] fixed // Both dimensions open bit [] arrayNxN [] // packed=open, unpacked=open // Example: generic C function handling different unpacked sizes // import accepts any 10x5 or 64x8 actual arguments through one import typedef struct {int i;} MyType; import "DPI" function void foo(input MyType i [][]); MyType a_10x5 [11:20][6:2]; MyType a_64x8 [64:1][-1:-8]; foo(a_10x5); // legal foo(a_64x8); // also legal — open array accepts both
▶ Calling Imported Functions
Calling an imported function looks and behaves exactly like calling a native SV function:
- Arguments with default values can be omitted.
- Arguments can be passed by name if all formals are named.
- Argument compatibility and coercion rules are the same as native SV.
- If coercion is needed, a temporary is created; for inout the temporary is copy-in/copy-out.
- Value-change events for output and inout arguments are detected and propagated after the C function returns to SV.
📋 The WYSIWYG Principle
What You Specify Is What You Get. The types of formal arguments of imported functions are exactly as declared in the import statement — no coercions or size changes happen between SV’s view of the formal and the C function’s view of the formal. Each side sees exactly the type it declared.
This matters because the SV and C declarations are in different languages — the compiler cannot compare them directly. The programmer is responsible for matching types correctly on both sides. Tools may generate C header files to help.
Fixed formal arguments
The formal has exactly the packed and unpacked ranges declared in the import statement — regardless of what the caller passes. The caller must supply a compatible actual argument.
Open array formal arguments
The unspecified ranges are determined at each call site from the actual argument. The C function receives the actual dimensions of whatever array the caller supplies at that particular call.
📋 Exported Functions
An exported function makes an SV function callable from C. The syntax is simple — just name the function to export.
// Export SV function 'compute' under its own name export "DPI" function compute; // Export SV function under a different C name export "DPI" c_compute = function compute;
Export rules
- An export declaration must appear in the same scope where the function is defined.
- Only one export declaration is allowed per function per scope.
- Multiple export declarations with the same C name are allowed if they are in different scopes and have the same type signature.
- Class member functions cannot be exported. All other SV functions can be exported.
- All exported functions are implicitly
context— they always need an associated instance context. - Argument types and result types must satisfy the same restrictions as imported functions.
📋 Exported Tasks
SV tasks can be exported to C using identical syntax to exported functions. All rules for exported functions apply to exported tasks, with these additional points:
- An exported task has no return value type in SV. On the C side, an exported task is declared as returning
int, where the integer value indicates whether a disable is active (1) or not (0). - It is never legal to call an exported task from within an imported function — this mirrors the native SV rule that a function cannot enable a task.
- An imported task can call an exported task only if the imported task is declared
context.
⛔ Disabling DPI Tasks — The Disable Protocol
A disable statement in SV can target a block that contains an active DPI call chain. When this happens, the C code must follow a clean protocol to acknowledge the disable and perform any necessary resource cleanup (close file handles, release memory, etc.) before returning.
An imported task/function enters the disabled state only after returning from a call to an exported task/function that itself was disabled. The C code can detect this using the API function svIsDisabledState().
The four-point protocol
- When an exported task returns because of a disable → it must return
1. Otherwise → return0. (Guaranteed by the SV simulator.) - When an imported task returns because of a disable → it must return
1. Otherwise → return0. (Programmer’s responsibility.) - Before an imported function returns because of a disable → it must call
svAckDisabledState(). (Programmer’s responsibility.) - Once in the disabled state, the import must not call any further exported tasks or functions. (Programmer’s responsibility.)
svIsDisabledState() returns 0. Only the exported task’s own disable handling is involved.
📋 Quick Reference
Declaration syntax
| Direction | Syntax | Notes |
|---|---|---|
| Import function | import “DPI” [pure|context] function ret_t name(args); | Completes instantly; zero sim time |
| Import task | import “DPI” [context] task name(args); | Can block; consume sim time; never pure |
| Export function | export “DPI” [c_id =] function sv_name; | Must be in defining scope; always context |
| Export task | export “DPI” [c_id =] task sv_name; | Returns int (0=ok, 1=disabled) |
Key rules at a glance
- pure: no side effects, no I/O, no global state, no PLI/VPI. Functions only. Allows compiler to eliminate calls or reuse cached results.
- context: required to call exported SV functions, PLI, or VPI. Carries an implicit scope. Suppresses some compiler optimisations.
- No qualifier: can have C side effects (file, global var) but must not call exported SV or PLI/VPI.
- ref is illegal in import declarations.
- class types cannot cross the DPI boundary.
- Return types: void, byte, shortint, int, longint, real, shortreal, chandle, string, scalar bit/logic, packed arrays ≤ 32 bits.
- Open arrays: use
[]for unspecified dimensions; actual dimensions determined at each call site. - Disable protocol: imported tasks return 1 on disable; imported functions call
svAckDisabledState(); no further exported calls after entering disabled state.
