DPI-C from scratch · Part A · Step 02
Data types: what C actually receives
Every DPI argument has two declarations, one in SystemVerilog and one in C, and no tool compares them for you unless you ask. This step maps every type that can cross the boundary, measures what Verilator really passes, and shows where the bits you did not think about end up: unused upper bits, sign bits, X and Z, and outputs nobody wrote.
§0 · TutorialThe step in one idea
The simulator and the C compiler each lay out data their own way, and DPI is the contract that says how a value of one is presented to the other. For small types the contract is plain C: an int is an int. For everything that has no C equivalent (vectors of arbitrary width, 4-state values, strings owned by the simulator) the contract is a canonical representation defined in IEEE 1800 Annex H and the header svdpi.h (Annex I). Learn the canonical forms once and you can pass anything a hardware testbench needs.
Three questions run through the whole step. What bits does C see? (layout, word order, unused bits, signedness). What bits does SV get back? (masking, 4-state planes, outputs C never wrote). Who guarantees it? The LRM, or just the simulator you happen to run. The difference matters: a model that works on Verilator because Verilator zeroes and masks unused bits can break on a commercial simulator that does neither, and the LRM allows both behaviors.
Every number below comes from the lab: one testbench per topic, a check.py with 72 assertions. Where the book (2012, IEEE 1800-2009, VCS-era) is wrong or dated, a Book erratum callout says so. Step 01 covered the build, linkage and the generated Vtop__Dpi.h header; this step assumes them.
0.1Three ways across the boundary
Annex H sorts every argument into one of three passing modes, and once you know the mode the C type follows mechanically (Fig. 0.1).
- By value: an
inputof a "small" type (the C-compatible ones:byte,shortint,int,longint, theirunsignedforms,real,shortreal,chandle,string, andbit/logicscalars). C gets a copy in a register or on the stack. - By const pointer: an
inputof a packed vector (anybit [N:0]orlogic [N:0], includinginteger,timeand evenlogic [0:0]). C getsconst svBitVecVal*orconst svLogicVecVal*pointing at an array of 32-bit chunks. - By pointer: every
outputandinout, of any type. C gets a pointer to a temporary, writes through it, and the simulator copies the temporary back into the SV variable after C returns.
int is int as an input and int* as an output; bit [39:0] is always a pointer. Section 0.9 opens the "temp" box.The full table, with what Verilator 5.052 generates in Vtop__Dpi.h (every row is in the lab's types or proto experiments):
| SystemVerilog | C, input | C, output/inout | Notes (measured) |
|---|---|---|---|
| byte | char | char* | plain char: signedness is the C compiler's choice (0.2) |
| byte unsigned | unsigned char | unsigned char* | |
| shortint [unsigned] | [unsigned] short | [unsigned] short* | |
| int [unsigned] | [unsigned] int | [unsigned] int* | |
| longint [unsigned] | [unsigned] long long | [unsigned] long long* | book says long int*: wrong on Windows |
| real | double | double* | |
| shortreal | float (LRM) | float* | Verilator: double (0.3) |
| bit | svBit | svBit* | uint8_t; output masked to bit 0 |
| logic, reg | svLogic | svLogic* | uint8_t, encoding 0/1/2(Z)/3(X) |
| bit [N:0] | const svBitVecVal* | svBitVecVal* | also bit [0:0] |
| logic [N:0], reg [N:0], integer, time | const svLogicVecVal* | svLogicVecVal* | also logic [0:0] |
| string | const char* | const char** | Verilator: const char** for output too |
| chandle | void* | void** | book says const void* for input |
| enum | same as its base type | enum byte → char | |
open array [] | const svOpenArrayHandle | svOpenArrayHandle | step 04 |
Table 12.1 of the book maps longint outputs to long int*. The LRM says long long. On LP64 Unix both are 64 bits, so the bug is invisible on Linux and macOS; on Windows (LLP64) long is 32 bits and the upper half of every longint output is lost. The table also omits the unsigned variants and integer/time, which are 4-state packed arrays and travel as svLogicVecVal*, not as C integers.
Do not write this table from memory. Let the simulator write it: Verilator's Vtop__Dpi.h, VCS's vc_hdrs.h, Questa's -dpiheader file and Xcelium's -dpiheader option all emit the exact prototypes. #include that header in your C file and the C++ compiler checks every definition against the SV declaration (step 01, 0.6). Section 0.3 shows that check catching a real bug.
0.2The integer types and the char trap
The C-compatible integer types are the boring, safe part: the round trips in the lab hit every extreme value and come back intact.
LOG sizeof char=1 short=2 int=4 longlong=8 float=4 double=8 svBit=1 svLogic=1 svBitVecVal=4 svLogicVecVal=8 ptr=8 char_signed=1
LOG byte min=-128 max=127
LOG short min=-32768 max=32767
LOG int min=-2147483648 max=2147483647
LOG long min=-9223372036854775808 max=9223372036854775807
LOG ulong max=18446744073709551615
The one trap is byte. The LRM maps it to plain char, and C leaves the signedness of plain char to the implementation: signed on x86 and on Apple ARM, unsigned on ARM Linux (and on PowerPC). The lab rebuilds the same code with -funsigned-char to emulate an ARM Linux host:
default (x86): LOG c_byte v=-128 LOG c_enum v=-2
-funsigned-char: LOG c_byte v=128 LOG c_enum v=254
both: LOG byte min=-128 max=127 LOG enum v=BUSY
Read the last line carefully: the round trip still works, because the bit pattern 0x80 goes to C and comes back unchanged. What breaks is anything C computes with the value: a comparison b < 0, a widening to int, an index. That is the worst kind of bug, one that passes a loopback test and fails in the model (Exercise 9).
/* byte arrives as plain char: never compute with it directly */
int c_isneg(char b) { return (signed char)b < 0; } /* or: int v = (signed char)b; */
Enumerations cross as their base type (typedef enum byte {...} → char), so C sees the encoding, not the name. If the C model needs the names, mirror the enum in a C header and keep the two in sync, or pass an int and convert at the boundary.
0.3real and shortreal
real is an IEEE-754 double on both sides and needs no comment: rt_real(0.1) returns 0.10000000000000001, the nearest double to 0.1. shortreal is supposed to be a C float. Verilator does not implement single precision at all: it warns SHORTREAL: Unsupported: shortreal being promoted to real and generates double in the prototype. A C file written to the LRM (float rt_sreal(float v)) still links, because the symbol name is all the linker checks, and then reads the wrong register:
C declared float : LOG c_sreal v=-1.5881868392106856e-23 LOG sreal v=1.2731974747767896e-314
C declared double: LOG c_sreal v=0.10000000000000001 LOG sreal v=0.10000000000000001
On x86-64 both float and double arguments travel in xmm0; the callee reads the low 32 bits of a double as a float and gets noise, and the caller reads a float's bits as a double and gets a denormal. Nothing crashes. Including the generated header turns this into a compile error (lab build types_hdr):
../../types.c:37:9: error: functions that differ only in their return type cannot be overloaded
Newer clang (16+) words the same error as error: conflicting types for 'rt_sreal'.
Note also the second line of the "double" run: the value is exactly the double 0.1, not the float 0.1 (0.100000001490116…). On Verilator a shortreal model never sees single-precision rounding, so a bit-exact float golden model cannot be checked through shortreal here.
Portable rule: do not use shortreal in DPI signatures. If the model is single precision, pass real (double) or the raw 32-bit pattern as int unsigned and convert with memcpy in C and $bitstoshortreal in SV.
0.4bit and logic scalars
A single-bit argument travels as a byte: svBit and svLogic are both uint8_t in svdpi.h. A 2-state bit uses values 0 and 1. A 4-state logic packs the two planes described in 0.8 into bits 0 and 1 of the byte, which gives the four constants of svdpi.h (Fig. 0.2).
bval set means "not a plain 0/1": Z when aval is 0, X when it is 1.Two measurements matter here. First, Verilator is a 2-state simulator: a logic that holds X or Z in the source is stored as 0, so C receives sv_0 for X, Z and 0 alike (LOG logic v0=0 v1=1 vx=0 vz=0). A literal Z argument does not even build: rt_logic(1'bz) stops Verilator with Unsupported tristate construct: ARG; the lab keeps the X and Z in variables to get past that. Second, C can write any byte into a scalar output. The book warns that this can corrupt the SV variable. Verilator masks it: the wrapper does b = (1U & b__Vcvt), so C writing 0xFE gives SV a clean 0 (LOG badbit b=0 l=0). The LRM does not require the mask, so write only sv_0…sv_x.
0.5Packed vectors: the canonical representation
A packed bit vector of any width crosses as an array of 32-bit chunks, svBitVecVal (= uint32_t). The layout is fixed by the LRM (H.7.7), independent of the simulator's internal storage:
- Chunk 0 holds bits
[31:0], chunk 1 bits[63:32], and so on: little-endian by word, whatever the declared range ([0:39]or[39:0], the LSB of the value is bit 0 of chunk 0). - The number of chunks is
SV_PACKED_DATA_NELEMS(W)= $\lceil W/32 \rceil$. - The last chunk uses only $W \bmod 32$ bits (when that is not 0). The LRM says the rest are undetermined, on input and on output.
LOG v40 w0=3456789a w1=00000012. A 200-bit value takes 7 chunks with 8 used bits in the last one: w6=000000c7 for a value whose top byte is C7.The C side always works with three numbers: the width $W$, the chunk count $N = \lceil W/32 \rceil$, and the used bits of the last chunk $W \bmod 32$. svdpi.h gives you SV_PACKED_DATA_NELEMS(W) and SV_MASK(n) ($2^n-1$ for $n < 32$):
/* 200-bit add (lab vec.c): ripple the carry through 7 chunks, then clean the top chunk */
void c_add200(const svBitVecVal* a, const svBitVecVal* b, svBitVecVal* sum) {
unsigned long long c = 0;
for (int k = 0; k < 7; k++) { c += (unsigned long long)a[k] + b[k]; sum[k] = (svBitVecVal)c; c >>= 32; }
sum[6] &= SV_MASK(200 - 6 * 32); /* 8 used bits: never leave junk above them */
}
What about the unused bits? The book (§12.2.3) warns that "the simulator may not mask the upper bits after calling a DPI routine", so junk that C leaves there can corrupt the SV variable. The lab asks Verilator directly: C writes 0xFFFFFFFF into both chunks of a bit [39:0] output.
LOG dirty d=ffffffffff eq=1 plus1=0000000000
Verilator masks: the variable is the 40-bit all-ones value, it compares equal to 40'hFF_FFFF_FFFF, and adding 1 wraps to 0. The generated wrapper copies back with VL_SET_Q_SVBV(40, v, v__Vcvt + 0), which keeps exactly 40 bits. Commercial simulators that pass a pointer straight into their own storage (to avoid the copy) may not.
Rule of thumb for the unused bits: on input, mask them before you use them; on output, write them as zero. Both are one line (& SV_MASK(W % 32)), both are free, and together they make the code correct under every simulator the LRM allows. Verilator's zeroing and masking hide the bug, which is exactly why you should not test portability on Verilator alone.
Which representation is this? svDpiVersion() (H.10.1.3) returns a string naming the DPI C layer the simulator implements: "1800-2005" means the canonical svBitVecVal/svLogicVecVal chunks described here; "SV3.1a" would mean the older opaque-handle representation of H.14 that the deprecated "DPI" string still selects. Verilator 5.052 returns 1800-2005 (lab: ver.sv). A C library shipped to several teams can check it once at start-up instead of trusting the build.
0.6Signed vectors and sign extension
Declaring a vector signed changes how SV interprets it, not how DPI lays it out. The canonical representation carries the $W$ bits and nothing else; the sign bit is just bit $W-1$, sitting in the middle of the last chunk. Nobody extends it into the unused bits for you:
c_dump_s40(-40'sd2) → LOG s40 w0=fffffffe w1=000000ff
Bits [63:40] of chunk 1 are zero, so (long long)v[1] << 32 | v[0] gives +1 099 511 627 774, not −2 (Exercise 6). The correct conversion masks to $W$ bits, then moves bit $W-1$ to bit 63 and shifts back arithmetically:
long long c_s40(const svBitVecVal* v) { /* bit signed [39:0] → long long */
unsigned long long u = ((unsigned long long)(v[1] & SV_MASK(8)) << 32) | v[0];
return (long long)(u << 24) >> 24; /* 64 - 40 = 24 */
}
svdpi.h offers SV_GET_SIGNED_BITS(VALUE, N) for this, and here is a surprise: the macro, copied verbatim from Annex I into every vendor's header, tests the wrong bit.
#define SV_GET_SIGNED_BITS(VALUE, N) \
((N) == 32 ? (VALUE) : \
(((VALUE) & (1 << (N))) ? ((VALUE) | ~SV_MASK(N)) : ((VALUE) & SV_MASK(N))))
/* ^^^^^^^^ bit N is the first UNUSED bit; the sign bit is N-1 */
LOG sign8_c raw=00000080 macro=128 right=-128
LOG sign8_c raw=000000ff macro=255 right=-1
For an 8-bit -128 the macro returns +128. It only "works" if the unused bits happen to be sign-extended already, which no simulator promises. Write the two-line version yourself.
SV_GET_SIGNED_BITS in svdpi.h (Verilator 5.052's copy, and the Annex I text it reproduces) sign-extends from bit N instead of bit N−1. Measured: SV_GET_SIGNED_BITS(0x80, 8) = 128. Do not use it.
0.7Wide vectors and the select helpers
Nothing changes past 64 bits: a 200-bit value is 7 chunks and C loops over them, as the 200-bit adder in 0.5 does (the lab checks s == a + b in SV). For field access across chunk boundaries svdpi.h provides bit- and part-select helpers that work on the canonical form, so you do not hand-code the word/offset arithmetic:
| Function | Does |
|---|---|
| svBit svGetBitselBit(const svBitVecVal* s, int i) | returns bit i |
| void svPutBitselBit(svBitVecVal* d, int i, svBit s) | writes bit i |
| void svGetPartselBit(svBitVecVal* d, const svBitVecVal* s, int i, int w) | d[w-1:0] = s[i+w-1:i], w ≤ 32 |
| void svPutPartselBit(svBitVecVal* d, const svBitVecVal s, int i, int w) | d[i+w-1:i] = s[w-1:0], w ≤ 32; other bits of d untouched |
| svGetBitselLogic / svPutBitselLogic / svGetPartselLogic / svPutPartselLogic | same on svLogicVecVal (both planes) |
The index is the bit position in the value (bit 0 = LSB), not in the declared range. The last column of svPutPartselBit is the catch. The lab extracts a 16-bit field at bit 100 into an output and inserts 0xBEEF at bit 70 of a 200-bit output:
LOG partsel_in w0=00003039 w2=00000000 w6=00000000 ← C prints the output BEFORE writing it
LOG partsel f=dead w_ok=0 w=00000000000000000000000000002fbbc00000000000003039
The field landed correctly (0x2fbbc at hex digit 17 is 0xBEEF << 70), but the low chunk still holds 0x3039 = 12345, which is the b operand of the previous call (c_add200). The output buffer arrived with stale stack contents, svPutPartselBit touched only 16 bits, and the rest went back into SV. Section 0.9 explains why; Exercise 7 fixes it with one memset.
w_ok=0, low chunk 00003039.0.84-state vectors: aval and bval
A logic/reg vector needs two bits per bit of value. The canonical form keeps them as two parallel planes, chunk by chunk: svLogicVecVal is struct { uint32_t aval; uint32_t bval; } (identical to VPI's s_vpi_vecval), and a $W$-bit value is an array of $\lceil W/32 \rceil$ of them (Fig. 0.5). The per-bit encoding is the one in Fig. 0.2: (bval, aval) = 00 → 0, 01 → 1, 10 → Z, 11 → X.
integer = logic signed [31:0], time = logic [63:0]) use the same form.Two shapes look alike and are not. input logic a is a scalar: one svLogic byte by value. input logic [0:0] b is a one-bit vector: const svLogicVecVal*. The lab's prototype dump confirms both (Spear's warning on p. 425 holds). Mixing them up compiles if you skip the generated header and reads garbage.
In Verilator the bval plane is always zero, because the simulator stores no X or Z to put in it. The lab passes 40'h12_3456_78zx and C reads:
LOG ldump w0.aval=34567800 w0.bval=00000000 w1.aval=00000012 w1.bval=00000000
The z and x nibbles arrive as 0 with no flag. The reverse direction loses information too: C writes aval=0x55, bval=0x33, which in a 4-state simulator is 8'b01zx_01zx; Verilator keeps only aval and SV prints 01010101.
logic), but Verilator cannot exercise its X/Z branches. Test them with a C unit test (Exercise 11) or on a 4-state simulator.Converting a model from 2-state to 4-state (§12.2.5) is mechanical: bit→logic and int→integer in the SV declarations; svBitVecVal→svLogicVecVal in C; read .aval where you read the word before; check .bval on inputs; write .bval = 0 on outputs unless you mean to drive X/Z.
0.9Directions: who owns which bytes
The LRM's rules for input, output and inout (§35.5.1.2 and §35.6.1 on the SV side, H.8.7–H.8.8 on the C side) are short:
- An
inputmust not be modified by C. Packed inputs come asconstpointers so the compiler enforces it; declare scalar inputsconsttoo (Sample 12.5). - An
outputhas an undetermined initial value. C must not read it, and must write all of it. - An
inoutstarts with the current SV value; C may read and update it. - Pointers to arguments are valid only during the call. Do not keep them.
refis not allowed in an import (the book says so in §12.1.3). Verilator 5.052 acceptsref int asilently and generatesint* a; a commercial simulator will reject the same file.
The generated code shows how Verilator implements this. For import "DPI-C" function void c_skip_out(output int o, output bit [39:0] v) the wrapper is:
void Vtop___024unit____Vdpiimwrap_c_skip_out_TOP____024unit(IData &o, QData &v) {
o = 0U;
v = 0ULL;
int o__Vcvt; // uninitialized local
svBitVecVal v__Vcvt[2]; // uninitialized local
c_skip_out(&o__Vcvt, v__Vcvt); // C gets pointers to the locals
o = (o__Vcvt); // copy back, whatever is there
VL_SET_Q_SVBV(40, v, v__Vcvt + 0); // copy back, masked to 40 bits
}
output in Verilator. The SV value never reaches C, and the copy back is unconditional. inout is the same picture plus a copy in (io__Vcvt = io;) before the call.The lab measures every consequence:
| Experiment | Measured (one run; junk values vary) | Meaning |
|---|---|---|
C reads *o before writing; SV had o = 77 | c_peek in=56579259 | C sees stack junk, never the SV value (number varies per run) |
| C never writes its outputs | skip o=32759 v=1234567890 | SV variables are overwritten with junk: the old value is not kept |
… where did 1234567890 come from? | c_scratch(40'h12_3456_7890) | the previous call's 40-bit temp, same stack slot |
inout int io = 10, C does *io += 1 | inout io=11 | copy in, copy out |
c_alias(x, x): same variable as input and output | i_after_write=5, x=100 | the input is a copy; writing the output does not change it |
"If my C function does not write the output, the SV variable keeps its old value" is false. On Verilator it gets stack junk, sometimes a plausible-looking leftover from another call (the 1234567890 above, or 0x3039 in 0.7). Every return path of a C function must write every output, including error paths. The book's own Sample 12.12 gets this wrong (0.12).
0.10string and chandle, briefly
A string input arrives as const char* to a NUL-terminated copy owned by the simulator, valid only during the call: c_strlen("hello, DPI") returns 10 and c_strlen("") returns 0 (never NULL). If C needs the text later, it copies it (strdup). Strings going out (output arguments and string results) raise the question of who owns the memory, which is step 04's topic.
A chandle is an opaque C pointer that SV can store, copy, compare with null and hand back, but never dereference. It is how C objects get an identity in SV:
chandle h = c_new_box(42); // C: void* c_new_box(int v) { int* p = malloc(...); *p = v; return p; }
$display(c_box_get(h)); // 42
$display(h == null, c_is_null(null)); // 0 1 : SV null arrives in C as NULL
Lifetime, per-instance state and C++ objects behind a chandle are step 03.
0.11What a function may return
A function result travels in a register, so §35.5.5 (repeated in H.8.9) limits it to "small values": void, byte, shortint, int, longint, real, shortreal, chandle, string, and scalar bit and logic. That is the whole list: a packed vector, even bit [6:0], is not on it and goes through an output argument. Verilator 5.052 is more permissive than the LRM: it also accepts packed bit results up to 32 bits (returned as an svBitVecVal), which is an extension that will not port. Its messages for the rest:
| Result type | Verilator 5.052 |
|---|---|
| bit [31:0], bit signed [15:0], bit [6:0] | svBitVecVal f(); |
| bit [32:0] | %Error: DPI function may not return a > 32 bits wide type other than basic types. |
| logic [6:0], integer, time | %Error: DPI function may not return a 4-state type other than a single 'logic' (IEEE 1800-2023 35.5.5) |
| logic | svLogic f(); |
| string, chandle | const char* f(); void* f(); |
§12.1.4 says a function "cannot return a vector such as bit [6:0]", and the book is right: §35.5.5 and H.8.9 allow only scalar bit/logic results. Verilator accepts the vector anyway (the lab's rt_bit7 returns 100 through an svBitVecVal result), so code that relies on it compiles here and fails on a strict tool. Return an int or use an output bit [6:0]. The confusion has a source: the LRM's own example in §35.5.4, import "DPI-C" function bit [15:0] getStimulus();, contradicts its normative list (see the LRM errata).
0.12Sample 12.12 on a 2-state simulator
Samples 12.11/12.12 turn the 7-bit counter (Sample 12.9) into a 4-state model that rejects X/Z on reset, load and i. The lab ports it (io_printf → printf; the chandle as void*) and drives exactly the cases the checks exist for: an uninitialized reset (X in a 4-state simulator) and a load value 7'b000_zzxx.
LOG c4 step=xreset out=1 ← reset X seen as 0: the counter counted
LOG c4 step=xload out=0 ← zzxx seen as 0000: loaded 0
LOG c4 step=reset out=0 (no "c4err" line at all)
On Verilator none of the three error branches can fire, so the model silently does something; on Questa or Xcelium the same testbench prints two errors. Reading the sample with 0.5 and 0.9 in mind also reveals two latent bugs (the subject of Exercise 12):
if (i->bval)tests all 32 bits of the chunk, including 25 undetermined ones. On a simulator that leaves junk there, a clean input raises an error.- Every error branch does
returnwithout writingcount. The SV variable then receives an undetermined value (0.9). The fix is to drive X explicitly:count->aval = count->bval = SV_MASK(7).
The book also mentions vpi_control(vpiFinish, 0) to stop the simulation from C. In practice, return a status and let SV call $fatal, or use an exported SV function (step 05).
0.13Commercial simulators (not measured here)
The LRM permits behaviors Verilator never shows, so code tested only here can still break. Expect on Questa/VCS/Xcelium: real X and Z in bval (your 4-state branches finally run); shortreal as a true float; undetermined unused bits in input chunks unless the tool documents otherwise; outputs that may not be masked on return. Their header generators (vc_hdrs.h, -dpiheader) are the equivalent of Vtop__Dpi.h: always include them. And they reject ref in imports, which Verilator lets through.
0.14Rules that keep you safe
- Include the simulator-generated prototype header in every DPI C file.
- Prefer fixed-size C types for computation (
int32_t,uint64_t); castbyte'schartosigned charbefore using it. - Avoid
shortrealin signatures. - Packed inputs: mask the last chunk with
SV_MASK(W % 32)before using it; sign-extend by hand from bit $W-1$. - Packed outputs: write every chunk; write the unused bits as zero.
- Outputs: write every output on every return path; never read one before writing it.
- 4-state: check
bvalon inputs, writebval = 0on outputs; test the X/Z paths outside Verilator. - Results wider than 32 bits, or 4-state, go through
outputarguments.
§1 · ObjectivesWhat you can do after this step
- Write the C prototype of any DPI import from its SV declaration, and check it against the simulator-generated header.
- Explain the three passing modes (by value, const pointer, pointer to a temporary) and pick the mode from type + direction.
- Read and write packed vectors of any width in canonical form: word order, chunk count, unused bits, sign extension.
- Use the
svGet/PutBitsel*andsvGet/PutPartsel*helpers correctly. - Handle 4-state values through aval/bval, and know exactly what a 2-state simulator does to them.
- State the LRM's guarantees for
output/inoutand the consequences of breaking them, with Verilator's measured behavior. - Recognize the platform traps: plain
char,shortreal,longvslong long, shifts ≥ the word width.
Prerequisite: step 01 (build, extern "C", the generated header). Next: step 03 (C state and chandle).
§2 · Exercises12 exercises, graded, with solutions
★ basic · ★★ intermediate · ★★★ real-design. Every code solution is in lab/ex/, built by run.sh and checked by check.py. "Ex. 12.N" is an exercise from the book (which has no solutions).
E1 ★Write the C prototype for this import, without looking at a generated header. Then check it against build/ex_protos.h.
import "DPI-C" function void e1(input byte unsigned a, output longint b, input bit [63:0] c,
output logic [3:0] d, input string e, inout bit f, output chandle g);
Show solution
void e1(unsigned char a, long long* b, const svBitVecVal* c, svLogicVecVal* d,
const char* e, svBit* f, void** g);
That is exactly what Verilator generates (asserted by check.py). Points people miss: bit [63:0] is a pointer to two chunks, not a uint64_t, even though it would fit; logic [3:0] is svLogicVecVal* even for 4 bits; an inout bit is svBit*; an output chandle is void**.
E2 ★Ex. 12.1. Write shift_c(i, n): i is a 32-bit unsigned input, n an int; positive n shifts left, negative shifts right, 0 does nothing. Test every case from SV, including n = ±32 and ±40, with SV's own shift as the reference.
Show solution
import "DPI-C" function int unsigned shift_c(input int unsigned i, input int n);
function automatic int unsigned shift_sv(int unsigned i, int n); // reference
return n >= 0 ? i << n : i >> -n;
endfunction
unsigned int shift_c(unsigned int i, int n) {
if (n >= 32 || n <= -32) return 0; /* SV semantics; in C this would be UB */
return n >= 0 ? i << n : i >> -n;
}
The type mapping is trivial; the trap is C. Shifting a 32-bit value by 32 or more is undefined behavior in C, and x86 masks the count to 5 bits. The lab's shift_naive_c (no guard) returns 800000f1 for n=32 (unchanged) and 0000f100 for n=40 (shifted by 8), where SV gives 0. A golden model that inherits C's shift semantics will disagree with the RTL exactly at the boundary cases. Lab: ex/shift.sv, 11 values of n, PASS.
E3 ★Ex. 12.2. Add a load flag: shift_ld_c(i, n, ld). When ld is 1, i shifted by n is loaded into an internal 32-bit register; when 0, the register itself is shifted. Return the register. What is the type of ld on each side?
Show solution
unsigned int shift_ld_c(unsigned int i, int n, svBit ld) {
static unsigned int reg; /* one register for the whole simulation */
reg = shift_c(ld ? i : reg, n);
return reg;
}
input bit ld ↔ svBit ld (a uint8_t by value). The lab loads 0xFF shifted left by 4, shifts right by 8, shifts by 40, and checks each step against an SV model. The static register is a design smell: two SV instances would share it. Step 03 fixes that with a chandle (Ex. 12.3–12.6).
E4 ★Predict, on Verilator: (a) C writes *o = 0x1FF into output bit [7:0] o; (b) C writes *b = 2 into output bit b. What does SV hold? What does the LRM guarantee?
Show solution
(a) 8'hFF: Verilator copies the chunk back masked to 8 bits (the 40-bit version is measured in the lab: dirty d=ffffffffff plus1=0000000000). (b) 0: scalars are copied back as 1U & value, and 2 has bit 0 clear (measured with 0xFE: badbit b=0). The LRM guarantees neither: writing bits outside the declared width, or a scalar value other than 0/1 (2-state) or 0–3 (4-state), is the C code's bug, and another simulator may store the junk. Correct C writes clean values.
E5 ★★Write int c_popcount200(input bit [199:0] v) in C. Make it correct on a simulator that leaves junk in unused bits. Test against $countones on 1000 random values plus all-zeros and all-ones.
Show solution
int c_popcount200(const svBitVecVal* v) {
const int W = 200, N = SV_PACKED_DATA_NELEMS(W); /* 7 chunks */
int n = 0;
for (int k = 0; k < N; k++) {
svBitVecVal w = (k == N - 1 && W % 32) ? v[k] & SV_MASK(W % 32) : v[k];
n += __builtin_popcount(w);
}
return n;
}
Without the mask the function passes on Verilator (unused bits are 0 there) and can over-count elsewhere by up to 24. Lab: ex_popcount all_ones=200 errs=0.
E6 ★★Find the bug. The function should return a bit signed [39:0] as a long long. What does it return for −2? Fix it.
long long c_s40_bad(const svBitVecVal* v) { return (long long)((unsigned long long)v[1] << 32 | v[0]); }
Show solution
It returns 1 099 511 627 774 (0xFF_FFFF_FFFE): the sign bit is bit 39, and chunk 1 carries only bits [39:32] with nothing above them (measured: s40 w1=000000ff). Fix: mask, then shift the sign bit to bit 63 and back arithmetically.
long long c_s40(const svBitVecVal* v) {
unsigned long long u = ((unsigned long long)(v[1] & SV_MASK(8)) << 32) | v[0];
return (long long)(u << 24) >> 24;
}
The lab checks 6 values including the extremes 40'sh7F_FFFF_FFFF and -40'sh80_0000_0000: bad_m2=1099511627774 good_m2=-2 errs=0. (Right-shifting a negative signed value is implementation-defined before C++20/C23 but arithmetic on every compiler you will use.)
E7 ★★Find the bug and fix it. The SV side checks w == (200'hBEEF << 70) and fails.
void c_put16(svBitVecVal* w, const svBitVecVal* f, int lsb) { /* output bit [199:0] w */
svPutPartselBit(w, *f, lsb, 16);
}
Show solution
w is an output: its initial contents are undetermined, and svPutPartselBit writes only bits [85:70]. The other 184 bits go back to SV as whatever the stack held (in the lab, the previous call's operand). Clear the whole output first:
memset(w, 0, SV_PACKED_DATA_NELEMS(200) * sizeof(svBitVecVal));
svPutPartselBit(w, *f, lsb, 16);
The lab's test calls a function with an all-ones 200-bit input first, to leave junk in the stack slot; with the memset: ex_partsel ok=1. If the SV side really wanted "insert a field into an existing value", the argument should have been inout.
E8 ★★A colleague's model uses import "DPI-C" function shortreal gain(input shortreal x); and float gain(float x) { return 2.0f * x; }. It links and runs on Verilator. Predict roughly what gain(1.5) prints, explain why, and give two fixes.
Show solution
Garbage, not 3.0. Verilator promotes shortreal to real and calls double gain(double); the C function reads the low 32 bits of the double in xmm0 as a float, and its float result is read back as a double, typically a tiny denormal (the lab's identity function gives 1.27e-314 for 0.1). The linker only matches names, so nothing complains. Fixes: (1) use real/double on both sides; (2) #include "Vtop__Dpi.h", which turns the mismatch into the compile error functions that differ only in their return type cannot be overloaded (lab build types_hdr). If single-precision rounding matters for the model, pass the IEEE bit pattern as int unsigned.
E9 ★★This passes all tests on your Mac and on the x86 farm. On the new ARM Linux farm, the scoreboard reports that negative samples are never detected. Why, and what is the fix?
int c_isneg(char b) { return b < 0; } /* import: function int c_isneg(input byte b) */
Show solution
The LRM maps byte to plain char, and plain char is unsigned on ARM Linux: b < 0 is always false. Fix: return (signed char)b < 0;. The lab builds the same file twice; with -funsigned-char the bad version returns 0 for −5 (bad_m5=0) while the fixed one passes all 256 values in both builds.
E10 ★★★Write c_mul128(input longint unsigned a, b, output bit [127:0] p): the full 128-bit product, without __int128. Check against SV's 128'(a) * 128'(b) on 2000 random pairs and on the maximum.
Show solution
void c_mul128(unsigned long long a, unsigned long long b, svBitVecVal* p) {
unsigned int x[2] = {(unsigned int)a, (unsigned int)(a >> 32)};
unsigned int y[2] = {(unsigned int)b, (unsigned int)(b >> 32)};
unsigned long long acc[4] = {0, 0, 0, 0};
for (int i = 0; i < 2; i++) { /* schoolbook, 32-bit limbs */
unsigned long long carry = 0;
for (int j = 0; j < 2; j++) {
unsigned long long t = (unsigned long long)x[i] * y[j] + acc[i + j] + carry;
acc[i + j] = t & 0xFFFFFFFFu;
carry = t >> 32;
}
acc[i + 2] += carry;
}
for (int k = 0; k < 4; k++) p[k] = (svBitVecVal)acc[k]; /* all 4 chunks written */
}
The limb width equals the chunk width, so the result maps 1:1 onto p[0..3]. Each partial sum fits in 64 bits: $(2^{32}-1)^2 + 2(2^{32}-1) = 2^{64}-1$. Lab: max=fffffffffffffffe0000000000000001 errs=0, which is $(2^{64}-1)^2 = 2^{128} - 2^{65} + 1$.
E11 ★★★Write int count_xz(const svLogicVecVal* v, int width): how many bits of a 4-state vector are X or Z. What does it return under Verilator for 40'hzz_xxxx_0001? How do you test it at all?
Show solution
int count_xz(const svLogicVecVal* v, int width) {
int n = 0;
for (int k = 0; k < SV_PACKED_DATA_NELEMS(width); k++) {
unsigned int b = v[k].bval; /* X or Z ⇔ bval = 1 */
if (k == SV_PACKED_DATA_NELEMS(width) - 1 && width % 32) b &= SV_MASK(width % 32);
n += __builtin_popcount(b);
}
return n;
}
Under Verilator it returns 0 (ex_xz verilator=0): the X and Z became 0 before DPI saw them. To test the logic, build the same file as a plain C program (-DUNIT) with hand-made svLogicVecVal values, including junk in the unused bits: ex_xz unit=24 (8 Z + 16 X bits). This is the general technique for 4-state C code on a 2-state simulator.
E12 ★★★Review Sample 12.12 (the 4-state counter7, ported in lab/counter4.c). Find two latent bugs that a 4-state simulator would expose, and fix them. Why does the lab's run on Verilator not show either?
Show solution
(1) if (i->bval) tests the whole chunk; 25 of its bits are undetermined, so a clean input can raise a false error. Use i->bval & SV_MASK(7). (2) The error branches return without writing count, so SV receives an undetermined value (0.9). Drive X instead, so the testbench sees the problem:
if ((reset & 2) || (load & 2) || (i->bval & SV_MASK(7))) {
printf("Z or X detected on an input\n");
count->aval = count->bval = SV_MASK(7); /* 7'bx */
return;
}
On Verilator the unused bits are zero (bug 1 hidden) and no input is ever X/Z, so the error branches never run (bug 2 hidden): counter4.log and ex_c4fix.log are identical, with no c4err line. Full fix: lab/ex/counter4_fix.c.
§3 · Quiz14 questions, self-grading
1. What does C receive for input bit [7:0] x?
unsigned char is what byte unsigned maps to; svBit is for a scalar bit.2. A bit [39:0] holds 40'h12_3456_789A. What is in v[1] per the LRM?
00000012, but the LRM only guarantees the 8 used bits; (c) is the Verilator-specific answer.3. C does printf("%d", *o) before writing output int o; SV set o = 77 before the call. What prints on Verilator?
c_peek in=56579259). It zeroes the SV variable before the call, not the temporary, so even 0 is not what C sees.4. C returns early and never writes output int o. SV had o = 77. After the call, on Verilator, o is…
o = o__Vcvt;). Measured: skip o=32759. "Unchanged" is the tempting wrong answer; X is impossible in a 2-state simulator and not what the LRM says either (it says undetermined).5. Which return type is legal for an imported function?
byte…longint, real, shortreal, chandle, string) and scalar bit/logic; longint unsigned is the legal way to return 64 bits. (d) is the tempting one: Verilator 5.052 compiles it (an extension, returned as svBitVecVal), but no packed vector is a legal result, so it will not port. (a) and (b) are rejected even by Verilator.6. input logic a and input logic [0:0] b arrive in C as…
7. A logic scalar has the value Z in a 4-state simulator. What byte does C get?
sv_z = 2: bval (bit 1) = 1, aval (bit 0) = 0. X is 3. On Verilator it would be 0, because Z is stored as 0 before DPI sees it.8. c_dump_s40(-40'sd2) on Verilator: what is v[1]?
w1=000000ff). 0xFFFFFFFE is v[0].9. SV_GET_SIGNED_BITS(0x80, 8) from svdpi.h returns…
macro=128 right=-128. It is in the Annex I header that every vendor ships.10. Verilator, a shortreal argument, C declares float, no generated header included. What happens?
double (with only a warning). With extern "C" the linker matches the name only, so the float/double mismatch is silent (measured 0.1 → −1.59e−23). No conversion happens because the caller and the callee never see each other's prototype.11. On ARM Linux, c_byte(char v) receives SV's byte value −128. What does printf("%d", v) print, and what does SV get back if C returns v?
char is unsigned there, so C computes with 128, but the bit pattern 0x80 travels back unchanged and SV's signed byte reads −128 (lab, -funsigned-char build). Loopback tests cannot catch this.12. You call c_alias(x, x) with (input int i, output int o). C writes *o = 100, then reads i. It reads…
i_after_write=5, then x=100 after the copy back.13. Which statement about ref in a DPI import is right?
int* without a warning (lab a_ref), so code using it is not portable to commercial simulators.14. Your C model reads a logic [6:0] input. Which checks make it correct on every LRM-compliant simulator? (two answers)
bval != 0 (the book's code) can flag a clean input, and relying on Verilator's zeroing is exactly what breaks on other simulators.§4 · LabMeasure the mapping yourself
Everything lives in 02_data_types/lab/. Run it:
cd tutorial/02_data_types/lab
python3 check.py # runs run.sh (18 clean builds, 2 fail on purpose; ~10-60 s with ccache), then 72 assertions
python3 check.py --no-run # re-check existing build/ logs
Predict first, then open the log: what does C see for X and Z? What happens to an output C never writes? What does a float-declared function read when Verilator passes shortreal? What is in the upper bits of a negative bit signed [39:0]?
| Experiment (files) | What it shows | Key result (measured) |
|---|---|---|
| types.sv/.c | scalar mapping, extremes round trip, sizes | all extremes intact; X,Z → 0 |
| types_fix | shortreal with double in C | 0.10000000000000001 |
| types (float) | shortreal with float in C | c_sreal −1.588e−23 |
| types_hdr | include Vtop__Dpi.h | compile error, as wanted |
| types_uchar | -funsigned-char (ARM Linux) | c_byte 128, enum 254 |
| zlit.sv | 1'bz literal argument | Unsupported tristate construct |
| vec.sv/.c | layout, signed, dirty output, 200-bit add, part-select, sign macro, 4-state | w1=00000012; s40 w1=000000ff; dirty masked; macro=128 |
| dir.sv/.c | output/inout/alias/scalar masking | junk in, junk out; inout ok; input is a copy |
| counter4.sv/.c | Samples 12.11/12.12 | no X/Z error ever fires |
| build/proto/* | prototypes and illegal result types | see 0.11; ref accepted |
| ver.sv, ver.c | svDpiVersion() (H.10.1.3) | LOG ver dpi=1800-2005 |
| ex/* | exercise solutions | all PASS |
Things to look at beyond the logs: build/vl_dir/Vtop___024unit__0.cpp (the generated wrappers of 0.9: the uninitialized temporaries and the masking copy-backs) and build/vl_types/Vtop__Dpi.h (the prototype table of 0.1, written by the tool). The junk values in dir.log (c_peek in=…, skip o=…) change from run to run; check.py asserts only that SV received exactly what C saw. One lab detail is also an SV lesson: $display(ok ? "PASS" : "FAIL") printed 1346458451, because a string literal inside an expression is an integral value ("PASS" = 0x50415353); the testbenches use an if.
§5 · Cheat sheetOne table, eight rules
| SV declaration | C prototype | Pitfall |
|---|---|---|
| input byte b | char b | signedness of char: cast to signed char |
| input longint x | long long x | not long (32 bits on Windows) |
| input shortreal f | float f (LRM) / double (Verilator) | silent garbage: avoid shortreal |
| input bit s / logic s | svBit s / svLogic s | logic: 0,1,2=Z,3=X; Verilator only 0/1 |
| input bit [W-1:0] v | const svBitVecVal* v | ⌈W/32⌉ chunks, LS first; mask last chunk |
| input bit signed [W-1:0] v | const svBitVecVal* v | no sign extension; SV_GET_SIGNED_BITS is buggy |
| input logic [W-1:0] v | const svLogicVecVal* v | check bval & mask; X/Z never reach C on Verilator |
| output T o | T* o | starts as junk; write it on every path |
| output bit [W-1:0] o | svBitVecVal* o | write all chunks; zero the unused bits |
| inout T io | T* io | the only direction where C may read first |
| input string s | const char* s | valid during the call only; copy to keep |
| input chandle h | void* h | SV null = C NULL |
| function bit [≤31:0] f() | svBitVecVal f() | wider or 4-state results: use an output |
| ref int r | (illegal) | Verilator accepts it: not portable |
- The simulator writes the prototypes: include its header, always.
- Three modes: small input by value, packed input by const pointer, every output/inout by pointer to a temp.
- $N = \lceil W/32 \rceil$ chunks, chunk 0 = bits [31:0], last chunk has $W \bmod 32$ used bits.
- Mask unused bits on read, zero them on write.
- Sign-extend by hand from bit $W-1$.
- Outputs are write-only and must be fully written on every return path.
- 4-state = aval/bval planes; bval 1 means X or Z. A 2-state simulator erases them before C runs.
- Verilator's kindness (zeroed inputs, masked outputs) is not the LRM's guarantee.
Spear & Tumbush, SystemVerilog for Verification, 3rd ed. (Springer, 2012), §12.1.3–12.1.4 and §12.2.3–12.2.5; IEEE Std 1800-2023 §35.5 and Annexes H and I. Lab: Verilator 5.052 on macOS x86_64 (Apple clang). All numbers on this page come from lab/ and are asserted by lab/check.py.