Part 7 · Advanced & Integration · Intermediate
Exports & Context Calls
export "DPI-C" to let C call SV, context imports, svSetScope/svGetScope, and the callback-into-TB pattern.
Exports: the reverse direction
An export "DPI-C" declaration makes a SystemVerilog function or task callable from C. The export is written inside the scope (module, interface, package, or program) that defines the function — and that scope matters, because the C side must call the export in the context of a specific instance of that scope.
module tb_top;
int unsigned err_count;
// SV function that C will call
function void report_c_error(input int code, input string msg);
err_count++;
$display("[C-MODEL] error %0d: %s", code, msg);
endfunction
// Make it visible to C — declared in the same scope
export "DPI-C" function report_c_error;
// The context import that will eventually trigger the callback
import "DPI-C" context function void c_model_step(input int unsigned d);
endmoduleWhy exports require context imports
C can only call an exported SV function while the simulator knows which instance scope the call belongs to. That scope is established when SV calls into C through a context import: the simulator records the call site, and any export the C code invokes during that call executes in the caller's scope. A non-context import gives C no scope, so calling an export from it is illegal.
svSetScope and svGetScope
Sometimes C needs to call an export later — from a different import call, or after stashing state — not just during the original call. The scope API makes scope a first-class value: svGetScope() captures the current scope as an svScope handle, and svSetScope() restores it before invoking the export.
// c_model.c
#include "svdpi.h"
/* prototype of the exported SV function */
extern void report_c_error(int code, const char* msg);
static svScope tb_scope; /* captured scope for later callbacks */
void c_model_init(void)
{
/* called via a context import; remember where we live */
tb_scope = svGetScope();
}
void c_model_step(unsigned int data)
{
if (data == 0xDEADu) {
/* restore scope before calling back into SV */
svSetScope(tb_scope);
report_c_error(42, "illegal data word");
}
}SCOPE FLOW [SV] ↔ [C]
tb_top (instance scope)
│
│ c_model_init() ← context import call
▼
[C] svGetScope() ──► tb_scope saved
...
│ c_model_step(0xDEAD) ← later context import call
▼
[C] svSetScope(tb_scope)
│
│ report_c_error(42,...) ← export executes in tb_top scope
▼
[SV] err_count++ inside tb_topPattern: C model signals an event to the TB
The classic use: a C model detects something asynchronously to the SV call flow (end of a frame, an internal error) and needs to wake the testbench. C cannot touch SV events directly, but it can call an exported function that does.
module c_bridge;
event frame_done;
int frame_len;
function void notify_frame_done(input int len);
frame_len = len;
-> frame_done; // wake any waiting TB process
endfunction
export "DPI-C" function notify_frame_done;
import "DPI-C" context function void c_push_byte(input byte b);
// TB side consumes the event like any other
task automatic wait_frame(output int len);
@(frame_done);
len = frame_len;
endtask
endmodule// frame_model.c
#include "svdpi.h"
extern void notify_frame_done(int len);
static int count = 0;
void c_push_byte(char b)
{
count++;
if ((unsigned char)b == 0x7E) { /* frame delimiter */
notify_frame_done(count); /* context call back into SV */
count = 0;
}
}Walkthrough
c_push_byte is a context import — every call carries the c_bridge instance scope.
On the delimiter byte, C calls the export, which fires the SV event in that scope.
Any TB process blocked in wait_frame() resumes — C indirectly controlled SV scheduling.
No svSetScope needed here because the callback happens during the import call itself.
Key takeaways
export "DPI-C" makes an SV function/task callable from C; declare it in the defining scope.
C may only call exports with a valid scope — established by a context import or svSetScope.
svGetScope/svSetScope let C stash a scope and call back later — the basis of persistent C-model callbacks.
C signals the TB by calling an export that fires an SV event — never by touching SV objects directly.
Common pitfalls
Calling an export from a non-context import — undefined behavior; usually a crash or scope error.
Forgetting svSetScope before a deferred callback — the export runs in the wrong (or stale) instance.
Exporting a blocking task and calling it from a C function import — functions cannot consume time; use task imports.
Multiple TB instances sharing one C model with a single static svScope — callbacks all land in one instance.