An export declaration makes a SystemVerilog function or task callable from C. The export is written in the scope that defines the function.
That scope matters because C must call the export in the context of a specific instance of that scope.
The next listing declares an export with the context import that triggers it. It needs its C half linked through DPI-C, so it is not run in this course's checks.
// not-run: needs its C half compiled and linked; the course runner builds SV only (Verilator 5.048 links DPI-C, Icarus 13 has no DPI)
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;
// Context imports that C uses; init captures the scope
import "DPI-C" context function void c_model_init();
import "DPI-C" context function void c_model_step(input int unsigned d);
endmoduleC 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.
A non-context import gives C no scope, so calling an export from it is illegal.
A context import. The simulator records the call site of the context import, and exports that C invokes during the call run in the caller scope.
Deferred callbacks need a scope that outlives one import call. The scope API makes scope a value: svGetScope captures it and svSetScope restores it.
The next listing stores the scope at init time and restores it before a later callback. It needs its C half linked through DPI-C, so it is not run in this course's checks.
// not-run: C half of a DPI pair; the course runner builds SV only (Verilator 5.048 compiles it as C++, so wrap it in extern "C")
// 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
v
[C] svGetScope() --> tb_scope saved
...
| c_model_step(0xDEAD) <- later context import call
v
[C] svSetScope(tb_scope)
|
| report_c_error(42,...) <- export runs in tb_top scope
v
[SV] err_count++ inside tb_topIn whichever instance was captured last. Each instance needs its own stored scope, or callbacks land in the wrong place.
A C model often detects something the testbench must react to, such as a frame end. C cannot touch SV events directly, so it calls an export that fires one.
The next listing shows the SV half of that pattern. It needs its C half linked through DPI-C, so it is not run in this course's checks.
// not-run: needs its C half compiled and linked; the course runner builds SV only (Verilator 5.048 links DPI-C, Icarus 13 has no DPI)
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
endmoduleThe C half counts bytes and fires the export on the delimiter. It needs its C half linked through DPI-C, so it is not run in this course's checks.
// not-run: C half of a DPI pair; the course runner builds SV only (Verilator 5.048 compiles it as C++, so wrap it in extern "C")
// 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;
}
}The cast to unsigned char is deliberate. SV byte is signed and C char signedness varies, so the comparison converts explicitly.
c_push_byte is a context import, so 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 testbench process blocked in wait_frame resumes, so C indirectly controls SV scheduling.
No svSetScope is needed here because the callback happens during the import call itself.
The next listing proves the same event wake-up in plain SystemVerilog. It runs in this course's checks.
timeunit 1ns; timeprecision 1ps;
module tb_event_callback;
event frame_done;
int frame_len;
int got_len;
int bad;
int count;
task automatic notify_frame_done(input int len);
frame_len = len;
-> frame_done;
endtask
task automatic push_byte(input logic [7:0] b);
count++;
if (b == 8'h7E) notify_frame_done(count);
endtask
initial begin
bad = 0; count = 0; frame_len = 0; got_len = 0;
fork
begin @(frame_done); got_len = frame_len; end
begin
#1;
push_byte(8'h11); push_byte(8'h22); push_byte(8'h7E);
end
join
if (got_len !== 3) bad++;
if (bad == 0) $display("PASS: event callback carried length 3 to the waiter");
else $fatal(1, "FAIL: event callback carried a wrong length");
$finish;
end
endmoduleThe wake-up can fire before the waiter blocks on the event. The waiter then blocks forever, since the trigger is already gone.