Contents

DPI-C from scratch · Reference

What doesn't work (or works differently) in Verilator 5.052

Every DPI-C behavior in this tutorial where Verilator 5.052 departs from the LRM, from commercial simulators, or from what the book assumes, in one place. Nothing here is from memory: each row links to the step and the lab file that measures it (check.py asserts it, or NOTES.md records the measurement).

52 items21 silent · 3 run-time · 16 build · 12 differentVerilator 5.052, macOS x86_64

The 10 that will bite you

Silent wrong results first: a build error costs you a minute, a silent one costs you a tape-out review.

  1. Outputs C never writes come back as garbage — silent wrong result.
  2. Missing context can call the wrong instance — silent wrong result.
  3. shortreal is passed as double — silent wrong result.
  4. X and Z arrive in C as 0 — silent wrong result.
  5. --threads: DPI runs on other OS threads — silent wrong result.
  6. Without the generated header, a parameter mismatch links silently — silent wrong result.
  7. pure is never exploited — silent wrong result.
  8. $error aborts at the first error, and final blocks don't run — aborts / crashes at run time.
  9. Dynamic arrays and queues can't be open-array actuals — breaks the build.
  10. No timing controls in exported tasks — breaks the build.

Three habits remove most of this page: include Vtop__Dpi.h in every C file, write every output on every path, and run with +verilator+error+limit+N. The rest are portability notes: code that passes here can still break on Questa, VCS or Xcelium, mostly because Verilator is 2-state and more forgiving with illegal code.

How to read the tables

SeverityMeaningCount
silent wrong resultBuilds and runs; a value, a call count or an instance is wrong, and nothing tells you.21
aborts / crashes at run timeSegfault, abort, or a skipped final.3
breaks the buildVerilator, the C++ compiler or the linker stops you. Annoying, but safe.16
just differentWorks, but not the way the LRM, the book or another simulator does.12
KindMeaningCount
LRM violationVerilator accepts code that IEEE 1800-2023 forbids. It will not port.5
LRM deviationLegal code, but Verilator's result or generated header differs from what IEEE 1800-2023 specifies.3
unsupported featureLegal SV that Verilator 5.052 rejects.6
implementation-definedThe LRM leaves it open; don't depend on either answer.26
undefined behaviorYour code breaks an LRM rule whose effect is undefined; any outcome is "correct", including the quiet one.1
bugWrong per its own spec, or a tool crash.3
platform / toolchainmacOS, clang or CPython, not Verilator itself.5
language gotchaPlain SystemVerilog, met while writing the labs.3

"Proof" links go to the step guide (number) and to the lab file. Run cd NN_*/lab && python3 check.py to reproduce.

Build and link (8)

