DPI-C tutorial · Part A · Step 04 · Spear & Tumbush §12.4–12.6
Arrays, structs and strings across the boundary
Scalars cross DPI by value. Everything bigger crosses by reference: C gets a pointer or a handle into storage that SystemVerilog owns. This step is about what that storage looks like, who may touch it, and for how long.
§0 · TutorialThe one rule: SystemVerilog owns the storage
A golden model rarely works on one number. It takes a packet, a frame of samples, a header, a matrix of coefficients, and returns another one. In the first three steps every argument was a scalar or a packed vector of a known width. Now we pass aggregates: unpacked arrays of fixed and unknown size, packed arrays of wide elements, structs and strings.
All of them obey one rule, and the rest of this page is detail around it: SystemVerilog owns every byte that crosses the boundary. C receives a pointer (fixed-size arrays, packed structs), a handle to a descriptor (open arrays) or a const char* (strings). That access is valid only for the duration of the call. C cannot allocate an array and give it to SV; SV cannot keep a pointer into C memory. Spear says it in one line under Sample 12.19: "There is no way to allocate an array in C and reference it in SystemVerilog." If C needs the data later, it copies. If SV needs C's data, C writes it into an SV-owned argument or returns a string that SV immediately copies.
The second theme is layout. For a few types (int, byte, longint, real, packed vectors in canonical form) the C view is fixed by IEEE 1800 Annex H (H.7.3, H.7.4, H.7.7). So is the element order of an unpacked array: lower index first, whatever the declared direction (H.7.3, H.7.6). For the rest (whether a whole open array is in C layout, how a simulator stores an unpacked array of bit [12:0]) the standard gives you access functions instead of a layout, and portable code uses them (H.12.3–H.12.6). The lab measures where Verilator 5.052 differs from what you might assume, and where it does not implement the standard at all (dynamic arrays, queues and unpacked structs as DPI arguments).
0.1Fixed-size unpacked arrays (Samples 12.18–12.21)
The simplest aggregate is an unpacked array with constant bounds in the import declaration. H.7.3 and H.11.4 map it to a C array of the element type, which C sees as a pointer to the first element. Each element is represented exactly as a scalar argument of that type would be (step 02): int is an int, byte is a char, a packed bit [31:0] is one svBitVecVal word, a packed logic [31:0] is one svLogicVecVal (an aval/bval pair).
// fib.sv (Samples 12.19 / 12.21, in a module instead of a program)
import "DPI-C" function void fib2(output bit [31:0] data[20]);
import "DPI-C" function void fib4(output logic [31:0] data[20]);
import "DPI-C" function int sum8(input int v[8]);
import "DPI-C" function void bytes_in(input byte b[4], output int packed_le);
// fib.c (Samples 12.18 / 12.20). Verilator compiles .c as C++: wrap in extern "C".
void fib2(svBitVecVal data[20]) { // [20] is documentation: C sees svBitVecVal*
data[0] = 1;
data[1] = 1;
for (int i = 2; i < 20; i++) data[i] = data[i-1] + data[i-2];
}
void fib4(svLogicVecVal data[20]) { // 4-state: write aval AND bval
data[0].aval = 1; data[0].bval = 0;
data[1].aval = 1; data[1].bval = 0;
for (int i = 2; i < 20; i++) {
data[i].aval = data[i-1].aval + data[i-2].aval;
data[i].bval = 0;
}
}
int sum8(const int* v) { int s = 0; for (int i = 0; i < 8; i++) s += v[i]; return s; }
void bytes_in(const char* b, int* packed_le) { memcpy(packed_le, b, 4); }
Do not guess the C prototype: Verilator writes it for you in V<top>__Dpi.h (step 01). For this file it generated, verbatim:
extern void bytes_in(const char* b, int* packed_le);
extern void fib2(svBitVecVal* data);
extern void fib4(svLogicVecVal* data);
extern int sum8(const int* v);
Note the const on input arrays and its absence on output. Include the header in your C file and the compiler checks your definition against it. The run prints:
LOG fib2 d0=1 d1=1 d19=6765
LOG fib4 d19=6765 unknown=0
LOG sum8 sum=140
LOG bytes_in le=44332211
bytes_in shows that a byte array is a plain char array in memory, one byte per element: copying four of them into an int on a little-endian x86 host puts b[0] in the least significant byte.
The bval you forgot. In a 4-state array C must write bval too; uninitialized it is garbage and a commercial simulator will show X or Z. The lab's fib4_bad writes aval = i and bval = 0xFFFFFFFF (all X by the encoding): Verilator prints d2=00000002 unknown=0. It is 2-state and simply ignores bval. A model that "passes" on Verilator can produce X on Questa or VCS. If your model is 2-state, declare the SV side with bit and use svBitVecVal; that is the honest contract.
Because SV owns the storage, the size must be known on the SV side before the call. If the model decides how much data it produces (a decoder, a variable-length packet), split it in two calls: a query (int c_len()) and a fill (c_fill(output … buf[])), with the SV side sizing its buffer in between. Exercise 9 does it for the reverse direction.
0.2Directions: who copies what, and when
For aggregates the direction means more than for scalars. The LRM lets the simulator pass arrays either by pointer to the actual or by pointer to a temporary that it copies in before the call and out after it. The rules C can rely on are the ones that hold in both implementations:
- input: C may read, must not write (the prototype is
const). - inout: C reads the current SV value and may write; whatever it does not write keeps its value.
- output: the value C sees on entry is undefined, and after return the whole argument is the result. Anything C did not write is undefined too.
The last rule bites. The lab's outdir.sv fills three arrays with 5, then calls a C function that writes only element 1 with 7:
LOG c_sees out0=0 <- output open array: C sees a zeroed temporary
LOG outdir out=0,7,0 io=5,7,5 outf0_is5=0 outf1=7 <- output: unwritten elements LOST; inout: kept
With an output open array Verilator hands C a zero-filled temporary and copies all of it back, so the 5s become 0. With an output fixed array the unwritten element came back as stack garbage (not 5; the exact value is not reproducible and the check only asserts "not 5"). With inout the untouched elements keep their 5. The practical rule: use output only when C writes every element; otherwise use inout. Exercise 10 shows a subtler version: a function that returns an error code before touching its output still overwrites it.
0.3Open arrays: one C function for any size
A fixed-size formal ties the C code to one array size and, worse, to a layout you reverse-engineered. Spear's §12.5 calls that "fragile". The alternative is the open array: write the unpacked dimension(s) as [] in the import, and the same C function accepts any actual size and any range. C now receives an svOpenArrayHandle. It is not a pointer to data; it is an opaque handle to a descriptor that the simulator builds for this call: element type and width, number of unpacked dimensions, the declared range of each dimension, and where the data lives.
// openarr.sv (Samples 12.22 / 12.24, corrected)
import "DPI-C" function void fib_oa(output bit [31:0] data[]); // one [] = 1 unpacked dim
import "DPI-C" function void query(input int a[][], output int q[11]);
import "DPI-C" function void mydisplay(inout int a[][]); // [][] = 2 unpacked dims
module top;
bit [31:0] fibs[20];
int a[6:1][8:3]; // descending, not 0-based: the C side must not care
int up[0:2][0:3]; // ascending
...
void fib_oa(const svOpenArrayHandle h) { // Sample 12.23, corrected
svBitVecVal* d = (svBitVecVal*)svGetArrayPtr(h); // contiguous storage (may be NULL, see 0.4)
int n = svSize(h, 1);
d[0] = 1; d[1] = 1;
for (int i = 2; i < n; i++) d[i] = d[i-1] + d[i-2];
}
Two bugs in the book's open-array example. Sample 12.22 calls fib_oa(data, SIZE) with two arguments but imports a one-argument function (the size travels inside the handle; the extra argument is a compile error). Sample 12.23 loops for (i=2; i<=svSize(data_oa,1); i++), which writes one element past the end of the array. The loop bound is i < svSize(h,1).
The descriptor is read with the query functions of H.12.2 (Spear's Table 12.3). Dimensions are numbered from 1, outermost first. Here is what C sees for the two arrays above, measured:
| Query | Meaning | int a[6:1][8:3] | int up[0:2][0:3] |
|---|---|---|---|
svLeft(h,d) / svRight(h,d) | bounds as written in the declaration | 6, 1 · 8, 3 | 0, 2 · 0, 3 |
svLow(h,d) / svHigh(h,d) | min / max index: use these in loops | 1, 6 | 0, 2 |
svIncrement(h,d) | 1 if left ≥ right, else −1 | 1 | −1 |
svSize(h,d) | elements in dim d (high − low + 1) | 6 · 6 | 3 · 4 |
svDimensions(h) | number of unpacked dimensions | 2 | 2 |
svSizeOfArray(h) | bytes of storage (0 if not contiguous C layout) | 144 | 48 |
The sign of svIncrement surprises people: it is +1 for the "Verilog-style" descending range [6:1] and −1 for [0:2]. Despite the name it is not the step that walks from left to right (that step is −1 for [6:1]); it is the Verilog notion of "is the range written big-endian". You will rarely need it: iterate from svLow to svHigh and you are always right. The sizes check out: $6 \times 6 \times 4 = 144$ bytes and $3 \times 4 \times 4 = 48$.
0.4Reaching the elements
H.12.4 and H.12.5 offer two ways in (Spear's Table 12.4):
void* svGetArrayPtr(h): the address of the whole array, only if the simulator stores it in the standard C layout; otherwiseNULL. Verilator's runtime returnsNULLwhen the variable is not flagged as C layout and, in the same case,svSizeOfArrayreturns 0.void* svGetArrElemPtr1(h, i),…Ptr2(h, i, j),…Ptr3and the variadicsvGetArrElemPtr(h, i, …): the address of one element, addressed with the SV indices (6..1, 8..3 in our example). Out of range returnsNULL.
The element-pointer form is the portable one, because it does not assume an order in memory. How much does that matter? The lab asks Verilator where things are, for int v[10:1] (filled with v[i] = i) and for a[6:1][8:3] after mydisplay stored a[i][j] = i*j:
LOG flat v0=1 v_last=10 v_at_low=0 m0=3 m1=4 m6=6
Offset 0 holds v[1], the low index, even though the declaration starts at 10. In two dimensions the storage is row-major starting at the low corner: m0 = a[1][3] = 3, m1 = a[1][4] = 4, m6 = a[2][3] = 6. A C programmer who reads [10:1] as "element 10 comes first" is wrong, and not only in Verilator: H.7.3 and H.7.6(c) fix the order for every tool, "elements with lower indices go first", the element with SV index min(L,R) has C index 0. Still write svGetArrElemPtr1(h, i) with SV indices: it is correct for any range and survives arrays that are not in C layout.
int sum_oa(const svOpenArrayHandle h) { // any size, any range, any direction
int s = 0;
for (int i = svLow(h, 1); i <= svHigh(h, 1); i++)
s += *(int*)svGetArrElemPtr1(h, i);
return s;
}
int probe_oob(const svOpenArrayHandle h) { // index 0 does not exist in [10:1]
return svGetArrElemPtr1(h, 0) == NULL ? -1 : 0;
}
LOG sum_oa fixed=55
%Warning: DPI svOpenArrayHandle function index 1 out of bounds; 0 outside [10:1].
LOG oob ret=-1
The 0-based reflex. for (i = 0; i < svSize(h,1); i++) svGetArrElemPtr1(h, i) is the most common open-array bug: correct for int a[10], a NULL dereference for int a[10:1] (index 0 does not exist). In the lab (ex/bug_loop) the simulator dies with SIGSEGV (exit code 139) before printing anything. The loop variable of an open-array loop is an SV index: start at svLow, stop at svHigh.
Each svGetArrElemPtr* call walks the descriptor and bounds-checks every index. For a 1 k-element golden-model loop that is noise; for 1 M elements called every clock it is not. When it matters, check once that svGetArrayPtr(h) is non-NULL and the element type is a C type (int, byte, longint, real), then index the flat block yourself from svLow, with a fallback to element access. Step 06 measures the cost per call.
0.5Multi-dimensional open arrays (Samples 12.24–12.25)
Each [] in the formal is one unpacked dimension. The actual must have the same number of unpacked dimensions; their sizes and ranges are free. Spear's example walks a 2-D array with svLow/svHigh and writes it back through inout:
void mydisplay(const svOpenArrayHandle h) { // Sample 12.25 (io_printf -> printf)
for (int i = svLow(h, 1); i <= svHigh(h, 1); i++)
for (int j = svLow(h, 2); j <= svHigh(h, 2); j++) {
int* a = (int*)svGetArrElemPtr2(h, i, j); // SV indices, both dimensions
*a = i * j;
}
}
SV filled a[i][j] = i + j before the call; the C side saw a[6][3] = 9 and replaced every element with i*j; back in SV, a[6][3] = 18, a[1][3] = 3, a[4][8] = 32. This is also the answer to Spear's Ex. 12.7 (what do the query functions return for Sample 12.24): svLeft(h,1)=6, svLeft(h,2)=8, svRight(h,1)=1, svRight(h,2)=3, svSize(h,1)=6, svSize(h,2)=6, svDimensions(h)=2, svSizeOfArray(h)=144; all measured in the table of 0.3.
Passing a 2-D array to a 1-D open formal (or the opposite) is a compile-time mismatch. Calling svGetArrElemPtr1 on a 2-D handle is caught at run time: Verilator warns "called on 2 dimensional array using 1 dimensional function" and returns NULL.
[6:1], [8:3]) does not change the memory order. This is the LRM's rule (H.7.3, H.7.6), not a Verilator choice; the access functions remain the safe way because they also work when svGetArrayPtr is NULL.0.6Dynamic arrays, queues and slices as actuals
IEEE 1800-2023 §35.6.1 says "it is sometimes permissible to pass a dynamic array as an actual argument" to an import, under the same rules as for native subroutines (§7.7); by those rules a queue qualifies too, an associative array does not. Commercial simulators accept both. That is the main reason open arrays exist in a testbench: a packet is usually a byte payload[] or a byte q[$]. Verilator 5.052 implements only fixed-size actuals. The lab builds the same call with four actuals:
Actual passed to input int a[] | Verilator 5.052 |
|---|---|
int a[4] (fixed) | works (sum=10) |
int a[] (dynamic) | %Error-UNSUPPORTED: Unsupported: DPI argument of type DYNARRAYDTYPE |
int a[$] (queue) | %Error-UNSUPPORTED: Unsupported: DPI argument of type QUEUEDTYPE |
fixed4[1:2] (slice) | %Error: Internal: Unexpected Call |
Commercial simulators accept all four. On Verilator the workaround is to move the data through a fixed-size buffer and pass the count explicitly; a wrapper function in SV keeps the call sites clean (Exercise 9 sums a 1000-element dynamic array through a 256-element chunk buffer and gets 499500, like d.sum()). It costs one copy, which is what a commercial simulator usually does internally anyway.
If you write a golden model meant to be shared with a team running Questa or VCS, keep the C signature as an open array (input byte data[]) and put the Verilator-specific chunking in an SV wrapper under `ifdef VERILATOR. The C code does not change, and on the commercial simulator you call it directly with the dynamic array.
0.7Packed elements: one packed dimension of any width (Samples 12.26–12.27)
An open array has exactly one packed dimension (the element's bits) plus one or more unpacked ones. The actual may have several packed dimensions as long as they flatten to the same total width: Spear passes bit [1:0][0:3][6:-1] bpack[9:1] (2·4·8 = 64 bits) to a bit [63:0] b64[] formal.
void view_pack(const svOpenArrayHandle h) { // Sample 12.27, corrected
for (int i = svLow(h, 1); i <= svHigh(h, 1); i++) { // book: i < svHigh -> skips bpack[9]
unsigned long long v = *(unsigned long long*)svGetArrElemPtr1(h, i);
printf("C: b64[%d]=%016llx\n", i, v);
}
}
LOG sv bpack2=1234567890abcdef bpack2_0=90abcdef bpack2_0_0=90
LOG c b64_1=0000000000000001
LOG c b64_2=1234567890abcdef
LOG c b64_9=0000000000000009
Sample 12.27 loops i < svHigh(h,1): with bpack[9:1] it prints elements 1 to 8 and never reaches 9. The bound must be <=. (Its sibling Sample 12.23 has the opposite bug, one past the end: these two together are the best argument for always writing svLow…<= svHigh.)
Reading a 64-bit element through an unsigned long long* works because Verilator stores a 64-bit element as a 64-bit integer. That is an accident of this element width. H.7.7 defines a canonical representation for packed data (svBitVecVal words, 32 bits each, least significant word first). For an array passed to an open-array formal, H.7.3 says packed elements are "in canonical form", and H.12.3 lets a simulator use a compacted storage instead only if it then refuses direct pointers: H.12.4 says svGetArrElemPtr* "shall return NULL if the representation of the array elements differs" from that of a single value. The lab asks Verilator how many bytes each element really takes:
| Element type | Canonical (svBitVecVal words) | Verilator storage (measured) |
|---|---|---|
bit [7:0] | 4 bytes (1 word) | 1 byte |
bit [12:0] | 4 bytes (1 word) | 2 bytes |
bit [63:0] | 8 bytes (2 words) | 8 bytes |
bit [99:0] | 16 bytes (4 words) | 16 bytes |
So the "obvious" code below is wrong for anything under 33 bits. With b8 = '{11, 22, 33, 44} it returns 44332211: it read four 1-byte elements as one word.
int raw8(const svOpenArrayHandle b) { // BUG: assumes canonical layout
return *(svBitVecVal*)svGetArrElemPtr1(b, 0); // Verilator: 4 elements at once -> 0x44332211
}
The portable path copies an element into (or out of) canonical form, whatever the simulator does internally:
svBitVecVal e[4]; // 100 bits -> 4 words, LSW first
svGetBitArrElem1VecVal(e, h, i); // copy element i OUT, canonical
svPutBitArrElem1VecVal(h, e, i); // copy element i IN
svGetLogicArrElem1VecVal(&lv, h, i); // 4-state flavor: svLogicVecVal
svBit b = svGetBitArrElem1(h, i); // 1-bit elements: bit m[]
The lab sums three 100-bit elements with a 32-bit carry chain over the canonical words and gets 3ffffffff0000000100000002, identical to SV's own w[0]+w[1]+w[2]; svPutBitArrElem1VecVal writes f0000000000000000cafef00d into w[1]; and svGetLogicArrElem1VecVal on a logic [7:0] element returns aval=a5 with bval overwritten to 0 (the lab pre-poisons it with 0xDEAD).
byte, shortint, int, longint) or when you checked the width. The Get/Put…VecVal functions are always safe.0.8Structs (Samples 12.28–12.29)
A packed struct is, for DPI, just a packed vector of its total width: it crosses as svBitVecVal* (or svLogicVecVal* if any member is 4-state) in canonical form. The first member declared occupies the most significant bits. Spear's pixel struct packed { bit [7:0] r, g, b; } is 24 bits with r in [23:16], g in [15:8], b in [7:0], all inside word 0.
typedef struct { unsigned char b, g, r; } *p_rgb; // Sample 12.28: overlay, reversed order
void invert(svBitVecVal* v) {
p_rgb rgb = (p_rgb)v; // little-endian: byte 0 = bits [7:0] = b
rgb->r = ~rgb->r; rgb->g = ~rgb->g; rgb->b = ~rgb->b;
}
void invert_bits(svBitVecVal* v) { *v = ~*v & 0x00FFFFFFu; } // portable: shifts and masks
LOG invert r=ed g=cb b=a9 <- 12,34,56 inverted (book version, x86)
LOG invert_bits r=12 g=34 b=56 <- inverted back (portable version)
The book's overlay works, and the book is honest about why: the struct lists the bytes backwards because x86 is little-endian. It is still a trap: it breaks on a big-endian host, and for a struct wider than 32 bits it depends on how fields straddle 32-bit words. The robust method treats the struct as a bit vector and extracts fields by bit position, with shifts or with Annex H's part-select helpers svGetPartselBit(dst, src, lsb, width) / svPutPartselBit(dst, value, lsb, width) (width ≤ 32).
typedef struct packed { bit [47:0] dst, src; bit [15:0] etype; } eth_hdr_t; // 112 bits
import "DPI-C" function int etype_of(input eth_hdr_t h); // C: const svBitVecVal* h (4 words)
import "DPI-C" function void make_hdr(output eth_hdr_t h);
int etype_of(const svBitVecVal* h) { svBitVecVal e; svGetPartselBit(&e, h, 0, 16); return e; }
void make_hdr(svBitVecVal* h) { // dst=[111:64] src=[63:16] etype=[15:0]
svPutPartselBit(h, 0x86DD, 0, 16);
svPutPartselBit(h, 0xA0A0A0A0, 16, 32); svPutPartselBit(h, 0xB0B0, 48, 16);
svPutPartselBit(h, 0x55667788, 64, 32); svPutPartselBit(h, 0x1122, 96, 16);
}
LOG etype c=00000800
LOG make_hdr dst=112255667788 src=b0b0a0a0a0a0 etype=86dd
Unpacked structs are different: the LRM allows them as DPI arguments when every member is a DPI-compatible type, mapped to a C struct with the same members (and the C compiler's padding). Verilator does not implement it:
%Error-UNSUPPORTED: structs.sv:10:50: Unsupported: DPI argument of type REFDTYPE 'ups_t'
Use a packed struct (a bit vector), or pass the members as separate arguments. Classes never cross: Spear's §12.6 already says to pack a class into a struct and unpack it on return (Sample 12.29's pack()/unpack() methods); that remains the pattern, and in UVM the pack_ints/unpack_ints field automation does the same job.
0.9Strings: in, out, returned (Samples 12.30–12.31)
An SV string is a dynamic object in the simulator's heap; C sees it as a NUL-terminated const char*. The generated prototypes tell the whole story:
extern int c_strlen(const char* s); // input string
extern const char* hex_static(int v); // function string
extern void name_of(int code, const char** name); // output string
extern void upcase(const char** s); // inout string
The ownership rules from §35.5.1.4 / H.6.6 (memory) and H.8.10 (strings), which every example in the lab follows:
- input: the pointer belongs to SV and is valid only during the call. C must not free it, must not write through it, and must copy the text if it needs it after returning (
keep()usesstrdup; SV then reassigns its variable andkept()still returnsfirst). - returned and output: C provides the pointer and keeps owning the memory; SV copies the characters into its own string and never frees C's pointer. So C must return memory that is still alive after the function returns: a string literal, a
staticbuffer, or the heap. Never a local array. - inout: C reads
*s(SV's text) and, to change it, stores a new pointer in*s; SV copies from there. C never writes into SV's buffer.
const char* hex_static(int v) { // Sample 12.30: a static buffer is alive after return
static char s[12];
snprintf(s, sizeof s, "%08x", v);
return s;
}
void name_of(int code, const char** name) { // output: point at long-lived storage
static const char* names[] = {"IDLE", "BUSY", "DONE"};
*name = (code >= 0 && code < 3) ? names[code] : "???";
}
Spear worries (before Sample 12.31) that a static buffer breaks when one statement calls the function twice, e.g. $display(print(a), print(b)): if the simulator kept the pointers instead of copying, both would show the second result. His fix is a ring of buffers. Verilator copies a returned string into a std::string immediately, so even the single static buffer works there:
LOG static_two a=00001111 b=00002222 <- two calls in one $display, one static buffer
LOG ring a=00000001 b=00000002 <- Sample 12.31's ring buffer, same result
Do not rely on that if the code has to run elsewhere: when the copy happens is an implementation detail, Spear wrote Sample 12.31 because he met simulators where it mattered, and a ring of N buffers costs nothing. The real trap is the local array:
const char* to_hex(int v) { // ex/bug_str.c
char s[12]; // dies when the function returns
snprintf(s, sizeof s, "%08x", v);
return s;
}
clang catches it at build time: warning: address of stack memory associated with local variable 's' returned [-Wreturn-stack-address]. Verilator shows C compiler warnings only in its build log, so read it (or add -CFLAGS -Werror=return-stack-address).
0.10Verilator 5.052 vs the LRM, for aggregates
| Feature (IEEE 1800-2023 cl. 35 / Annex H) | Verilator 5.052 (measured) |
|---|---|
| Fixed-size unpacked arrays, all element types | yes |
| Open arrays from fixed-size actuals, 1–3 dims, all query functions | yes |
| Dynamic array / queue / slice as open-array actual | no (UNSUPPORTED / internal error) |
svGetArrayPtr, svSizeOfArray | yes; NULL / 0 if not C layout |
| Element storage of packed elements (canonical, H.7.3; else NULL pointers, H.12.4) | native: 1/2/4/8 bytes up to 64 bits, 32-bit words above, with non-NULL pointers (deviation) |
svGet/PutBitArrElem*VecVal, svGet/PutLogicArrElem*VecVal | yes (logic: bval always 0) |
4-state values (bval) | ignored on input to SV, 0 on output to C |
| Packed structs | yes (bit vector) |
| Unpacked structs | no (UNSUPPORTED REFDTYPE) |
| Strings: input / output / inout / return | yes; returned string copied at once |
output array, unwritten elements | open array: 0; fixed array: garbage |
The pattern that survives every simulator: open arrays of C-compatible element types (byte, int, longint, real) for bulk data, accessed with svLow/svHigh/svGetArrElemPtr1; packed vectors and packed structs for headers and wide words, accessed with canonical svBitVecVal words and part-selects; strings that C copies on the way in and returns from static or heap storage. Step 06 builds the golden model with exactly these pieces, and step 08 passes the same arrays to Python.
§1 · ObjectivesWhat you can do after this step
- Write the import and the C prototype for fixed-size arrays of any element type, and say what the C pointer points at (Fig. 0.2).
- Choose
input/inout/outputfor an array knowing exactly what C sees on entry and what SV gets back. - Write C code for an open array that works for any size, any range, any direction (
svLow…svHigh,svGetArrElemPtr*), and predict every query function for a given declaration. - Explain why a raw element pointer is unsafe for packed elements narrower than 33 bits in Verilator, and use the canonical
Get/Put…VecValcopies instead. - Pass packed structs of any width and extract fields by bit position; know that Verilator rejects unpacked structs.
- Pass strings in all directions without dangling pointers or leaks.
- Work around Verilator's missing support for dynamic arrays and queues as open-array actuals.
Prerequisites: 02 (canonical svBitVecVal/svLogicVecVal, type table) and 03 (C-side state and lifetimes).
§2 · Exercises12 graded exercises, with solutions
★ basic · ★★ intermediate · ★★★ real design. Every solution with code is in lab/ex/ and checked by check.py; the numbers quoted come from the run.
E1 ★Write. A C function ex1_sum that returns the sum (as longint) of an int open array of any size and range. Test it with int a10[10:1] (a10[i] = i) and int a37[3:7] (a37[i] = -i).
Show solution
import "DPI-C" function longint ex1_sum(input int a[]);
long long ex1_sum(const svOpenArrayHandle h) {
long long s = 0;
for (int i = svLow(h, 1); i <= svHigh(h, 1); i++) s += *(int*)svGetArrElemPtr1(h, i);
return s;
}
Result: a10=55 a37=-25. Lesson: longint maps to long long (int64); the loop never mentions 0 or the declared direction.
E2 ★Predict. bit [7:0] m[2:5][3:0] is passed to input bit [7:0] m[][]. What do svLeft, svRight, svLow, svHigh, svIncrement, svSize return for dimensions 1 and 2, and what are svDimensions and svSizeOfArray in Verilator?
Show solution
Dim 1 ([2:5]): left 2, right 5, low 2, high 5, increment −1 (left < right), size 4. Dim 2 ([3:0]): left 3, right 0, increment +1, size 4. svDimensions = 2 in Verilator (unpacked dims only). H.12.2 says the queries follow the semantics of §20.7, where $dimensions counts packed and unpacked dimensions (3 here), with the packed part as dimension 0; the LRM is ambiguous, so do not hard-code either answer. svSizeOfArray = 16: 16 elements × 1 byte, because Verilator stores bit [7:0] natively in a byte (0.7). With canonical words it would have been 64. All measured (LOG ex2).
E3 ★Ex. 12.7. For the code of Samples 12.24 and 12.25 (int a[6:1][8:3]), what do svLeft(h,1), svLeft(h,2), svRight(h,1), svRight(h,2), svSize(h,1), svSize(h,2), svDimensions(h), svSizeOfArray(h) return?
Show solution
6, 8, 1, 3, 6, 6, 2, 144. The last one is $36 \times 4$ bytes; Verilator reports it because int is a C type and the array is contiguous (on a simulator that does not use C layout it could legitimately be 0). Measured in openarr.log (LOG query_a).
E4 ★★Find the bug. This works for int a[10] and crashes for int a[10:1]. Why, and what is the minimal fix?
int sum_bug(const svOpenArrayHandle h) {
int s = 0;
for (int i = 0; i < svSize(h, 1); i++) s += *(int*)svGetArrElemPtr1(h, i);
return s;
}
Show solution
svGetArrElemPtr1 takes an SV index. For [10:1], index 0 does not exist, the function returns NULL and the dereference segfaults. In the lab the process exits with code 139 before the $display (ex/bug_loop). Fix: for (int i = svLow(h,1); i <= svHigh(h,1); i++). Note that svSize was right; only the starting point was wrong.
E5 ★★Write. ex5_hist(input byte data[], output int hist[]): hist has 16 bins and counts the high nibble of each byte, treating bytes as unsigned. Check against an SV computation over 64 bytes data[i] = byte'(i*37).
Show solution
void ex5_hist(const svOpenArrayHandle data, const svOpenArrayHandle hist) {
for (int b = svLow(hist, 1); b <= svHigh(hist, 1); b++) *(int*)svGetArrElemPtr1(hist, b) = 0;
for (int i = svLow(data, 1); i <= svHigh(data, 1); i++) {
unsigned char v = *(unsigned char*)svGetArrElemPtr1(data, i); // byte is SIGNED char
(*(int*)svGetArrElemPtr1(hist, v >> 4))++;
}
}
Result: match=1 bin0=6 bin15=3. Two lessons. (1) hist is output: C must write every element, so it zeroes the bins first (0.2). (2) SV byte is signed; read it as char and v >> 4 is negative for half the values, an out-of-range index. Cast to unsigned char. (The bin index v >> 4 is used as an SV index directly, which is valid only because hist is declared [16], i.e. [0:15]; for a general range add svLow(hist,1).)
E6 ★★Predict. bit [7:0] b8[4] = '{8'h11, 8'h22, 8'h33, 8'h44} is passed to input bit [7:0] b[], and C returns *(svBitVecVal*)svGetArrElemPtr1(b, 0). What does SV print with %h on Verilator? What should portable code do?
Show solution
44332211. Verilator stores each bit [7:0] element in one byte; reading a 32-bit word at element 0 picks up four elements, little-endian. Portable: svBitVecVal w; svGetBitArrElem1VecVal(&w, b, 0); gives 0x11 on every simulator (or declare the formal as byte b[], a C type, and read chars). Measured: LOG raw8 elem0=44332211.
E7 ★★Write. For eth_hdr_t of 0.8, write ex7_swap_macs(inout eth_hdr_t h) that exchanges dst and src and leaves etype alone. No C struct overlays.
Show solution
void ex7_swap_macs(svBitVecVal* h) { // dst = [111:64], src = [63:16]
svBitVecVal dl, dh, sl, shi;
svGetPartselBit(&dl, h, 64, 32); svGetPartselBit(&dh, h, 96, 16);
svGetPartselBit(&sl, h, 16, 32); svGetPartselBit(&shi, h, 48, 16);
svPutPartselBit(h, sl, 64, 32); svPutPartselBit(h, shi, 96, 16);
svPutPartselBit(h, dl, 16, 32); svPutPartselBit(h, dh, 48, 16);
}
With dst=aaaaaaaaaaaa src=123456789abc etype=0806 the result is dst=123456789abc src=aaaaaaaaaaaa etype=0806. Lesson: part-selects are limited to 32 bits, so a 48-bit field moves in two pieces; the positions come straight from the SV declaration (last member at bit 0).
E8 ★★Find the bug. Why does this sometimes print garbage, and how would you catch it before running?
const char* to_hex(int v) {
char s[12];
snprintf(s, sizeof s, "%08x", v);
return s;
}
Show solution
s lives in the function's stack frame, which is released at return; SV copies the string after that, from memory the next call may overwrite. It is undefined behavior: it may even "work" in a small test. clang flags it at compile time and the warning lands in Verilator's build log: address of stack memory associated with local variable 's' returned [-Wreturn-stack-address] (checked). Fix: static char s[12]; (Sample 12.30) or a ring of static buffers (Sample 12.31).
E9 ★★Write (Verilator workaround). Your model is int c_sum(input int a[]), but your data is a dynamic array int d[] of arbitrary length, and Verilator rejects it as an actual. Write an SV wrapper sum_dyn(const ref int d[]) that feeds the model through a fixed buffer of 256 elements. Test with 1000 elements d[i] = i.
Show solution
import "DPI-C" function int ex9_sum_n(input int chunk[256], input int n);
function automatic longint sum_dyn(const ref int d[]);
int chunk[256];
longint s = 0;
for (int base = 0; base < d.size(); base += 256) begin
int n = (d.size() - base < 256) ? d.size() - base : 256;
for (int k = 0; k < n; k++) chunk[k] = d[base + k];
s += ex9_sum_n(chunk, n);
end
return s;
endfunction
Result: sum=499500 expect=499500. Lessons: the count must travel separately because a fixed formal carries no size; and do not name the buffer buf: it is a Verilog keyword (gate primitive), a syntax error the lab hit on the first try.
E10 ★★★Write. int ex10_matmul(input int a[][], input int b[][], output int c[][]) computes c = a × b for arbitrary ranges and returns −1 if the shapes do not match. Test with int ma[1:2][0:2] (i+j), int mb[0:2][5:4] (i−j), int mc[2][2]. Then explain why calling it with a mismatching b and the same mc "erases" the previous result.
Show solution
int ex10_matmul(const svOpenArrayHandle a, const svOpenArrayHandle b, const svOpenArrayHandle c) {
int n = svSize(a, 1), k = svSize(a, 2), m = svSize(b, 2);
if (svSize(b, 1) != k || svSize(c, 1) != n || svSize(c, 2) != m) return -1;
for (int i = 0; i < n; i++) // 0-based OFFSETS, converted below
for (int j = 0; j < m; j++) {
int s = 0;
for (int t = 0; t < k; t++)
s += *(int*)svGetArrElemPtr2(a, svLow(a, 1) + i, svLow(a, 2) + t) *
*(int*)svGetArrElemPtr2(b, svLow(b, 1) + t, svLow(b, 2) + j);
*(int*)svGetArrElemPtr2(c, svLow(c, 1) + i, svLow(c, 2) + j) = s;
}
return 0;
}
Result: c00=-16 c01=-22 c10=-25 c11=-34, bad_rc=-1. With three arrays of different ranges the clean way is to loop over 0-based offsets and add each array's own svLow. Note that "column 0" of b is SV index 4 (its low index), not 5 (its left index).
The erase: c is output. Even when the function returns −1 without touching it, the simulator copies the whole temporary back, and in Verilator that temporary is zero-filled (0.2). The lab passes a separate scratch array to the failing call. Either do that or declare c as inout.
A second lesson the lab taught by accident: the first version declared int rc = ex10_matmul(ma, mb, mc); inside a begin block of an initial. A variable declaration with initializer in a static context is initialized once at time 0, before the foreach loops filled ma/mb, so the model multiplied zeros. Declare first, assign in a statement.
E11 ★★★Write. ex11_xor(input bit [127:0] w[], output bit [127:0] x): XOR-reduce an open array of 128-bit words, portable to any simulator.
Show solution
void ex11_xor(const svOpenArrayHandle w, svBitVecVal* x) { // x: 4 canonical words
svBitVecVal e[4];
memset(x, 0, 4 * sizeof(svBitVecVal));
for (int i = svLow(w, 1); i <= svHigh(w, 1); i++) {
svGetBitArrElem1VecVal(e, w, i); // canonical copy, never a raw pointer
for (int k = 0; k < 4; k++) x[k] ^= e[k];
}
}
Matches SV's own XOR (match=1). For a scalar packed output the C side gets a pointer to ceil(128/32) = 4 words; write all of them.
E12 ★★Ex. 12.1, on arrays. Spear's Ex. 12.1 shifts one 32-bit value left for n > 0, right for n < 0, not at all for n = 0. Write ex12_shift_all(inout int unsigned v[], input int n) that applies it to every element. What should happen for n = 40?
Show solution
void ex12_shift_all(const svOpenArrayHandle v, int n) {
for (int i = svLow(v, 1); i <= svHigh(v, 1); i++) {
unsigned* p = (unsigned*)svGetArrElemPtr1(v, i);
if (n >= 32 || n <= -32) *p = 0; // C: shifting by >= the width is UB
else if (n > 0) *p <<= n;
else if (n < 0) *p >>= -n;
}
}
From {80000001, f, 1, ffffffff}: n=4 → 00000010,000000f0,00000010,fffffff0; then n=-8 → 00000000,00000000,00000000,00ffffff; then n=40 → 0. Lesson: SV defines x << 40 on 32 bits as 0; C does not define it at all (x86 masks the count to 5 bits, so it would shift by 8). A golden model must reproduce the SV semantics explicitly, and the type must be unsigned so >> is logical.
§3 · Quiz14 questions, self-grading
1. import "DPI-C" function void f(output bit [31:0] d[20]); What is the C parameter type?
bit [31:0] element is one canonical word. Not const, because it is output. A handle appears only for [] formals; C has no array-by-value parameters.2. A C function receives output int c[] and writes only c[1]. SV had '{5,5,5} before the call. What does SV hold after, in Verilator?
output means the whole array is the result. Verilator passes a zero-filled temporary and copies all of it back (measured). '{5,7,5} is what inout gives. The LRM leaves the unwritten values undefined, so do not rely on the zeros either.3. For int a[6:1], which loop is correct in C?
i <= 1: the body never runs. (d) never runs either.4. What does svIncrement(h,1) return for int a[6:1]?
[6:1], −1 for [0:2].5. Verilator stores int v[10:1] with v[i] = i. What is at offset 0 of svGetArrayPtr(h)?
v0=1: low index first, regardless of declared direction, which is what H.7.3/H.7.6 require of every tool. svGetArrElemPtr1(h, i) with SV indices is still the safest way. svGetArrayPtr is non-NULL for C-layout arrays like int.6. Which actual can you pass to input int a[] in Verilator 5.052?
%Error-UNSUPPORTED; a slice gives an internal error. §35.6.1 permits dynamic arrays (and, through §7.7, queues); commercial simulators accept them.7. For input bit [7:0] b[], the C code does *(svBitVecVal*)svGetArrElemPtr1(b, 0). Why is it wrong in Verilator?
bit [7:0] element in 1 byte (measured 44332211) and still hands out a pointer to it. The LRM wants canonical form for packed open-array elements (H.7.3) or a NULL pointer when the storage differs (H.12.4), so (d) describes the standard, not Verilator: this is a Verilator deviation. The portable path is the copy made by svGet/PutBitArrElem*VecVal.8. typedef struct packed { bit [7:0] r, g, b; } RGB_T; crosses as svBitVecVal* v. Which bits hold r?
r is [23:16]. On little-endian x86 that is byte 2 in memory (why the book's C struct lists b, g, r); on big-endian it would be byte 1. Bit positions are host-independent; byte positions are not.9. Which way of passing struct { int a; byte b; shortint c; } (unpacked) works in Verilator 5.052?
Unsupported: DPI argument of type REFDTYPE 'ups_t'. A chandle is a C pointer SV cannot dereference; it does not carry SV data.10. An imported function receives input string s and stores the const char* in a C global to use in the next call. What is wrong?
11. Which return statement is safe for import "DPI-C" function string f();?
12. import "DPI-C" function void upcase(inout string s); How does C change the string?
const char**: the text is const. C points *s at its own storage and SV copies from there after return. Writing in place or freeing SV's buffer corrupts the simulator's heap.13. A C model writes data[i].aval but leaves bval uninitialized for a logic [31:0] data[20] output. What do you see?
bval = 0xFFFFFFFF Verilator still shows the aval value and $isunknown = 0. A 4-state simulator decodes the garbage bval as X/Z. Bugs that hide on Verilator are the worst kind; write bval, or use bit.14. Which formal accepts bit [1:0][0:3][6:-1] bpack[9:1]?
longint is signed, the packed actual is unsigned, so the types differ.§4 · LabEverything above, measured
Directory 04_arrays_structs_strings/lab/. One self-checking testbench per topic, top module top, Verilator 5.052 (--binary --timing, flags in ../../common/lab.sh).
cd tutorial/04_arrays_structs_strings/lab
python3 check.py # builds everything in parallel, runs, asserts (69 checks)
python3 check.py --no-run # re-check the logs in build/ only
Predict before you run: (1) the eleven query values for int up[0:2][0:3]; (2) whether static_two prints two different strings; (3) the bytes per element for bit [12:0]; (4) which of the four kinds builds fail.
| Experiment | Files | What to look at | Result (Verilator 5.052) |
|---|---|---|---|
| Fixed arrays | fib.sv/.c | build/vl_fib/Vtop__Dpi.h, bval ignored | d19=6765; fib4_bad unknown=0 |
| Open arrays | openarr.sv/.c | query table, flat order, NULL on bad index or dims | bytes=144; v0=1; 2 run-time warnings |
| Actual kinds | kinds.sv/.c | build/k_*.build | fixed OK; dyn, queue, slice fail |
| Packed elements | packed.sv/.c | layout, raw pointer vs VecVal | 1/2/8/16 bytes; raw8=44332211 |
| Structs | structs.sv/.c | overlay vs part-select; s_ups.build | both invert; unpacked UNSUPPORTED |
| Strings | strings.sv/.c | prototypes, static vs ring, strdup | all pass; returned string copied at once |
| Directions | outdir.sv/.c | unwritten elements | out=0,7,0 io=5,7,5 |
| Exercises | ex/* | ex.log, bug_loop.log, bug_str.build | E1–E12 as quoted; bug_loop rc 139 |
Things worth opening by hand: build/vl_strings/Vtop__Dpi.h (the string prototypes), build/k_dyn.build (the unsupported message), and build/bug_str.build (clang's warning, buried in the make output).
§5 · Cheat sheetOne table, ten rules
| SV formal | C parameter | Access | Pitfall |
|---|---|---|---|
| input int v[8] | const int* | v[k], k = 0..7 | size is a contract, not checked |
| output bit [31:0] d[20] | svBitVecVal* | d[k] | write every element |
| output logic [31:0] d[20] | svLogicVecVal* | d[k].aval/.bval | forgetting bval hides on Verilator |
| input byte b[4] | const char* | b[k] | signed: cast to unsigned char |
| inout int a[][] | svOpenArrayHandle | svGetArrElemPtr2(h,i,j), SV indices | 0-based loop → NULL |
| input bit [N-1:0] w[] | svOpenArrayHandle | svGetBitArrElem1VecVal(e,h,i) | raw pointer wrong if N ≤ 32 (Verilator) |
| inout struct packed … | svBitVecVal* | svGet/PutPartselBit, shifts | C struct overlay: endian, word straddle |
| input string s | const char* | read during call | copy to keep |
| function string f | const char* f(…) | return static/heap | never a local array |
| output string s | const char** | *s = ptr | SV copies, never frees |
| inout string s | const char** | read *s, assign new ptr | no in-place writes |
- SV owns all argument storage; C borrows it until return.
- Read the prototype from
V<top>__Dpi.h, and#includeit. output= C writes everything; otherwiseinout.- Open-array loops run
svLow…<= svHighon SV indices. svIncrementis not a loop step.- Memory order of unpacked arrays is the simulator's business: use
svGetArrElemPtr*unless you checkedsvGetArrayPtr. - Packed elements: canonical copies (
…VecVal), not raw pointers. - Packed structs: bit positions, first member = MSBs.
- Verilator: no dynamic arrays, queues, slices or unpacked structs as DPI arguments; no 4-state.
- Strings: copy what comes in, return what stays alive.