C++ Part II · Module 28 · C/C++ and SystemVerilog DPI
Exporting a SystemVerilog Function to C
An import lets the testbench call the model; an export lets the model call the testbench. It is the less-used direction and the more surprising one, because it collides with something Chapter 28.2 established: a C function has no scope. When SystemVerilog calls C, the call carries a caller; when C calls SystemVerilog, there is no caller to carry — so the question of which instance of which checker the call lands in has to be answered by something other than the call itself. That is the whole difficulty of the export direction, and it is measured here rather than described, because the problem is an ordinary C++ one about state the callee cannot see. The measurement shows the same callback crediting two different checker instances depending only on what was established beforehand, with nothing in either language objecting to the wrong answer. The chapter also separates exporting a function from exporting a task, which differ in exactly one respect that matters and several that do not.
Expert20 min readC++Part IIVerificationDPI
Part II · Module 28 · Chapter 28.4 · Exporting a SystemVerilog Function to C
1. What an Export Is
An export makes a SystemVerilog function or task callable from C. The declaration goes on the SystemVerilog side — like an import, which is the part that catches people out — but it names something that already exists there:
// Reference syntax -- not executed here, per Chapter 28.1 §5.
module checker;
function void note_mismatch(int index);
$display("mismatch at %0d in %m", index);
endfunction
export "DPI-C" function note_mismatch; // <-- no signature, just the name
endmoduleTwo differences from an import are worth noticing immediately.
The export declaration carries no signature. Just export "DPI-C" function note_mismatch; — the types come from the SystemVerilog function's own declaration, because it is defined right there. An import needs a signature because the thing it describes is elsewhere.
And the C side declares it as an ordinary function:
extern "C" void note_mismatch(int index); // provided BY the simulatorSo from C's point of view there is no difference at all between calling an exported SystemVerilog function and calling any other C function. The simulator supplies the symbol.
2. When You Actually Need One
Exports are rarer than imports, and the honest list of good reasons is short.
Reporting into the testbench's own mechanism. The model finds a mismatch and wants it recorded by the checker that owns the comparison — with the right instance name, the right verbosity filter, the right counter. Chapter 27.6's scoreboard reporting, reached from the model.
Fetching configuration the testbench owns. A mode, a timeout, a plusarg the testbench already parsed.
Letting the model request something timed. This is the case that requires a task rather than a function, and §3 is about why.
And three reasons that look good and are not.
Passing data back. An output argument on the import is simpler, and it keeps the direction of control in one place.
Logging. Writing to a file from C++ is easier than calling back, unless you specifically need the testbench's filtering.
Reaching the DUT. The model should not be touching signals at all — Chapter 27.1 §3.
The pattern to prefer, where it fits: make the import's return value or an output argument carry the answer, and use an export only when the model genuinely needs something the testbench alone can do. Every export adds a direction of coupling, and the one problem exports have is the subject of §4.
3. Function Against Task
The distinction matters in exactly one way, and it is the way Chapter 28.2 §4 set up.
| exported function | exported task | |
|---|---|---|
| may consume simulation time | no | yes |
| the C declaration returns | the function's return type | int — a status, not the result |
| the import that calls it must be | anything | declared context |
| callable from | any C code the simulator invoked | only within a context import's call |
The one rule to remember: if the exported thing can consume time, it must be a task, and the import that leads to it must be declared context. A C function reached from a non-context import has no established scope and no legitimate way to block, and calling a task from there is an error rather than a slow path.
And the detail that surprises people: an exported task's C declaration returns int. That value is a status — non-zero conventionally meaning the task was disabled — and the task's actual outputs come back through output arguments. So:
// Reference syntax.
task automatic wait_cycles(int n);
repeat (n) @(posedge clk);
endtask
export "DPI-C" task wait_cycles;extern "C" int wait_cycles(int n); // returns a STATUS, not a valueMistaking that int for a result is a real bug and a quiet one, because an int is exactly what a careless reading expects.
4. The Scope Problem, Measured
Here is the part that is genuinely hard, and it is not about syntax.
A C function has no scope. When the model calls note_mismatch(7), the symbol is a single C function — but in a real testbench there may be four checker instances, and the exported function is a member of a module instance, so the call has to land in one of them. Nothing in the call says which.
DPI's answer is svSetScope and svGetScope: the scope is ambient state, established before the call and read by the simulator when the exported function runs. A context import receives its caller's scope automatically; anything else has to be told.
That mechanism needs svdpi.h, which Chapter 28.1 §5 measured as absent here. The problem, though, is ordinary C++ — a callee that depends on state it cannot see — so it can be measured directly with an explicit context pointer standing in for the ambient scope:
struct Scope { const char* name; long hits; };
static Scope gA{"tb.agent[0].checker", 0};
static Scope gB{"tb.agent[1].checker", 0};
static Scope* gCurrent = nullptr; // what svSetScope would establish
extern "C" void tb_note_mismatch(int index) /* the exported function */
{
if (!gCurrent) { std::printf(" call with NO scope set -- nowhere to land\n"); return; }
++gCurrent->hits;
std::printf(" mismatch %d recorded in %s\n", index, gCurrent->name);
}The model calls the same name every time and knows nothing about scopes:
extern "C" void model_run(int n)
{
for (int i = 0; i < n; ++i)
if (i % 2 == 0) tb_note_mismatch(i);
}Three runs — no scope, then each of the two instances:
A. the callback with no scope established
call with NO scope set -- nowhere to land
B. scope set to agent[0]
mismatch 0 recorded in tb.agent[0].checker
mismatch 2 recorded in tb.agent[0].checker
C. scope set to agent[1]
mismatch 0 recorded in tb.agent[1].checker
mismatch 2 recorded in tb.agent[1].checker
mismatch 4 recorded in tb.agent[1].checker
D. where the hits landed
tb.agent[0].checker : 2
tb.agent[1].checker : 3
a callback with the wrong scope set credits the wrong instance,
and nothing in either language objectsThree things to read off it.
The model's code is identical in all three runs. model_run calls tb_note_mismatch with the same arguments; only ambient state differs. So the correctness of the callback is not a property of the model at all — it is a property of what the caller established, which is exactly why this is the hard part of the export direction.
With no scope established there is nowhere for the call to land. The mock prints a message; a real DPI call with no scope is an error or lands somewhere unintended, depending on the simulator. Either way the information is lost.
And the wrong scope is silent. Run C credited agent[1] with mismatches that the model found while processing — as far as the model knows — the same data as run B. Two counters, three hits and two hits, and no diagnostic anywhere. A scope bug does not look like a bug; it looks like the other instance being busier.
5. Getting the Scope Right
Three arrangements, in order of preference.
Use a context import, and let the scope come for free. An import declared context passes its caller's scope to the C side automatically, so an exported function called from within that import's execution lands in the right place with nothing to manage. This covers the overwhelming majority of real uses, because the usual shape is "the testbench calls the model, and the model calls back during that call".
// Reference syntax.
import "DPI-C" context function int model_step(input byte unsigned data);Capture and restore it explicitly when the callback happens outside an import's call — from a thread the model owns, or from a callback the model registered earlier. The idiom is to save svGetScope() during a context import and svSetScope() before calling back.
Or avoid the problem: give the model a handle instead of relying on ambient scope. If the C++ model holds a chandle per instance — Chapter 28.13 §2's design — then the model knows which instance it belongs to without any ambient state, and the callback can carry that identity as an argument. This is strictly more robust because the identity travels in the call rather than beside it, which is the same reasoning Chapter 26.4 §6's callout used for mutexes: an association that is not expressible in the code is one that can be got wrong.
The anti-pattern is the one the measurement shows: a single global for the current scope, set in one place and read in another. It works until two things are in flight, and then it silently credits the wrong instance.
6. The Failure Modes
| symptom | likely cause |
|---|---|
| the callback's output names the wrong instance | wrong scope established — §4 |
| no output at all from the callback | no scope established, or the export was never declared |
| an undefined symbol for the exported name | the export declaration is missing on the SystemVerilog side |
| the exported task returns a confusing integer | that int is a status, not the result — §3 |
| the call works from one import and not another | the other import is not declared context |
| the simulation hangs in the callback | an exported task blocked; a function cannot, a task can |
The third row is worth a note because it is the one with a clean error message. An exported function that was never declared export simply does not exist as a symbol, so the link fails and names it — which is the best possible outcome and the only failure in this table that a tool catches for you.
7. The Table
| the question | the answer | measured |
|---|---|---|
| what is an export | a SystemVerilog function made callable from C | §1 |
| where is it declared | the SystemVerilog side, like an import | §1 |
| does the declaration carry a signature | no — only the name | §1 |
| what does C see | an ordinary extern "C" function the simulator supplies | §1 |
| when do you need one | reporting into the testbench, fetching its configuration, timed requests | §2 |
| function against task | only a task may consume simulation time | §3 |
| what does an exported task return in C | int — a status, not the result | §3 |
| what does a task require of the import | it must be declared context | §3 |
| why is scope the hard part | a C function has no scope; the call must land somewhere | §4 |
| what did the measurement show | identical model code crediting 2 hits to one instance and 3 to another | §4 |
| is a wrong scope diagnosed | no — it looks like the other instance being busier | §4 |
| best way to get it right | a context import, then explicit save/restore, then a handle | §5 |
| who can test an export | neither side alone — the failure is in the pairing | §5 callout |
8. What to Actually Do
- Prefer an import with an
outputargument over an export wherever the answer can travel that way — it keeps correctness inside the testable half. - Use an export only when the testbench alone can do the job: its reporting mechanism, its configuration, or something timed.
- Declare the import
contextwhenever an exported function may be called during it, and let the scope come for free. - Use a task, not a function, for anything that can consume time — and remember its C return is a status.
- Never treat an exported task's
intas the result. Outputs come back throughoutputarguments. - Make the instance identity travel in the call — a
chandleor an id argument — rather than relying on ambient scope. - Never keep a single global "current scope" set in one place and read in another.
- Expect neither side's unit tests to catch a scope bug, and add a test that asserts which instance was credited.
- Check that the
exportdeclaration exists when you get an undefined symbol for a callback — that is the one failure here with a clean message.
9. Mistakes Worth Making Once
Putting the export declaration on the C side. Both directions are declared in SystemVerilog.
Giving the export declaration a signature. It takes only a name.
Calling an exported task from a non-context import. No scope, no legitimate way to block.
Reading an exported task's return value as the answer. It is a status.
Relying on a global current-scope variable. §4: two instances, 2 and 3 hits, no diagnostic.
Testing the model and the checker separately and believing the export is covered. The failure is in the pairing.
Using an export to pass data back. An output argument is simpler and keeps the model responsible.
Using an export to reach the DUT. The model should not be touching signals.
Using an exported function for something that needs to wait. Only a task may consume time.
10. Interview Relevance
The frozen curriculum marks this High. The question is usually what is a DPI export, and when would you use one?
The complete short answer: an export makes a SystemVerilog function or task callable from C, it is declared on the SystemVerilog side like an import but carries only a name rather than a signature, and from C's point of view it is an ordinary extern "C" function the simulator supplies. You need one when the model must reach something only the testbench can do — its reporting mechanism with the right instance name and verbosity, configuration it already parsed, or a request that consumes simulation time.
The two details that mark real use are the task distinction and the scope problem. Only a task may consume simulation time, the import that leads to it must be declared context, and an exported task's C return is an int status rather than the result — outputs come back through output arguments, and mistaking that status for the answer is a quiet bug.
Then scope, which is the real content. A C function has no scope, so when it calls back there is nothing in the call to say which instance of which checker it should land in; DPI answers it with ambient state established by svSetScope, and a context import supplies it automatically. The measurement is worth quoting: with byte-identical model code, the same callback credited one checker instance with two hits and another with three depending only on what was established beforehand, and nothing in either language objected. Which leads to the senior observation — a scope bug is invisible to unit tests of either side, because the model is behaving correctly and the testbench is simply being called, so the identity is better passed in the call as a handle or an id than established beside it.
11. What You Should Be Able to Say Now
- That an export makes a SystemVerilog function or task callable from C, is declared on the SystemVerilog side, and carries only a name.
- That C sees it as an ordinary
extern "C"function supplied by the simulator. - The legitimate uses: the testbench's reporting mechanism, its configuration, and anything that must consume time.
- That only a task may consume simulation time, that the import leading to it must be
context, and that an exported task's C return is anintstatus rather than the result. - That a C function has no scope, so the hard part of the export direction is where the call lands.
- That with identical model code, the same callback credited 2 hits to one instance and 3 to another purely from ambient state — with no diagnostic.
- That the preference order is a
contextimport, then explicit save and restore, then carrying the identity in the call as a handle or an id. - That a single global "current scope" is the anti-pattern, because it fails silently as soon as two things are in flight.
- That a scope bug is invisible to unit tests of either side, since the failure lives in the pairing — the same shape as a deadlock or a shared-mistake model.
- That the one export failure with a clean message is a missing
exportdeclaration, which shows up as an undefined symbol.
12. Exercises
Exercise 1 — the declaration. Write an exported function and its C declaration. Note what the export declaration does not contain.
Exercise 2 — the scope measurement. Build §4's two-instance callback with an explicit context pointer and reproduce the 2-and-3 result. Then set the wrong scope deliberately and confirm nothing objects.
Exercise 3 — no scope at all. Call the callback with no scope established and decide what your real flow should do in that case.
Exercise 4 — the status integer. Write an exported task, declare it in C, and write down what its return value means. Then write the line that misuses it.
Exercise 5 — identity in the call. Rewrite the callback so the instance identity is an argument rather than ambient state, and satisfy yourself that the correct instance is now visible by inspection.
Exercise 6 — the test neither side has. Write the test that would have caught a scope bug, and note which component it belongs to.
13. Summary
- An export makes a SystemVerilog function or task callable from C. It is declared on the SystemVerilog side, carries only a name, and appears to C as an ordinary
extern "C"function the simulator supplies. - Use one only when the testbench alone can do the job — its reporting mechanism, its configuration, or something timed. Otherwise an
outputargument on the import is simpler and keeps correctness in the testable half. - Only a task may consume simulation time, the import that reaches it must be declared
context, and an exported task's C return is anintstatus rather than the result. - The hard part is scope, because a C function has none. Measured with an explicit context pointer standing in for the ambient scope: byte-identical model code credited two hits to
agent[0]and three toagent[1], decided entirely by what was established beforehand. - And the wrong scope is silent. It does not look like a bug; it looks like the other instance being busier. No diagnostic appears in either language.
- Preference order: a
contextimport that supplies the scope for free; explicit save and restore for callbacks outside an import's call; and best of all, carry the instance identity in the call as a handle or an id, so the correct instance is visible by inspection. - Never use a single global current-scope variable set in one place and read in another.
- A scope bug is invisible to unit tests of either side, because the model is behaving correctly and the testbench is simply being called — so the test that catches it must assert which instance was credited.
- The one export failure with a clean message is a missing
exportdeclaration, which the linker reports as an undefined symbol.
Chapter 28.5 goes under both directions to the thing that makes a symbol findable at all: the difference between C and C++ linkage.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
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 C++ Programming curriculum.