ItemExpected (LRM / commercial)Verilator 5.052WorkaroundProof
.c files are compiled as C++
breaks the build · implementation-defined
Commercial flows compile .c as C; a plain C definition links.Every user file goes through $(CXX) (clang: treating 'c' input as 'c++'). Without C linkage the symbol is mangled and the link fails: Undefined symbols for architecture x86_64: "_factorial". C idioms that are invalid C++ (implicit void* from malloc) fail too.Wrap definitions in extern "C" { }, or #include "Vtop__Dpi.h" first (its prototypes give the definitions C linkage). Write C that also compiles as C++.01 · noextern.c
03 · nocxx.cpp
Without the generated header, a parameter mismatch links silently
silent wrong result · implementation-defined
Nothing in the LRM checks the C side; any simulator behaves the same.C linkage carries no parameter types, so the book's counter7(c7* inst, …) links against the SV chandle import and runs. With the header included, a return-type or parameter mismatch inside extern "C" is a compile error (conflicting types); a parameter mismatch defined outside extern "C" becomes a C++ overload and fails only at link (Undefined symbols "_negate").Always #include "Vtop__Dpi.h" (it is in the -Mdir, found with no extra flag) and copy prototypes from it.01 · mismatch_hdr2.c
03 · book_sig.c
import "DPI" is rejected
breaks the build · implementation-defined
"DPI" selects the deprecated SV3.1a packed-argument passing (opaque handles, H.14), not just an old spelling. §35.5.4: the tool shall give a warning or an error; Verilator's error is compliant.%Error-UNSUPPORTED: Unsupported DPI type 'DPI': Use 'DPI-C'.Write "DPI-C".01 · legacy.sv
Importing strlen (or anything returning size_t) directly fails
breaks the build · implementation-defined
longint unsigned maps to a 64-bit unsigned C type.Verilator emits unsigned long long strlen(const char*), which clashes with size_t strlen from <string.h> (included by the generated code). No SV type maps to unsigned long. libm sin/pow/fabs/log10 and libc access import fine.Write a one-line C wrapper with your own name.01 · ex/ex5_strlen.sv
Same C name, two signatures
breaks the build · implementation-defined
Illegal everywhere (the LRM requires one signature per C name).Duplicate declaration of DPI function with different signature, followed by an internal error.One import per C name, in one package.01 · dup.sv
A comment starting with Verilator: is a pragma
breaks the build · implementation-defined
A comment is a comment.%Error-BADVLTPRAGMA: Unknown verilator comment (any case of the first word).Don't start comments with the tool's name.07
08
Warnings disappear on an up-to-date build
just different · implementation-defined
—Verilator skips verilation (and its warnings) when the -Mdir is current. C compiler warnings (-Wreturn-stack-address) appear only in the build log, replayed by ccache on hits.Clean build in CI scripts that grep warnings; read the build log, not just the run log.02 · run.sh
04
macOS: the book's <wait.h> and WEXITSTATUS(system(cmd))
breaks the build · platform / toolchain
Sample 12.52 compiles on Linux.<wait.h> does not exist; WEXITSTATUS on an rvalue does not compile as C++ (cannot take the address of an rvalue).#include <sys/wait.h>; store system()'s result in a variable first.07 · ex/spawn.c

Types and directions (5)

