C++ Part II · Module 28 · C/C++ and SystemVerilog DPI
Arrays Across the Boundary
SystemVerilog has two kinds of array that look similar in a declaration and are completely different things in memory, and the DPI mapping treats them completely differently too. An unpacked array is a collection of elements and arrives in C as a pointer to those elements. A packed array is a single bit vector that happens to be written with a range, and arrives as an array of thirty-two-bit chunks with the value spread across them. Getting that first question wrong means the C side is reading the right memory with the wrong interpretation, and the measurement in this chapter shows how forgiving that can look: an array of four integers read as eight short integers returned exactly the correct sum, because the padding bytes in the data happened to be zeros. The same wrong code returned a wrong answer for a different element count — so whether the bug is visible is a property of the data rather than of the code, which is the least convenient property a bug can have.
Expert20 min readC++Part IIVerificationDPI
Part II · Module 28 · Chapter 28.8 · Arrays Across the Boundary
1. The Question That Decides Everything
Two SystemVerilog declarations that look almost the same:
bit [31:0] packed_vector; // ONE value, 32 bits wide
bit unpacked_array [32]; // THIRTY-TWO values, each 1 bit
int unpacked_ints [4]; // FOUR values, each 32 bitsThe dimensions written before the name are packed; those after it are unpacked. That is the whole syntactic rule, and it decides everything downstream:
| packed | unpacked | |
|---|---|---|
| what it is | one value, a bit vector | a collection of values |
| arithmetic on the whole thing | yes — it is an integer | no |
| how DPI passes it | an array of 32-bit chunks | a pointer to the elements |
| the C type | svBitVecVal* / const svBitVecVal* | const int*, const unsigned char*, … |
| indexing in C | by bit position within chunks | by element, as any C array |
So the first question for any array argument is not "what is the element type" but "is this one value or many?" — and the answer changes the C declaration completely rather than slightly.
2. Unpacked Arrays: a Pointer to Elements
The easy and common case. A SystemVerilog int a[4] arrives as a pointer to four C ints, contiguous, with the element stride you would expect:
A. an unpacked array of int, as C sees it
element size 4, total 16 bytes, stride between [0] and [1] = 4
sum over 4 ints = 10 (expected 10)Which makes the C side unremarkable:
extern "C" int sum_unpacked(const int* a, unsigned n) // SV: int a[4]
{
int s = 0; for (unsigned i = 0; i < n; ++i) s += a[i]; return s;
}Three things about that declaration are load-bearing.
const, because the SystemVerilog direction is input — Chapter 28.3 §4's only enforceable direction check.
The element type must be the exact mapped type from Chapter 28.7 §2. const int* for int a[], const unsigned char* for byte unsigned a[]. §3 measures what the wrong one does.
And the length is a separate argument, because a C pointer carries no size. That is §5's contract, and it is the part that has no language support on either side.
For a fixed-size unpacked array the mapping is this simple. Chapter 28.9 is the case where the size is not fixed at compile time, which needs an entirely different mechanism.
3. The Wrong Element Type
Here is the measurement that makes this chapter worth its length. The same array of four ints — {1, 2, 3, 4} — read through a const short* instead:
B. the same memory read with the wrong element type
sum over 8 shorts = 10 <-- no error, no warning, wrong answer
sum over 4 shorts = 3 <-- reads only the first two intsThe first row returned 10, which is the correct answer.
Work out why, because the reason is the lesson. On a little-endian machine the four 32-bit values {1, 2, 3, 4} occupy sixteen bytes which, read as eight 16-bit values, are {1, 0, 2, 0, 3, 0, 4, 0} — the low half of each integer followed by its zero high half. Summing all eight gives 1 + 0 + 2 + 0 + 3 + 0 + 4 + 0 = 10, identical to the correct sum.
So for this data the bug is undetectable. Not hard to detect — undetectable, because the output is bit-for-bit what a correct program produces.
And the second row shows the same code failing: summing only four shorts covers just the first two integers, giving 1 + 0 + 2 + 0 = 3. One element count reveals the bug and another conceals it completely, with no change to the code.
Three conclusions.
The visibility of an array-layout bug is a property of the data. Small positive values with zero high halves hide it; large values, negative values, or any value using the full width expose it immediately. This is the same shape as Chapter 28.7 §4's truncation being invisible for small inputs, and it has the same remedy.
The element count interacts with it. The wrong element type also changes how many elements the array has — sixteen bytes is four ints or eight shorts — so a length argument computed on one side and consumed on the other can be right, half, or double.
And nothing anywhere objects. No compiler warning, no runtime error, no sanitizer finding — the read is within the allocation, correctly aligned, and perfectly well-defined C++. It is simply not what the testbench meant.
4. Packed Arrays: Chunks
A packed vector wider than 64 bits has no C scalar, so it crosses as an array of 32-bit chunks, low bits first:
D. a packed array is a bit vector, not an element array
chunk[0]=0x89ABCDEF (bits 31:0) chunk[1]=0x01234567 (bits 63:32)
assembled 64-bit value = 0x0123456789ABCDEF
bit 0 = 1, bit 63 = 0
reading chunk[0] as if it were the HIGH half gives 0x89ABCDEF01234567chunk[0] holds bits 31:0 — the least significant end — and assembling the pair gives 0x0123456789ABCDEF. The last line is the trap: treating chunk[0] as the high half gives 0x89ABCDEF01234567, a completely different value that is just as plausible to look at.
Two more facts the measurement supplies.
The chunk count is (width + 31) / 32, and for a width that is not a multiple of 32 the top chunk's spare bits are not yours — Chapter 28.7 §6's masking rule.
And the byte order inside a chunk is the host's:
E. byte order inside one chunk is the host's
0x89ABCDEF in memory: EF CD AB 89Which matters the moment you memcpy a chunk array into a byte buffer or compare it against bytes from a file. The chunk ordering is defined by DPI; the byte ordering within each chunk is defined by the machine, and the two are independent. Chapter 27.9 §4's rule applies unchanged: write the bytes individually and the question disappears.
The practical advice for packed vectors is short. Up to 64 bits, declare a scalar — longint unsigned to unsigned long long — and skip chunks entirely. Above that, write one helper that assembles and one that disassembles, test them against known values, and never open-code the shifts at a call site.
5. The Size Contract
A C pointer carries no length. So for every array argument, the size is a separate contract that neither language enforces, and there are three ways to state it.
| how | what it looks like | risk |
|---|---|---|
| a length argument | f(const int* a, unsigned n) | the two can disagree |
| a compile-time constant both sides use | f(const int a[4]) plus a shared constant | drifts when one side changes |
| the open-array API | the handle carries its own bounds | none — Chapter 28.9 |
The length argument is the usual choice and it is a second thing to get wrong. It can be the element count or the byte count; it can be computed from $size() on one side and a literal on the other; and when it is too large the C side reads past the array, which is a real out-of-bounds read that Chapter 25.3 measured the consequences of.
Two habits that make it survivable.
Name the unit in the parameter. unsigned nElems rather than unsigned len, because len is exactly the word that gets read as bytes by one side and elements by the other.
And pass the size from the SystemVerilog side rather than hard-coding it in C. $size(payload) at the call site is one place that tracks the declaration; a 4 in the C file is a second place that does not.
The strongest answer is the third row: if the size can vary, use an open array and let the handle carry its own bounds, which removes the contract entirely. That is Chapter 28.9's subject, and the size contract is the main reason it exists.
6. Two Dimensions
A 2D unpacked array arrives as C expects it, row-major and contiguous:
C. a two-dimensional unpacked array is row-major in C
bytes in order: 1 2 3 4 5 6
so m[1][0] is at flat index 3, not 1For int m[2][3] holding {{1,2,3},{4,5,6}} the flat order is 1 2 3 4 5 6, so m[1][0] is at flat index 3 — row stride 3, not column stride 2.
The failure is transposition, and it is quiet. A C side that computes flat[col * rows + row] instead of flat[row * cols + col] reads valid elements in the wrong order, so a checksum over the whole array still matches while any element-wise comparison fails — and for a square array with symmetric data it matches entirely. The same data-dependent visibility as §3.
The defensive habit: index through one helper rather than at every call site.
static inline std::size_t at(std::size_t row, std::size_t col, std::size_t cols)
{ return row * cols + col; }One place to get right, one place to test, and the cols argument makes the stride explicit rather than implied.
7. The Failure Modes
| symptom | likely cause |
|---|---|
| the answer is right for small values and wrong for large ones | wrong element type — §3 |
| the answer is right for a checksum and wrong element-wise | transposition — §6 |
| the value is right but with its halves exchanged | chunk order — §4 |
| the C side reads garbage past the end | the length argument is too large — §5 |
| half the array is processed | the length is in the wrong unit — §5 |
| the top bits of the last element are noise | the width is not a multiple of 32 — §4 |
| the C side modified the testbench's array | missing const on an input — §2 |
The first row is the diagnostic worth internalising, because it is the signature of every layout bug in the chapter: correct for small, simple, symmetric data and wrong otherwise. If a test passes with {1,2,3,4} and fails with real traffic, suspect the layout before the algorithm.
8. The Table
| the question | the answer | measured |
|---|---|---|
| the first question about an array argument | packed or unpacked — one value or many | §1 |
| which dimensions are packed | those written before the name | §1 |
| how does an unpacked array arrive | a pointer to the elements, contiguous | §2 |
| what was the measured stride | 4 bytes for int, 16 bytes total for four | §2 |
| what does the wrong element type give | 10 — the correct answer for this data | §3 |
| and for a different count | 3, reading only the first two ints | §3 |
| does anything object | no — well-defined C++, in bounds, aligned | §3 |
| how does a packed vector arrive | 32-bit chunks, chunk[0] is bits 31:0 | §4 |
| what does swapping the halves give | 0x89ABCDEF01234567 instead of 0x0123456789ABCDEF | §4 |
| is byte order within a chunk defined by DPI | no — it is the host's: EF CD AB 89 | §4 |
| who enforces the array size | nobody — it is a contract in a second argument | §5 |
| 2D layout | row-major; m[1][0] is flat index 3 | §6 |
| why do these bugs hide | their visibility is a property of the data | §6 callout |
| what fixture exposes them | distinct, full-width, signed, non-square | §6 callout |
9. What Actually to Do
- Ask packed or unpacked first. It changes the C declaration completely.
- Declare the exact mapped element type from Chapter 28.7 §2, with
constfor aninput. - Pass the length from the SystemVerilog side —
$size(...)— rather than hard-coding it in C. - Name the unit in the parameter:
nElemsornBytes, neverlen. - Use a 64-bit scalar for any packed vector up to 64 bits and avoid chunks entirely.
- Write one assemble and one disassemble helper for wider vectors, tested against known values.
- Mask the top chunk when the width is not a multiple of 32.
- Index 2D arrays through one helper that takes the stride explicitly.
- Choose fixture data with no symmetry: every element distinct, full width, at least one negative, non-square.
- Suspect layout before algorithm when a test passes with small simple values and fails with real traffic.
- Prefer an open array when the size can vary, so the handle carries its own bounds.
10. Mistakes Worth Making Once
Treating a packed vector as an element array. It is one value, not many.
Reading an int array through a short*. Measured: the correct answer, for that data.
Testing an array interface with {1, 2, 3, 4}. It passes every layout bug in this chapter.
Hard-coding the length in C. A second place that does not track the declaration.
A parameter called len. Bytes on one side, elements on the other.
Assembling chunks with the halves exchanged. A different, equally plausible value.
Assuming DPI defines the byte order inside a chunk. It is the host's.
Summing the whole top chunk of a bit [11:0]. Twenty bits are not yours.
Transposing a 2D index. A matching checksum and a failing element-wise comparison.
Omitting const on an input array. The model may now modify the simulator's storage.
11. Interview Relevance
The frozen curriculum marks this Critical, and the question is usually how do you pass an array through DPI? — which has a trap in it, because the right first move is to ask a question back.
Ask whether the array is packed or unpacked. Packed dimensions are written before the name and the array is one value, a bit vector, which crosses as an array of 32-bit chunks with chunk[0] holding bits 31:0. Unpacked dimensions are written after the name, the array is a collection, and it crosses as a plain pointer to the elements. Those are different C declarations, not variations on one.
Then the two things with no language support. The element type must be the exact mapped type, and the size is a separate contract that neither side enforces — so pass it from the SystemVerilog side with $size() and name the unit in the parameter.
Then the measurement, which is the memorable part. An array of four ints read through a short* returned 10 — exactly the correct sum — because on a little-endian machine {1,2,3,4} as shorts is {1,0,2,0,3,0,4,0}. The same wrong code returned 3 for a different element count. So whether an array-layout bug is visible is a property of the data, which means a fixture of small positive values passes all of them, stably, however many times you run it.
And the conclusion a senior candidate draws is about the fixture rather than the code: use data with every element distinct, every element using the full width, at least one negative value, and a non-square shape — because then a transposition, a wrong element type and a chunk swap all become loud. Finishing with the diagnostic — correct for small simple data and wrong for real traffic means suspect the layout before the algorithm — shows you have debugged one.
12. What You Should Be Able to Say Now
- That the first question is packed or unpacked, that packed dimensions are written before the name, and that the two cross completely differently.
- That an unpacked array arrives as a pointer to the elements — measured stride 4 bytes for
int, 16 bytes for four — withconstfor aninput. - That a packed vector arrives as 32-bit chunks with
chunk[0]holding bits 31:0, that the chunk count is(width + 31) / 32, and that swapping the halves gives0x89ABCDEF01234567instead of0x0123456789ABCDEF. - That byte order inside a chunk is the host's (
EF CD AB 89), not DPI's. - That reading four
ints as eightshorts returned 10, the correct answer, and as fourshorts returned 3 — so the bug's visibility is a property of the data. - That nothing objects to the wrong element type: the read is in bounds, aligned and well-defined.
- That nobody enforces the array size — it is a separate argument, best passed from SystemVerilog with
$size(), with the unit in the parameter's name. - That a 2D unpacked array is row-major, so
m[1][0]is at flat index 3, and that a transposition passes a checksum while failing element-wise. - That a fixture of distinct, full-width, signed, non-square data makes every failure in the chapter loud, and that
{1,2,3,4}makes them all invisible. - That the diagnostic signature is correct for small simple values, wrong for real traffic.
13. Exercises
Exercise 1 — the two declarations. Write a packed and an unpacked SystemVerilog array of the same total width and write the C parameter each would need.
Exercise 2 — the stride. Print the element size, total size and the byte distance between two elements of a C array.
Exercise 3 — reproduce the hidden bug. Sum four ints as eight shorts and confirm you get the correct answer. Then change one value to 70000 and confirm it breaks.
Exercise 4 — the element count. Sum the same memory as four shorts and explain the result from the byte layout.
Exercise 5 — the chunks. Assemble a 64-bit value from two 32-bit chunks, then swap the halves and compare. Then dump one chunk's bytes.
Exercise 6 — the transposition. Build a 2 by 3 array, index it both ways, and find data for which a checksum still matches.
Exercise 7 — the fixture. Write the four-element array from §6's callout and confirm it exposes a wrong element type, a chunk swap and a transposition.
14. Summary
- The first question about any array argument is packed or unpacked — one value or many — because the two cross as completely different C types. Packed dimensions are written before the name.
- An unpacked array is a pointer to contiguous elements, measured at a 4-byte stride for
intand 16 bytes for four of them, declaredconstfor aninput, with the element type exactly as Chapter 28.7 §2 specifies. - Measured: reading those four
ints through ashort*returned 10 — the correct answer — because{1,2,3,4}little-endian is{1,0,2,0,3,0,4,0}as shorts. The same code returned 3 for a different element count. - So nothing objects and the data decides whether the bug is visible: the read is in bounds, correctly aligned and well-defined C++, and small positive values conceal it permanently.
- A packed vector wider than 64 bits crosses as 32-bit chunks with
chunk[0]holding bits 31:0; swapping the halves yields a different, equally plausible value, and byte order inside a chunk is the host's, not DPI's. - Nobody enforces the array size. Pass it from SystemVerilog with
$size(), name the unit in the parameter, and prefer an open array when the size can vary. - 2D unpacked arrays are row-major, so
m[1][0]is flat index 3 — and a transposition matches a whole-array checksum while failing element-wise. - The fixture is the fix. Distinct, full-width, signed, non-square data makes every layout failure loud;
{1, 2, 3, 4}makes all of them invisible however often you run it. - And the diagnostic signature to remember: correct for small simple values and wrong for real traffic means suspect the layout, not the algorithm.
Chapter 28.9 removes the size contract entirely, by letting the array carry its own bounds.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
