Skip to content
VLSI Mentor

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.

A loop showing the two crossing directions, read top to bottom. First a block labelled SystemVerilog with the caption four-state and timed. Below it a block labelled import with the caption SystemVerilog calls C. Below that a block labelled C or C plus plus with the caption two-state and untimed. Below that a block labelled export with the caption C calls SystemVerilog, which returns to SystemVerilog at the top. An arrow runs from SystemVerilog down through import to the C block, and a second arrow runs from the C block down to export and back to SystemVerilog, so the two crossings form a loop. The C block and both crossing blocks are highlighted, because they are what this module is about. All four blocks sit inside one operating system process.SystemVerilog4-state, timedimportSV calls CexportC calls SVC / C++2-state, untimed12
Figure 1 — the boundary as a loop rather than a window. SystemVerilog is four-state and timed, C and C++ are two-state and untimed, and both crossing directions are declared on the SystemVerilog side: an import brings a C function in, an export makes a SystemVerilog function callable out. Everything shown sits inside one operating-system process.

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SystemVerilog: this is a DECLARATION. Reference syntax -- not executed here.
import "DPI-C" function int crc32(input int data);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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:

piecewho states itchecked?
the nameboth sidesyes — by the linker, as a symbol
the arityboth sidesno
each argument's typeboth sidesno
the qualifier (pure, context)SystemVerilog onlyno — 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.

SystemVerilogC++
bit valuesfour: 0, 1, X, Ztwo: 0, 1
integer widthsarbitrary — bit [11:0]fixed — 8, 16, 32, 64
arrayspacked and unpacked, different thingsone kind, contiguous
array boundsany range, ascending or descendingalways 0-based ascending
stringsa dynamic first-class typea pointer and a convention
structspacked or unpackedlayout chosen by the ABI

The four-state problem is the one worth measuring, because it is the one that silently loses information:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 bit

Thirty-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 bitbval bitvalue
000
101
01Z
11X

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 cleanlyneeds carecannot cross
int, byte, shortint, longintarrays — packed against unpacked, 28.8a SystemVerilog class
real, shortrealstrings — ownership, 28.10a virtual interface
bit (two-state)open arrays — API only, 28.9an event
chandle (an opaque pointer)logic — four-state, §3a 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 questionthe answermeasured
how many times is a binding declaredtwice — an SV import and a C definition§2
which parts are machine-checkedonly the name, and only as a symbol§2
how many bit values does each side haveSV four, C++ two§3
what does 32 bits of 4-state costtwo words, 8 bytes§3
what happens reading aval alonea definite value, every unknown bit discarded§3
is long the C type for SV intno — long is 8 bytes here§3
can a DPI function consume simulation timeno — a task can, a function cannot§4
does a C function know its caller's scopeno — that is what context is for§4
where does the C++ runin the simulator's process, as a shared object§5
what happens when it crashesthe simulation dies, usually with no waveform§5
what happens if an exception escapesundefined — catch everything at the boundary§5
why is the contract uncheckedC linkage discards the type information on purpose§5 callout
where should the evidence go insteada build-time checker, one generated source, a known-value call§5 callout
what cannot crossclasses, 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 long and 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 chandle to 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 import and 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 logic does not belong in the interface.
  • That the IEEE 1800 aval/bval encoding is 00 = 0, 10 = 1, 01 = Z, 11 = X, quoted from the standard rather than measured here.
  • That long is not the C type for a SystemVerilog int — 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 context exists 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 chandle is 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/context qualifier 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 logic out of the interface.
  • The ABI is part of the contract: long is 8 bytes here, so it is not the C type for a SystemVerilog int.
  • 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 chandle to 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.