C++ Part II · Module 28 · C/C++ and SystemVerilog DPI
The DPI Architecture: Two Languages, One Process
DPI is usually taught as a list of declarations, which makes it look like a syntax to memorise. It is more useful to see it as a boundary between two things that disagree about almost everything — what a value is, and when code runs — with a contract stretched across the gap and nothing enforcing it. SystemVerilog has four-state logic, packed and unpacked arrays, arbitrary bit widths and a notion of simulation time; C++ has two-state integers of fixed widths, one array layout and no notion of time at all. The chapter draws that boundary, names what crosses it in each direction, and then dwells on the single architectural fact that everything else follows from: the C++ does not run in a separate process. It is compiled into a shared object the simulator loads, so a call costs what a function call costs, your model shares the simulator's heap, and your null dereference ends the regression run with no waveform written.
Advanced20 min readC++Part IIVerificationDPI
Part II · Module 28 · Chapter 28.2 · The DPI Architecture: Two Languages, One Process
1. The Picture
Two languages, one process, and a contract in the middle crossed in both directions.
Three things to read off it. The two sides disagree about what a value is — §3. The two sides disagree about when code runs — §4. And the whole picture is one process — §5, which is the fact with the most consequences.
2. What a Binding Is Made Of
A binding is declared twice, and that is the structural fact to hold on to.
// SystemVerilog: this is a DECLARATION. Reference syntax -- not executed here.
import "DPI-C" function int crc32(input int data);// C++: this is the DEFINITION.
extern "C" int crc32(int data) { return someAlgorithm(data); }The simulator reads the first and generates the marshalling code; the linker resolves the name against the second. Neither step compares the two for agreement, because by the time the linker is involved the only thing left of the declaration is a symbol name — Chapter 28.5 measures exactly what survives.
Four pieces make up the contract:
| piece | who states it | checked? |
|---|---|---|
| the name | both sides | yes — by the linker, as a symbol |
| the arity | both sides | no |
| each argument's type | both sides | no |
the qualifier (pure, context) | SystemVerilog only | no — it is a promise about C |
Only the first row is machine-checked, and only as a string. Chapter 28.12 §2 measures a mismatch in the second and third rows that links cleanly and returns a wrong number, and Chapter 28.11 §3 measures the cost of a false claim in the fourth.
3. Two Type Systems
The deepest disagreement, and the source of Chapters 28.7 through 28.10.
| SystemVerilog | C++ | |
|---|---|---|
| bit values | four: 0, 1, X, Z | two: 0, 1 |
| integer widths | arbitrary — bit [11:0] | fixed — 8, 16, 32, 64 |
| arrays | packed and unpacked, different things | one kind, contiguous |
| array bounds | any range, ascending or descending | always 0-based ascending |
| strings | a dynamic first-class type | a pointer and a convention |
| structs | packed or unpacked | layout chosen by the ABI |
The four-state problem is the one worth measuring, because it is the one that silently loses information:
E. 4-state logic is NOT a C scalar
32 bits of 4-state needs 8 bytes (two words), not 4
aval=0x0000000F bval=0x0000000A
bits where bval is 1 (not 0 or 1) : 2 of 32
reading aval alone yields 15 and DISCARDS every bval bitThirty-two bits of four-state logic require two thirty-two-bit words, not one: a value word and a second word recording which bits are not 0 or 1. The standard calls the pair svLogicVecVal and names its members aval and bval; the IEEE 1800 encoding is the specified table below, which I am quoting rather than measuring, because no simulator here can produce a four-state value to check it against:
aval bit | bval bit | value |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 0 | 1 |
| 0 | 1 | Z |
| 1 | 1 | X |
What is measured is the consequence, and it is the important half: in the transcript two of the thirty-two bits had bval set, and reading aval alone yields a definite 15 with no indication that two of its bits were unknown. That is the mechanism behind a finding this track has now made twice — the C track's Module 25 measured an X read through an integer interface arriving as a number — and it is why Chapter 27.1 §6 puts X-propagation checks on the list of things a C++ model must not be asked to do.
So the practical rule is to keep four-state values out of the interface. Declare DPI arguments as int, byte, bit — the two-state types — and let SystemVerilog resolve any X before the call. A logic argument compiles and hands you a problem.
And one smaller trap in the same table, measured in the same run:
NOTE: `long` here is 8 bytes, so it is NOT the type for `int`The ABI is part of the contract. A SystemVerilog int is 32 bits and the C type for it is int; long is 64 bits on this platform and 32 bits on Windows, so writing long is a width bug on one of them. Chapter 28.7 §2 is the full mapping, and the reason it is a table of exact types rather than a rule of thumb.
4. Two Execution Models
The second disagreement, and the source of Chapters 28.4 and 28.11.
SystemVerilog code runs at a simulation time, inside a scope. A statement happens at time 1750 ns, in the initial block of instance tb.agent[1].driver, possibly concurrently with other processes, and it may consume time by waiting.
A C++ function runs now, to completion, in no scope. It cannot wait, it does not know what time it is, and it has no idea which SystemVerilog instance called it.
Three consequences that shape the rest of the module.
A DPI import cannot consume simulation time — unless it is declared as a task rather than a function, and only then may it call back into something that blocks. A function must return without time passing.
A C++ function has no scope of its own, so when it calls back into SystemVerilog the call needs a scope to land in. That is what context is for and what svSetScope establishes; Chapter 28.4 §4 measures the equivalent problem with an explicit context pointer, where the wrong scope silently credits the wrong instance.
And the simulator may legitimately not call your function when you have told it the call is unnecessary. That is the pure qualifier, and Chapter 28.11 §3 measures a false purity claim turning a thousand calls into zero.
5. One Process, and What Follows
Here is the architectural fact that has the most practical consequences, and the one most often skipped.
The C++ is compiled into a shared object which the simulator loads into its own process. There is no second process, no pipe, no serialisation. A DPI call is a function call through a pointer.
Five things follow, and the last three are the ones that bite.
It is fast. The crossing costs a call, not a context switch — which is why Chapter 27.10 §6's rule is about granularity rather than about avoiding the boundary.
State persists naturally. A static or heap object in your C++ lives for the simulation, which is what makes a stateful model possible at all — Chapter 28.13 §2's handle.
Your crash is the simulator's crash. A null dereference in your model does not produce a DPI error; it terminates the simulation, usually with no waveform written and no log flushed. The defensive answer is to validate at the entry points, which is why Chapter 28.13's boundary checks the handle on every call.
Your memory error corrupts the simulator's heap. You and the simulator share one allocator. A buffer overrun in your model can manifest as the simulator misbehaving somewhere unrelated — and Module 25's measurement applies: the symptom appears far from the cause, and ASan on the C++ side is the instrument that finds it.
And your exception is undefined behaviour. An exception thrown out of a DPI function propagates into code that was not compiled to unwind. The rule is absolute: catch everything at the boundary, which is why Chapter 28.13 §2's entry points are wrapped in try/catch(...) returning an error code.
6. What Crosses, and What Cannot
| crosses cleanly | needs care | cannot cross |
|---|---|---|
int, byte, shortint, longint | arrays — packed against unpacked, 28.8 | a SystemVerilog class |
real, shortreal | strings — ownership, 28.10 | a virtual interface |
bit (two-state) | open arrays — API only, 28.9 | an event |
chandle (an opaque pointer) | logic — four-state, §3 | a mailbox or semaphore |
The right-hand column deserves a sentence, because its contents are exactly the things a UVM testbench is built from. A DPI boundary carries data, not objects. You cannot hand your C++ a uvm_sequence_item; you hand it the fields, or a chandle to something your C++ owns. That constraint is why Chapter 28.13's design is a handle plus scalars rather than anything resembling a shared object graph.
7. Where the Code Lives
The build shape, which follows from §5:
Your C++ becomes a shared object. Compiled position-independent, linked as a library, loaded by the simulator — typically through a command-line switch naming the object. The exact switch is simulator-specific, which is one reason Chapter 28.1 §5's advice is to compile a two-line test before planning anything.
The model itself should be a plain library with no interface headers in it. Chapter 27.1 §7's three consumers argument applies unchanged: the model is testable without a simulator only if it does not include the simulator's headers, and here that matters doubly because svdpi.h is absent on this machine — a model that included it could not be compiled here at all, while the models in this module compile and run.
And the boundary file is small and separate. One file containing the extern "C" entry points, which Chapter 28.6 §3 argues should be the only thing with C linkage in the whole build.
8. The Table
| the question | the answer | measured |
|---|---|---|
| how many times is a binding declared | twice — an SV import and a C definition | §2 |
| which parts are machine-checked | only the name, and only as a symbol | §2 |
| how many bit values does each side have | SV four, C++ two | §3 |
| what does 32 bits of 4-state cost | two words, 8 bytes | §3 |
what happens reading aval alone | a definite value, every unknown bit discarded | §3 |
is long the C type for SV int | no — long is 8 bytes here | §3 |
| can a DPI function consume simulation time | no — a task can, a function cannot | §4 |
| does a C function know its caller's scope | no — that is what context is for | §4 |
| where does the C++ run | in the simulator's process, as a shared object | §5 |
| what happens when it crashes | the simulation dies, usually with no waveform | §5 |
| what happens if an exception escapes | undefined — catch everything at the boundary | §5 |
| why is the contract unchecked | C linkage discards the type information on purpose | §5 callout |
| where should the evidence go instead | a build-time checker, one generated source, a known-value call | §5 callout |
| what cannot cross | classes, virtual interfaces, events, mailboxes | §6 |
9. What to Actually Do
- Treat the boundary as a contract declared twice and checked nowhere, and supply the evidence yourself.
- Prefer generating both sides from one description over checking two hand-written ones.
- Assert a known value through the boundary at the start of every simulation. One call, every class of mismatch.
- Keep four-state types out of the interface. Pass
int,byte,bit; resolve X in SystemVerilog before the call. - Write the exact C type from the mapping table, never
longand never a typedef whose width you have not checked. - Remember you are in the simulator's process: validate at the entry points, and never let an exception escape.
- Run the model's own tests under ASan, because a heap error here corrupts the simulator rather than your program.
- Keep the model free of interface headers so it compiles and tests without a simulator.
- Put every
extern "C"entry point in one small file. - Pass data, not objects. A
chandleto something your C++ owns is the way to carry structure across.
10. Mistakes Worth Making Once
Assuming the simulator checks your C signature. Only the name is checked, and only as a string.
Declaring a logic argument. It compiles, and an X arrives as a definite number.
Using long for a SystemVerilog int. Correct on one platform, a width bug on another.
Expecting a DPI function to wait. A function cannot consume time; only a task can.
Calling back into SystemVerilog without establishing a scope. It lands in the wrong instance, or nowhere.
Letting an exception escape the boundary. Undefined behaviour, in the simulator's process.
Dereferencing a null handle in the model. The simulation dies with no waveform.
Putting a buffer overrun in a model and debugging the simulator. You share its heap.
Including the simulator's DPI header in the model itself. Now it cannot be unit-tested — and on this machine it cannot even be compiled.
Trying to pass a class or a virtual interface. DPI carries data.
11. Interview Relevance
The frozen curriculum marks this Critical, and the question is usually how does DPI work? — which invites a syntax answer. The architectural answer is better and shorter.
Lead with one process. The C code is compiled into a shared object the simulator loads, so a call is a function call rather than any kind of inter-process communication. Then the three consequences, which are what the interviewer is actually probing for: state persists naturally between calls, a crash in your C code kills the simulation rather than producing a DPI error, and an exception escaping the boundary is undefined behaviour — so every entry point validates its inputs and catches everything.
Then the two disagreements. SystemVerilog has four-state logic, arbitrary widths, packed and unpacked arrays and a notion of time; C++ has two-state fixed-width integers, one array layout and no time. The concrete detail worth quoting is that 32 bits of four-state needs two 32-bit words — a value word and an unknown word — so reading only the value word hands you a definite number with every X silently discarded. Which is why you keep logic out of the interface and resolve X on the SystemVerilog side.
Then the point that separates a strong answer: the contract is declared twice and checked nowhere. The name is resolved by the linker as a string; arity, types and the pure/context qualifier are not checked at all, because C linkage deliberately discards the type information C++ mangling would have carried. And then say what you do about it — generate both sides from one description if you can, add a build-time cross-check if you cannot, and assert a known value through the boundary on every run.
12. What You Should Be Able to Say Now
- That a binding is declared twice — a SystemVerilog
importand a C definition — and that only the name is machine-checked, as a symbol. - That the two type systems disagree on bit values (four against two), widths, array kinds, array bounds, strings and structs.
- That 32 bits of four-state logic needs two 32-bit words, and that reading the value word alone yields a definite number and discards every unknown bit — so
logicdoes not belong in the interface. - That the IEEE 1800
aval/bvalencoding is 00 = 0, 10 = 1, 01 = Z, 11 = X, quoted from the standard rather than measured here. - That
longis not the C type for a SystemVerilogint— it is 8 bytes on this platform — so the mapping table is exact types, not rules of thumb. - That a DPI function cannot consume simulation time and a task can, and that a C function has no scope, which is what
contextexists for. - That the C++ runs inside the simulator's process as a shared object: calls are cheap, state persists, a crash kills the simulation with no waveform, a heap error corrupts the simulator, and an escaping exception is undefined behaviour.
- That the contract is unchecked because C linkage deliberately discards type information, which is the same trade Chapters 24.1 and 24.5 measured.
- That the three places to put the missing evidence are a build-time checker, one generated source, and a known-value call at the start of every run.
- That DPI carries data, not objects — no classes, virtual interfaces, events or mailboxes — which is why a
chandleis how structure crosses.
13. Exercises
Exercise 1 — the two declarations. Write an import and its C definition, then list which parts of the pair any tool in your flow would check.
Exercise 2 — the four-state cost. Build the two-word representation and confirm that reading the value word alone loses the unknown bits. Count them with a popcount.
Exercise 3 — the ABI trap. Print sizeof(long) on two platforms you have access to, and decide what that means for a declaration using it.
Exercise 4 — the escaping exception. Write a C-linkage function that throws, call it from C, and record what happens. Then wrap it in try/catch(...).
Exercise 5 — the known-value call. Design the one-call self-test you would assert at the start of every simulation, and say which mismatch classes it catches that a static checker cannot.
Exercise 6 — one source. Take an import declaration and write the generator that emits both it and the C++ prototype from one description.
14. Summary
- DPI is a boundary between two systems that disagree about what a value is and when code runs, with a contract across it that is declared twice and enforced nowhere.
- Only the name is machine-checked, and only as a symbol; arity, argument types and the
pure/contextqualifier are not checked at all. - SystemVerilog has four bit values and C++ has two. Measured: 32 bits of four-state needs two 32-bit words, and reading the value word alone returns a definite number while discarding every unknown bit — two of thirty-two in the transcript. Keep
logicout of the interface. - The ABI is part of the contract:
longis 8 bytes here, so it is not the C type for a SystemVerilogint. - A DPI function cannot consume simulation time, and a C function has no scope of its own — hence tasks, and hence
context. - The C++ runs in the simulator's own process as a loaded shared object. Calls are cheap and state persists; a crash kills the simulation with no waveform, a heap error corrupts the simulator's own allocator, and an exception escaping the boundary is undefined behaviour.
- The contract is unchecked by design, because C linkage discards exactly the type information that C++ mangling would have carried — the same trade Chapter 24.5 measured for a drifted signature.
- So supply the evidence yourself, in one of three places: a build-time cross-checker, a single generated source for both declarations, or a known-value call asserted at the start of every run. Writing the declaration carefully is not evidence; a test that would fail if it were wrong is.
- DPI carries data, not objects — no classes, virtual interfaces, events or mailboxes — which is why structure crosses as a
chandleto something the C++ side owns.
Chapter 28.3 builds one complete import end to end, with the C++ half executed and the declaration marked as the reference syntax it is.
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.
