C Part II · Module 19 · DPI-C Foundations
Where DPI Code Runs: The Simulator Execution Context
Your C is not a library your program calls. It is code loaded into somebody else's program, and four assumptions that hold for a standalone function stop holding there. A static variable is shared by every instance that calls you. Your debug output arrives in the log after the lines it should have appeared between — measured here, printed last despite being called in the middle. Memory you allocate outlives the call and nothing says who frees it. And whether two calls can overlap is a property of the simulator, not of your code.
Advanced22 min readCPart IIVerificationDPI-C
Part II · Module 19 · Chapter 19.7 · Where DPI Code Runs: The Simulator Execution Context
1. Somebody Else's Program
Chapter 19.6 ended with a shared object the simulator can load and a symbol it can find. What happens when it calls is a different question, and the answer starts from one fact:
Your C is not a library your program calls. It is code loaded into a program you did not write.
That program has its own lifetime, its own output stream, its own memory management, and possibly its own threads. Four assumptions that are safe in a standalone C program stop being safe there, and all four are ordinary C problems rather than DPI ones — which is why Part I's chapters keep being the ones cited.
2. State Is Shared
The first and most surprising.
static uint32_t counter = 0;
uint32_t c_next(void) { return ++counter; }Measured, calling it from two different places in one program:
three calls from 'instance A': 1 2 3
one call from 'instance B' : 4 <- SAME counter, not a fresh oneThere is one counter, belonging to the shared object. Four SystemVerilog instances of a module that each call c_next share it; two unrelated testbench components share it; a monitor and a scoreboard share it.
This catches people because the SystemVerilog side has instances and the C side does not. A module instantiated four times has four copies of its variables. A C function imported into that module has one copy of its statics, for all four.
The fix is the ordinary C one: do not keep per-caller state in a static. Either pass the state in:
struct model_state { uint32_t counter; /* ... */ };
uint32_t c_next(struct model_state *s) { return ++s->counter; }or hand out opaque handles the caller holds, which is the same thing with the allocation moved. Either way the state belongs to the caller, which is where the instances are.
And one consequence worth naming: a function with statics cannot be declared pure — Chapter 19.5 §3 established why, and this measurement is the reason it was taken.
3. Output Ordering Is Wrong by Default
This one is measured, and it is the chapter's most practically useful finding.
Your model prints a debug line. The simulator prints lines before and after the call. The log should show them in that order.
static void sim_print(const char *s) { write(1, s, strlen(s)); } /* "the simulator" */
int main(void) {
sim_print("[sim] before the DPI call\n");
printf("[dpi] inside the C function\n"); /* your model */
sim_print("[sim] after the DPI call\n");
}Measured:
[sim] before the DPI call
[sim] after the DPI call
[dpi] inside the C functionThe model's line is last, and it was called in the middle. Measured both to a terminal and through a pipe — which is what a log file or a tool capture looks like — with the same result.
The cause is Chapter 12.4's subject arriving where it does real damage. printf writes into a FILE buffer that is flushed when it fills, at exit, or at a newline if the stream is line-buffered. The simulator's own output does not go through your FILE buffer at all. Two independent paths to the same destination, flushed on different schedules.
The fix:
printf("[dpi] inside the C function\n");
fflush(stdout);Measured:
[sim] before the DPI call
[dpi] inside the C function
[sim] after the DPI callCorrect order.
4. Lifetime Is Not Yours
A model returns a pointer. Who owns what it points at, and for how long?
static char *last = NULL;
const char *c_describe(int code) {
free(last); /* free the previous one */
last = malloc(32);
if (!last) return "";
snprintf(last, 32, "code=%d", code);
return last; /* valid until the NEXT call */
}Measured:
first : code=7
second : code=7
third : code=9That works, and the contract is invisible. A caller that keeps the pointer from the second call and reads it after the third is reading freed memory — Chapter 9.10's use-after-free, with nothing in the interface warning about it.
Chapter 9.8 established the rule and it is unchanged here: for each allocation, exactly one piece of code is responsible for freeing it, and that responsibility is written down. What DPI adds is that the two pieces of code are now in different languages, so there is no shared header to write it in.
The three workable contracts:
The model owns it, valid until the next call. The measured example. Cheap, and the caller must copy if it needs it longer. Write it in the comment.
The caller supplies the buffer. The model writes into memory the caller owns, and a length comes with it — Chapter 14.7's discipline, which Chapter 19.4 §3 already applied to the length argument.
The model allocates and the caller frees, through a release function the model also provides. Most flexible, most error-prone, and it requires the caller to be disciplined in a language where your free is not available.
The second is almost always right for DPI, for the reason Chapter 14.7 gave: a pointer that crosses a boundary should have a length crossing with it, and a caller-supplied buffer makes both explicit.
5. Threading Is Not Guaranteed
From the LRM and tool documentation — and this is the one that cannot be measured here, so it is marked as such.
Simulators may execute SystemVerilog processes concurrently, and a multi-core simulation may call your imported function from more than one thread. Whether a particular simulator does, and under what conditions, is a property of that tool.
What follows if it does is Chapter 17.5's three non-guarantees becoming live:
- a static counter is not atomic — 17.5 §2 measured
v++as three instructions, andvolatiledoes not help - there is no mutual exclusion — 17.5 §4, and nothing in DPI provides one
- ordering between your accesses and anything else is not guaranteed — 17.5 §3
The practical response is the one §2 already argued for a different reason: a function with no shared mutable state has no threading problem. Pass the state in, and the question does not arise.
Where shared state is genuinely required — a cache, a log, a counter of calls — then it needs a real mechanism, and Chapter 17.5 §7 named them: _Atomic for a counter, a lock for a critical section. volatile is not one of them, which is the thing 17.5 exists to say.
6. What This Means for How You Write the C
Four rules, each from one of the sections above:
No per-caller state in statics. Pass a state pointer, or hand out handles. §2.
Do not print; report through an export. Or flush every line. §3.
Write the lifetime contract next to the declaration, and prefer a caller-supplied buffer with a length. §4.
Assume you may be called concurrently unless the tool promises otherwise, which mostly means having no shared mutable state at all. §5.
Every one of those is also good standalone C — Chapter 13.11 would endorse all four — which is the thread running through this module. DPI does not require you to write C differently. It removes the margin you had for writing it badly.
7. The Misconception: It Is Just a Function
"It is an ordinary C function. The simulator calls it, it returns, nothing else is different."
The function is ordinary. The process it runs in is not yours, and each of the four sections above is an assumption that holds in a standalone program and fails here.
In a standalone program: a static is yours alone, your printf ordering is the ordering you see, memory you allocate lives until you free it, and you know whether you have threads.
In a simulator's process: the static is shared by everything that calls you, your output is one of two unsynchronised streams, your memory outlives calls with no agreed owner, and whether you have threads is the tool's decision.
The accurate framing:
The C is ordinary. The context is not. Everything that goes wrong here is an ordinary C problem that a standalone program would not have exposed.
Which is why every fix in this chapter came from Part I — Chapter 12.4 for buffering, Chapter 9.8 for ownership, Chapter 9.10 for use-after-free, Chapter 17.5 for what volatile does not do. There is no DPI-specific defensive technique, because there is no DPI-specific problem.
8. Module 19 in One Page
The module's description was "the SystemVerilog/C boundary explained before any declaration is written… syntax arrives only after the architecture that makes it readable", and it had an unusual constraint: DPI does not run on this machine. Chapter 19.1 §4 verified that at both declaration scopes, with zero mentions in the tool's manual and no svdpi.h on the system, and every chapter has marked its claims measured or from the LRM.
| Chapter | The measured result |
|---|---|
| 19.1 | DPI rejected at module and $unit scope; man iverilog mentions it zero times. The C side is ordinary C — c_add(40,2) = 42, check value 0xF4, sanitiser-clean, no DPI header |
| 19.2 | The C is ordinary in both directions. An export is an undefined symbol — nm -u shows _sv_report, linking alone gives one error. The stub practice runs the whole C half with no simulator |
| 19.3 | An imported task returns int as a completion status with outputs via pointers — measured rc=0 data=84, rc=1 output untouched |
| 19.4 | Four vectors — 0xF4, 0x52, 0x00, 0x00 — and a wrong length is global-buffer-overflow at crc.c:5 under ASan, or 40 and a clean exit without it |
| 19.5 | __attribute__((const)) collapsed two calls into one; a debugging printf in such a function printed one line for two calls |
| 19.6 | _c_add as C, __Z5c_addii as C++, restored by extern "C"; a static function is absent entirely from nm -gU |
| 19.7 | One static counter across callers — 1 2 3 then 4; a model's printf arriving last despite being called in the middle |
Three threads run through all seven.
The C half is ordinary C, and that is the module's most useful fact. No header for the scalar cases, no marking, no simulator awareness — which means it can be written, compiled, tested, sanitised and debugged with no simulator present. Chapters 19.2 §5, 19.4 §6 and 19.6 §7 each built on that, and the payoff was measured: a length bug found by AddressSanitizer in milliseconds, with a line number, that a simulation would have reported as a wrong checksum.
The failures that are quiet are the ones you can check offline. A mangled symbol, a static function, a wrong length, a broken purity promise — all diagnosable with nm, a sanitizer and a test program. The failures that need a simulator — a declaration mismatch, a missing library path — are the loud ones.
Every defensive technique came from Part I. Buffering from 12.4, ownership from 9.8, pointer-and-length from 14.7, the optimiser's licence from 17.1, what volatile does not promise from 17.5. There is no DPI-specific discipline, and §7 argues that is because there is no DPI-specific problem.
9. What You Should Be Able to Say Now
- Your C runs in somebody else's process — its lifetime, its output stream, its memory management, possibly its threads.
- State is shared. Measured: one static counter across callers —
1 2 3then4. SystemVerilog has instances; your statics do not. Pass state in. - A function with statics cannot be
pure(19.5 §3). - Output ordering is wrong by default. Measured: the model's line printed last despite being called in the middle, both to a terminal and through a pipe.
fflushfixes it; measured correct afterwards. - That is Chapter 12.4's buffering — two unsynchronised paths to one destination.
- Prefer an export to a
printfso the message goes through the testbench's own mechanism, in order, with its severity levels (19.2 §2). - Instrumenting a system you do not control changes it — the third instance in Part II, after 17.6 §4's barrier and 19.5 §3's elided call.
- Lifetime has no default contract. Three workable ones; a caller-supplied buffer with a length is almost always right (14.7). Write it beside the declaration.
- Threading is the tool's decision — not measurable here. If it happens, 17.5's three non-guarantees are live, and no shared mutable state means no problem.
- The C is ordinary; the context is not. Every fix in this chapter came from Part I.
10. Exercises
- Reproduce §2's counter and call it from two functions. Then rewrite it to take a state pointer and confirm two independent counters.
- Reproduce §3's interleaving, both to a terminal and through a pipe. Add
fflushand confirm the order. Then trysetvbuf(stdout, NULL, _IONBF, 0)instead and compare. - §3's callout prefers an export to a
printf. Write the C side of that — a prototype and a call — and the stub you would test it with (19.2 §5). - Implement §4's three lifetime contracts for the same function. For each, write the comment that states it, and say which you would ship.
- Construct the use-after-free in §4's measured example — keep the pointer from one call and read it after the next. Confirm what AddressSanitizer says.
- A model keeps a cache of previously computed results. List what §2 and §5 together require of it, and say whether it can be
pure. - For each of §6's four rules, name the Part I chapter it comes from and state the rule in that chapter's own terms.
11. Summary
- Your C is loaded into a program you did not write. Four standalone assumptions fail.
- State is shared — measured
1 2 3then4. SystemVerilog has instances; your statics do not. Pass state in. - Output ordering is wrong by default — measured, the model's line printed last;
fflushfixes it. Chapter 12.4's buffering, two unsynchronised paths. - Prefer an export to a
printf— right order, right mechanism, right severity. - Instrumenting a system you do not control changes it — the third instance in Part II.
- Lifetime has no default owner. Prefer a caller-supplied buffer with a length (14.7, 9.8); write the contract beside the declaration.
- Threading is the tool's decision — not measurable here. No shared mutable state, no problem; otherwise 17.5's mechanisms, and not
volatile. - The C is ordinary; the context is not. Every defence came from Part I.
12. What Comes Next
This module covered the boundary's architecture — the two directions, functions against tasks, the qualifiers, the build, and the process your C runs in. One thing was deferred at every opportunity: what actually happens to a value as it crosses.
Chapter 19.4 §3 declared input byte unsigned data[] and said an array arrives as a pointer. Chapter 19.1 §3 said SystemVerilog's int maps to C's int and measured sizeof(int) as 4. Both are true and both are the easy cases.
Module 20, DPI-C Type Mapping, is the hard ones, and its own description is blunt about why it matters: "the heart of DPI and the source of most of its bugs. A SystemVerilog type and a C type are two different descriptions of storage."
It begins with why the mapping is not one to one (20.1) and with svdpi.h itself (20.2) — the header this machine does not have, which Chapter 19.1 §4 noted. Then the integer types (20.3) and signedness across the boundary (20.4), which is Chapter 14.2 §4's decision arriving where two type systems disagree about it. Then the one with no C counterpart at all: 2-state against 4-state values (20.5), because C has no way to represent x or z, and packed data through svBitVecVal and svLogicVecVal (20.6). Then real and floating point (20.7), strings (20.8), structures (20.9) — where Chapter 14.5's padding judgement returns one level up — and chandle, the opaque pointer that is how §2's state-passing advice is actually implemented (20.10).
That last one is worth looking forward to. This chapter argued that per-caller state must not live in a static and should be passed in. chandle is the SystemVerilog type that holds a C pointer, and it is what makes that advice expressible across the boundary.
Standards & specifications
- Governing standard
- IEEE Std 1800 (SystemVerilog)(opens IEEE in a new tab)
Defines SystemVerilog language semantics — syntax, data types, scheduling and the behaviour a conforming simulator must produce. It does not define tool-specific synthesis support or vendor methodology.
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.
