Skip to content
VLSI Mentor

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:

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

The dimensions written before the name are packed; those after it are unpacked. That is the whole syntactic rule, and it decides everything downstream:

packedunpacked
what it isone value, a bit vectora collection of values
arithmetic on the whole thingyes — it is an integerno
how DPI passes itan array of 32-bit chunksa pointer to the elements
the C typesvBitVecVal* / const svBitVecVal*const int*, const unsigned char*, …
indexing in Cby bit position within chunksby 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:

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

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

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

The 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:

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

chunk[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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
E. byte order inside one chunk is the host's
   0x89ABCDEF in memory: EF CD AB 89

Which 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.

howwhat it looks likerisk
a length argumentf(const int* a, unsigned n)the two can disagree
a compile-time constant both sides usef(const int a[4]) plus a shared constantdrifts when one side changes
the open-array APIthe handle carries its own boundsnone — 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:

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

For 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.

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

symptomlikely cause
the answer is right for small values and wrong for large oneswrong element type — §3
the answer is right for a checksum and wrong element-wisetransposition — §6
the value is right but with its halves exchangedchunk order — §4
the C side reads garbage past the endthe length argument is too large — §5
half the array is processedthe length is in the wrong unit — §5
the top bits of the last element are noisethe width is not a multiple of 32 — §4
the C side modified the testbench's arraymissing 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 questionthe answermeasured
the first question about an array argumentpacked or unpacked — one value or many§1
which dimensions are packedthose written before the name§1
how does an unpacked array arrivea pointer to the elements, contiguous§2
what was the measured stride4 bytes for int, 16 bytes total for four§2
what does the wrong element type give10 — the correct answer for this data§3
and for a different count3, reading only the first two ints§3
does anything objectno — well-defined C++, in bounds, aligned§3
how does a packed vector arrive32-bit chunks, chunk[0] is bits 31:0§4
what does swapping the halves give0x89ABCDEF01234567 instead of 0x0123456789ABCDEF§4
is byte order within a chunk defined by DPIno — it is the host's: EF CD AB 89§4
who enforces the array sizenobody — it is a contract in a second argument§5
2D layoutrow-major; m[1][0] is flat index 3§6
why do these bugs hidetheir visibility is a property of the data§6 callout
what fixture exposes themdistinct, 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 const for an input.
  • Pass the length from the SystemVerilog side — $size(...) — rather than hard-coding it in C.
  • Name the unit in the parameter: nElems or nBytes, never len.
  • 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 — with const for an input.
  • 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 gives 0x89ABCDEF01234567 instead of 0x0123456789ABCDEF.
  • That byte order inside a chunk is the host's (EF CD AB 89), not DPI's.
  • That reading four ints as eight shorts returned 10, the correct answer, and as four shorts 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 int and 16 bytes for four of them, declared const for an input, with the element type exactly as Chapter 28.7 §2 specifies.
  • Measured: reading those four ints through a short* 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.