ItemExpected (LRM / commercial)Verilator 5.052WorkaroundProof
shortreal is passed as double
silent wrong result · implementation-defined
Table H.1 (H.7.4): shortreal ↔ float.Warning SHORTREAL; Vtop__Dpi.h declares double. A C definition with float links silently and reads garbage (0.1 arrives as -1.59e-23).Use real/double. If you must use shortreal, include the header: the mismatch becomes a compile error.02 · types.c
ref arguments on imports are accepted
silent wrong result · LRM violation (accepts illegal code)
IEEE 1800 forbids ref in imports; commercial tools reject it.Accepted with no warning even with -Wall, mapped to int*.Use inout (or output). Code that works here will not port.01 · ref.sv
03
Outputs C never writes come back as garbage
silent wrong result · implementation-defined
The value of an unwritten output is undetermined (§35.5.1.2, H.6.3); tools differ.The generated wrapper passes an uninitialized stack temporary (svBitVecVal v__Vcvt[2], o__Vcvt) and copies it back unconditionally: the SV variable gets stale stack data (measured: the previous call's 40-bit temp).Write every output on every path, including error paths.02 · dir.c
05 · outarg.c
Unused upper bits and sign extension
just different · implementation-defined
The LRM does not guarantee zeroed unused bits on input, nor masking on output.Inputs arrive with zero unused bits and no sign extension; output copy-back masks to the declared width (VL_SET_Q_SVBV(40, …), scalars 1U & x). Sloppy C works here.Mask on input (SV_MASK) and write only the declared bits, so the code survives Questa/Xcelium.02 · vec.c
SV_GET_SIGNED_BITS tests the wrong bit
silent wrong result · bug in Verilator / svdpi.h
Sign-extend an N-bit value from bit N-1.The macro in vltstd/svdpi.h (Annex I text) tests bit N: SV_GET_SIGNED_BITS(0x80, 8) = 128.Sign-extend by hand: (int32_t)(v << (32-N)) >> (32-N).02 · vec.c

4-state values (3)

ItemExpected (LRM / commercial)Verilator 5.052WorkaroundProof
X and Z arrive in C as 0
silent wrong result · implementation-defined
4-state simulators deliver X/Z in bval; Sample 12.12's checks fire.Verilator is 2-state: an uninitialized reset is seen as 0 (the counter counts), 7'b000_zzxx is loaded as 0. None of the sample's error branches can fire.Don't validate X/Z in C under Verilator; check in SV with $isunknown on a 4-state simulator, or document the limitation.02 · counter4.c
A 1'bz literal as an argument
breaks the build · unsupported feature
Legal; C sees sv_z.%Error-UNSUPPORTED: Unsupported tristate construct: ARG.Pass a variable (which then arrives as 0, see above).02 · zlit.sv
bval written by C is dropped
silent wrong result · implementation-defined
C can return X/Z through svLogicVecVal outputs.Output bval bits are ignored; svGetLogicArrElem*VecVal writes bval = 0.Report errors through a return status, not by driving X.02 · counter4.c
04

Arrays and open arrays (8)

ItemExpected (LRM / commercial)Verilator 5.052WorkaroundProof
Dynamic arrays and queues can't be open-array actuals
breaks the build · unsupported feature
§35.6.1 (with §7.7): any unpacked array (fixed, dynamic, queue) may be passed to input T a[].%Error-UNSUPPORTED: Unsupported: DPI argument of type DYNARRAYDTYPE (or QUEUEDTYPE), sometimes followed by Internal Error: ../V3Task.cpp:759.Copy into a fixed-size buffer and pass the count as an extra int (the contract in golden_pkg.sv).04 · kinds.sv
06 · golden_pkg.sv
08 · tb_dyn_fail.sv
A slice as an open-array actual
breaks the build · unsupported feature
Legal.%Error: Internal: Unexpected Call for a[1:2].Copy the slice into a fixed buffer.04 · kinds.sv
An assignment pattern '{…} as an open-array actual
breaks the build · unsupported feature
Legal.Unsupported: Assignment pattern applies against non-struct/union data type: 'UNSIZEDARRAYDTYPE'.Assign the pattern to a fixed array variable first.08
Element types must match exactly
breaks the build · implementation-defined
Type-equivalent element types are interchangeable.logic signed [15:0] c[8] passed to input shortint x[8]: %Error-UNSUPPORTED: Shape of the argument does not match the shape of the parameter.Declare the actual with the formal's exact type.08 · tb_shape_fail.sv
Packed open-array elements are stored natively
silent wrong result · LRM deviation
Packed elements of an open-array actual are in canonical form (H.7.3); if a tool stores them otherwise, svGetArrElemPtr* shall return NULL (H.12.4).bit[7:0] = 1 byte, bit[12:0] = 2, bit[63:0] = 8, bit[99:0] = 16. Casting a svGetArrElemPtr1 result to svBitVecVal* reads neighbouring elements for widths ≤ 32.Use svGetBitArrElem1VecVal / svPutBitArrElem1VecVal, never a raw pointer cast.04 · packed.c
Unwritten elements of output arrays
silent wrong result · implementation-defined
Undetermined.Open-array output temporaries arrive zero-filled and are copied back whole (unwritten elements become 0). Fixed-size output arrays return garbage in unwritten elements. inout keeps them.Write every element, or use inout.04 · outdir.c
An input array pointer aliases the SV variable
silent wrong result · implementation-defined
Pointers into arguments are valid only during the call.svGetArrayPtr() of an input fixed array points at the SV variable itself: a pointer C/Python keeps sees later SV writes. Output open arrays are a fresh stack temporary on every call.Copy what you need before returning; never keep argument pointers.08
Out-of-range element access
just different · implementation-defined
Undefined.Run-time %Warning: DPI svOpenArrayHandle function … and NULL.Iterate svLow … <= svHigh; check for NULL.04 · openarr.c

Structs and strings (2)

ItemExpected (LRM / commercial)Verilator 5.052WorkaroundProof
Unpacked structs as DPI arguments
breaks the build · unsupported feature
Allowed (C sees a struct with the LRM's layout rules).Unsupported: DPI argument of type REFDTYPE '<name>'.Pass a packed struct (as svBitVecVal*) or separate members.04 · structs.sv
Returned strings are copied immediately
just different · implementation-defined
A returned const char* must stay valid until the next call; tools may read it later.Verilator copies it into a std::string at once: one static buffer survives two calls in one $display.Don't rely on it: return static or caller-owned storage, as H.8.10 requires.04 · strings.c

chandle, threads and ordering (3)

ItemExpected (LRM / commercial)Verilator 5.052WorkaroundProof
chandle behaves like a 64-bit integer
silent wrong result · LRM violation (accepts illegal code)
IEEE 1800-2023 §6.14 forbids arithmetic, continuous assignment and events on chandles.Under verilator --lint-only -Wall: h + 1, assign g = h and @(h) are accepted silently; int x = h only warns (WIDTHTRUNC). Only a chandle inside a packed struct is an error.Treat chandles as opaque: assign, compare to null, pass. Nothing else.03 · neg/
--threads: DPI runs on other OS threads
silent wrong result · implementation-defined
Single-threaded simulators call DPI from one thread.Default --threads-dpi pure: pure imports run on several threads concurrently; non-pure calls are serialized onto one thread that is not the thread that ran initial (3200/3200 calls off the init thread with --threads 4). Embedded CPython without PyGILState_Ensure segfaults.Don't mark stateful C pure; use --threads-dpi none, or attach per call (PyGILState_Ensure/Release).03 · threads.sv
07 · threads.c
Order of same-time calls and final blocks
just different · implementation-defined
Unspecified by the LRM.Two always blocks calling the same import at the same time: the second-declared instance ran first (u2 before u1). final: top's ran before the instances' (u2, then u1).Never depend on it; free shared C state in one place.03 · static_counter.sv

Export, context, scopes and time (8)

ItemExpected (LRM / commercial)Verilator 5.052WorkaroundProof
No timing controls in exported tasks
breaks the build · unsupported feature
Exported tasks may consume time (Sample 12.40).Any #, @, or delay nested in a called task: %Error-UNSUPPORTED: … Unsupported: Timing controls inside DPI-exported tasks.Pull model: SV owns the loop and the waiting; C is a zero-time iterator returning the next command.05 · xtime.sv
05 · mem_pull.c
Missing context can call the wrong instance
silent wrong result · undefined behavior
Calling an export from a non-context import is an error (§35.5.3); its behavior is undefined (H.9.3).Fatal only if no scope was ever set: Testbench C called 'X' but scope wasn't set, preceded by %Warning: DPI C Function called by Verilog DPI import with missing 'context' keyword. If an earlier context call left a scope, the export of that instance runs with no error.Every import that reaches an export, VPI or the scope API is context. No exceptions.05 · noctx.c
An imported function may call an exported task
silent wrong result · LRM violation (accepts illegal code)
Illegal: functions can't call tasks, across DPI too.Accepted.Import as task when the C side calls an exported task.05 · fn2task.sv
Exported tasks return void in the header
just different · LRM deviation
§35.8, H.8.2: an exported task has the same C header as an import task, returning int (the disable flag).Vtop__Dpi.h declares exported tasks void, imported tasks int; svIsDisabledState() links and returns 0.Include the header and never use an exported task's return value.05 · xtask.c
svGetScopeFromName wants the full %m name
just different · implementation-defined
Name format is tool-specific.top.b2, top.gen[0].u work; TOP.top.b2 and relative names return NULL.Get names from SV ($sformatf("%m")) instead of hard-coding them.05 · scopes.c
context costs ~6× a plain call
just different · implementation-defined
—Per call (5 M calls, idle machine): generic ≈ pure ≈ 1.3–1.8 ns, context ≈ 10 ns, context + 1 export ≈ 18–21 ns.Irrelevant unless you call per cycle in a hot loop; batch if so.05 · bench.c
svGetTime ignores the time type and the scope
silent wrong result · LRM deviation
H.13: vpiScaledRealTime returns real time in the scope's unit; svGetTimeUnit returns that scope's unit.Always simulation time in precision units in high/low; t.real is never written (garbage if uninitialized); a module with timeunit 1us reports −9.Use vpiSimTime plus svGetTimePrecision(NULL, …) and scale in C.05 · svtime.c
A dynamic-array formal on an export crashes Verilator
breaks the build · bug in Verilator / svdpi.h
Erroneous (§35.5.6; exports can't have open arrays either, §35.5.6.1): a compile error.%Error: Verilator internal fault, sorry. (no message pointing at the export).Exports take fixed-size arrays or scalars; pass sizes explicitly.05 · xdyn.sv

pure and combinational calls (3)

ItemExpected (LRM / commercial)Verilator 5.052WorkaroundProof
pure is never exploited
silent wrong result · implementation-defined
The LRM lets a tool drop or merge calls to a pure function.Every call executes: discarded results, duplicates (f(2)+f(2)), loop-invariant calls, also with -O3. A pure function with hidden state ran all 13 calls. A lying pure goes unnoticed until another tool uses its license.Mark pure only what really is (CRC, factorial). Never stateful models.01 · pure.sv
03 · pure_lie.sv
05 · pure.sv
Call count in always_comb / assign
just different · implementation-defined
Evaluation count is implementation-defined; event-driven tools may evaluate more often.Measured one call per input change per call site (2 sites × 10 changes → 2 + 20 calls). A DPI function in a continuous assign ran 11 times for 5 input changes in step 05's lab.Keep stateful models out of combinational code.01 · comb.sv
05 · comb.sv
pure is accepted where the LRM forbids it
silent wrong result · LRM violation (accepts illegal code)
Only nonvoid functions without output/inout can be pure; a task never (§35.5.1.3, §35.5.2).pure function void, pure task and a pure function with an output all build and run, no warning.Use pure only on side-effect-free functions that return a value.01 · pure_rules.sv

Embedded Python (4)

ItemExpected (LRM / commercial)Verilator 5.052WorkaroundProof
python3-config --ldflags is not enough
breaks the build · platform / toolchain
—It lacks -lpython3.14: the link fails without --embed.python3-config --embed --ldflags (the py_flags helper in common/lab.sh).07 · run.sh
numpy can be imported once per process
aborts / crashes at run time · platform / toolchain
—Py_FinalizeEx + re-init + import numpy: cannot load module more than once per process. A first import costs ~0.1 s.Initialize once, finalize once (at exit).07 · lifecycle.c
Any CPython call before init segfaults
aborts / crashes at run time · platform / toolchain
—E.g. PyLong_FromNativeBytes before Py_Initialize: segfault on the first DPI call.Route every bridge entry point through the function that initializes.08 · golden_py.c
Python and C stdout interleave wrongly
just different · platform / toolchain
—Without flushing, Python's text appears at finalize; flushing only one side puts the other side's text last.Flush both sides (or buffered_stdio=0 + fflush).07 · buffering.c

Testbench-level gotchas (8)

ItemExpected (LRM / commercial)Verilator 5.052WorkaroundProof
$error aborts at the first error, and final blocks don't run
aborts / crashes at run time · implementation-defined
$error reports and continues.Default error limit is 1: $error becomes $stop ("Aborting…", exit 1). final blocks do not run, so a Py_FinalizeEx or a free in final is skipped.Run with +verilator+error+limit+N; count mismatches in the scoreboard and report once.05
07 · errors.sv
08 · tb_err.sv
$finish doesn't stop the calling process
just different · implementation-defined
—Statements after $finish in the same initial still execute, then final blocks run.Put $finish last.07
A sized negative literal in dist is unsatisfiable
silent wrong result · bug in Verilator / svdpi.h
x dist {-16'sh8000 := 5} on a shortint weights -32768.%Warning-UNSATCONSTR and randomize() returns 0.Write -32768; always check randomize()'s result.06
randomize() calls an external SMT solver
just different · implementation-defined
—~5–10 ms per call (z3), CPU mostly idle (cpu 0.68 s vs walltime 12.9 s for 2500 calls); a DPI call into C costs 2–10 ns.Use $urandom stimulus when you measure anything else.06 · fir_tb.sv
Static initializer inside a loop of an initial
silent wrong result · language gotcha
—int len = $urandom_range(…) declared in a loop body runs once at time 0 (always 0); no warning. The scoreboard tested 0 bytes and still passed.Declare it automatic, or assign separately.08 · tb_crc.sv
Unpacked port element types are checked inconsistently
silent wrong result · LRM violation (accepts illegal code)
Non-equivalent element types are illegal on unpacked array ports.A whole shortint c[8] on a logic signed [15:0] coef[8] port fails (Array element types are not equivalent), but a sub-array .coef(c[u]) of shortint c[2][8] builds and simulates.Declare stimulus arrays with the port's exact type.08 · tb_port_fail.sv
$display(c ? "PASS" : "FAIL") prints a number
silent wrong result · language gotcha
—The ternary yields an integral, not a format string.$display("%s", c ? "PASS" : "FAIL").01
02
buf is a keyword
breaks the build · language gotcha
—int buf[256]; is a syntax error (gate primitive).Pick another name.04

Spear & Tumbush ch. 12: book errata

Samples that are wrong, or that don't work as printed in a 2026 flow (C compiled as C++, Verilator, macOS). Details and the corrected code are in the linked step.

SampleProblemFixStep
12.3The \expect backslash is essential (without it: syntax error, unexpected expect). The stat = function int file_exists(...) example can't work: stat takes a struct stat*.Escape the keyword; import access(path, F_OK) instead.01
Table 12.1longint outputs as long int*.long long* (32-bit long on Windows loses the upper half).02
12.7No extern "C".Fatal in any flow that compiles the file as C++ (Verilator).03
12.9io_printf/veriuser.h; handle declared c7* instead of void*; no null guard.printf + fflush; void* (the header then agrees); check for NULL.03
12.12if (i->bval) tests all 32 bits (25 undetermined); every error branch returns without writing count.Mask with SV_MASK(7); drive X explicitly: count->aval = count->bval = SV_MASK(7).02
12.22Calls fib_oa(data, SIZE) with two arguments; the import takes one.Drop the size: it travels inside the open-array handle.04
12.23Loops i <= svSize(h,1): writes one past the end.i < svSize(h,1), or svLow … <= svHigh.04
12.27Loops i < svHigh(h,1): skips the last element.i <= svHigh(h,1).04
12.37 / 12.41 / 12.45while (!feof(file)), unchecked fopen.while (fscanf(f, " %c", &cmd) == 1); check fopen.05
12.40 / §12.8.3Timed exported tasks: legal SV, but don't build on Verilator.Pull model (SV waits, C iterates).05
12.41Exported tasks declared extern int (per LRM; footnote: VCS used void).Verilator also uses void: include the header, ignore the return.05
12.43R1 22 44 reads a location no command wrote.A guaranteed mismatch; the lab keeps it as a negative test.05
§12.8.4 / 12.44Text says mem_build returns the index; the sample declares it void and C trusts the file.Return the index and check it in C.05
§12.8.6"…use that handle in a call to svGetScope" means svSetScope; the previous scope is never restored.prev = svSetScope(s); …; svSetScope(prev);05
12.52<wait.h>; WEXITSTATUS(system(cmd)).<sys/wait.h>; store the status in a variable first.07

Not an erratum: §12.1.4's "a function cannot return bit [6:0]" is correct per §35.5.5 and H.8.9; Verilator accepts packed bit results up to 32 bits as an extension (02).

Errata in the LRM itself (IEEE 1800-2023)

The standard's normative text wins over its examples. These are the places where the two disagree, or where the normative header has a bug.

WhereProblemWhat is rightStep
§35.5.4 exampleimport "DPI-C" function bit [15:0] getStimulus(); returns a packed vector.§35.5.5 and H.8.9 allow only byte…string and scalar bit/logic results. Use an output or shortint unsigned.02
H.9.4 Example 1Calls svGetScopeByName (no such function) and writes svScope = svGetScope();, assigning to a type name.svScope s = svGetScopeFromName(path); / svScope s = svGetScope();05
H.10.2 Example 2o3[1].b = 0;o3[1].bval = 0; (s_vpi_vecval has aval/bval).02
H.10.1.2 / Annex ISV_GET_SIGNED_BITS(VALUE, N) tests bit N, not the sign bit N−1: SV_GET_SIGNED_BITS(0x80, 8) = 128. Every tool copies this header.Sign-extend yourself: (v & (1u << (N-1))) ? (v | ~SV_MASK(N)) : (v & SV_MASK(N)).02