C Part II · Module 21 · Arrays and Open Arrays in DPI-C
Multidimensional Arrays Across the Boundary
C's layout for a nested array is fixed by the standard and measurable in three sizeof values. Whether that is what arrived is a different question, and getting it wrong does something no other bug in this module does: it permutes. Every value recovered is a genuine element, nothing goes out of bounds, and the multiset is unchanged — so a sum, a checksum over all elements, a minimum or a count agrees exactly. Measured here, a transposed read of a symmetric matrix is identical to the correct one, which means the most natural first test cannot tell the two layouts apart.
Expert23 min readCPart IIVerificationDPI-C
Part II · Module 21 · Chapter 21.6 · Multidimensional Arrays Across the Boundary
1. Two Questions, Not One
Chapter 21.5 settled one dimension's bounds by asking for them. A second dimension does not simply repeat the exercise, because it introduces a question a single dimension cannot pose: of the two indices, which one moves fastest through memory?
And that splits into two questions that must be kept apart.
What does C do? Fixed, specified, and measurable — §2.
What arrived? A separate question, answered by the declaration and the API, per dimension — §4.
The purpose of this chapter is worded carefully and so is this: "without assuming C row-major layout is what arrived". That is not a claim that the other side is column-major. It is an instruction not to skip the second question.
2. What C Guarantees
From the C standard, an array of arrays is contiguous and the last subscript varies fastest. Measured on a 3 × 4 array of uint32_t:
sizeof(a) = 48, sizeof(a[0]) = 16, sizeof(a[0][0]) = 4
flat order: 0 1 2 3 10 11 12 13 20 21 22 23
element [1][2] is at flat index 6, value 12
the LAST index moves fastest: a[r][c] is at r*4 + cThe three sizeof values are the layout stated as arithmetic: the whole array is 48 bytes, a row is 16, an element is 4, and 48 = 3 × 16. There is no padding between rows and no freedom for the implementation to arrange it otherwise.
So on the C side, a[r][c] is at flat index r * C + c, and that is a guarantee rather than a convention.
3. What a Wrong Assumption Produces
Read the same bytes as though the first index moved fastest — c * R + r. Measured:
recovered: 0 3 12 21 1 10 13 22 2 11 20 23
correct : 0 1 2 3 10 11 12 13 20 21 22 23
identical: NORow by row:
row 0: correct 0 1 2 3 | transposed read 0 3 12 21
row 1: correct 10 11 12 13 | transposed read 1 10 13 22
row 2: correct 20 21 22 23 | transposed read 2 11 20 23
every value is a real element; only the POSITIONS moved4. Why This Failure Is Different From Every Other One in the Module
Every array bug so far has produced wrong data. This one produces a permutation.
0, 3, 12, 21 are all genuine elements of the array. Nothing was read out of bounds, nothing was truncated, no value was fabricated, and the multiset of recovered values is exactly the multiset that was sent.
That has a direct, practical consequence worth stating in one line, because it tells you which of your existing checks could never have caught it:
Any check that does not depend on position will agree. A sum. A minimum or maximum. A count. A histogram. A checksum computed over all elements in storage order. An "all values are in range" assertion.
Only a position-sensitive check disagrees — comparing a[1][2] against an expected a[1][2], or a checksum that folds the index in with the value.
So if a model is transposing and your scoreboard compares aggregate statistics, the scoreboard is structurally incapable of noticing, and Chapter 18.2's point applies in its most literal form: the check cannot fail, so its passing carries no information about this.
5. Asking Instead of Assuming
Chapter 21.5's discipline generalises directly: the array tells you, per dimension.
For an open array, the API reports how many dimensions there are and the low and high index of each. Both facts are needed — Chapter 21.5 measured why low cannot be assumed for one dimension, and with two there are two lower bounds to not assume.
const int d0lo = oaLow(h, 0), d0hi = oaHigh(h, 0);
const int d1lo = oaLow(h, 1), d1hi = oaHigh(h, 1);
for (int i = d0lo; i <= d0hi; i++)
for (int j = d1lo; j <= d1hi; j++)
use(oaElemPtr(h, i, j)); /* the API computes the address */The address is computed by the accessor, not by you. That is the whole defence: the moment you write base + (i * ncols + j) * elem, you have asserted a major order, an element size and two index bases, and §3 measured what one wrong assertion out of those four produces.
For a fixed-size array, there is no API, so the layout is part of the contract Chapter 21.3 made you write down — and it needs the dimension order in it, not just the sizes:
/* DPI import. SystemVerilog side: int coef [0:2][0:3] (unpacked, 3 x 4)
Received flat, row-major: element [r][c] is at r*4 + c.
If the declaration's dimension ORDER changes, this file is wrong and
nothing will say so — §4: a transposed read permutes rather than corrupts. */And keep the dimension kinds straight. Chapter 21.2's distinction applies per dimension: a packed dimension is a bit range of one value and is handled with Chapter 20.6's chunks and Chapter 15.2's masks; an unpacked dimension is a collection of element objects. A declaration can have both, and they are not two instances of the same thing.
6. The Finding: The Test You Would Naturally Write Cannot Tell
Measured on a 3 × 3 array:
a 3x3 array: the transposed read agrees on the diagonal: yes
and on the whole array: NOThe diagonal always agrees, because [i][i] is a fixed point of transposition. So a test that checks only diagonal elements passes a transposed model.
Then the sharper one:
measured on a symmetric matrix: transposed read identical: YESIdentical. Not similar — every element. A transposed read of a symmetric matrix is the matrix.
And symmetric data is exactly what gets reached for in a first test: a correlation matrix, a distance table, a symmetric filter kernel, an identity, anything built from a[i][j] = f(i + j) or f(i * j). The most natural stimulus for a two-dimensional interface is the stimulus least able to discriminate between the two layouts.
This is the module's pattern for the third time. Chapter 21.2 §5 measured 8-bit elements making the packed and unpacked models byte-identical. Chapter 21.5 §6 measured [0:7] making the reflex loop correct. Here, symmetric data makes the two major orders indistinguishable. In all three the common case is not merely unlikely to fail — it is incapable of failing, and it supplies confirming evidence for an assumption nothing is checking.
7. The Misconception: They Are Both Just Nested Arrays
"A two-dimensional array is a block of contiguous elements on both sides. I can
memcpyit into a C array, or cast the pointer touint32_t (*)[4], and index it normally."
The premise is nearly right and the conclusion does not follow.
Contiguity is not the question; order is. Both layouts are contiguous blocks of the same twelve elements — that is why the memcpy succeeds and why every byte is valid afterwards. Measured, the bytes are all present and all genuine; what differs is which index reaches which one.
The cast compiles and runs. uint32_t (*)[4] over the right number of bytes is a well-formed access to memory you own. There is no overflow, no undefined behaviour and nothing for a sanitizer to report — Chapter 21.1 §6's boundary for the last time in this module.
The first element is always right, and so is the last. Measured, the transposed read's [0][0] is correct, and so is [2][3]. A spot check of "does the first value look right" confirms a transposed model.
And the aggregate checks all agree — §4. The sum, the count, the checksum over storage order.
So the belief is held up by four independent pieces of confirming evidence, and §6 adds a fifth whenever the test data is symmetric.
The corrected position:
Both are contiguous; neither of them tells you the order. C's row-major flattening is a guarantee about C, not a description of what arrived. Ask the array per dimension, let the accessor compute the address, and if you must flatten by hand, write the formula and the declaration it came from in a comment — because every check that would have caught you being wrong is a check you have to have chosen deliberately.
8. What You Should Be Able to Say Now
- Two questions, kept apart: what C does (specified) and what arrived (per declaration, per dimension).
- C is row-major by guarantee — measured,
sizeof48 / 16 / 4, anda[r][c]atr * C + c. - A wrong major order permutes — measured,
0 3 12 21where0 1 2 3belong, every value a genuine element, nothing out of bounds. - Any position-insensitive check agrees: sum, count, min, max, histogram, whole-array checksum. Only a position-sensitive check disagrees.
- The first and last elements are always right under transposition, so a spot check confirms the bug.
- Measured, a 3 × 3 transposed read agrees on the diagonal, so a diagonal-only test passes a transposed model.
- Measured, a transposed read of a symmetric matrix is identical — and symmetric data is the natural first stimulus.
- Third instance of the module's pattern: 21.2 §5 (8-bit elements), 21.5 §6 (
[0:7]), and here. The common case cannot fail. - Ask per dimension and let the accessor compute the address; writing
base + (i * n + j) * elemasserts four things at once. - For a fixed-size array, the contract must record the dimension order, not only the sizes.
- A discriminating stimulus encodes position asymmetrically —
r * 100 + c, and a non-square shape when you can get one.
9. Exercises
- Print
sizeof(a),sizeof(a[0])andsizeof(a[0][0])for a 3 × 4 array and show that the first is the product of the other two counts. Which of the three is the row stride? - Read the same buffer with both index formulas and print both sequences. Which two positions agree, and why are those two not evidence?
- Compute the sum and the minimum of both readings. Why do they match, and what does that tell you about your scoreboard?
- Build a symmetric 3 × 3 matrix and compare the two readings element by element. Now change one element so it is not symmetric — how many positions differ?
- Write a stimulus with
a[r][c] = r + cand thena[r][c] = r * 100 + c. Which one can detect a transposition, and state the property that separates them. - Take a 2D test case from a suite you have. Would transposing the input change the expected output? What follows if not?
- Write the §5 comment for a fixed-size two-dimensional DPI argument, including the flattening formula and the declaration it depends on.
- For a declaration with one packed and one unpacked dimension, say which Chapter's techniques apply to each, and why they are not interchangeable.
10. Summary
- Two questions: C's layout (guaranteed) and the arriving layout (per declaration, per dimension).
- C is row-major — measured
sizeof48 / 16 / 4,a[r][c]atr * C + c. - A wrong major order permutes rather than corrupts — measured
0 3 12 21, every value genuine, nothing out of bounds. - Position-insensitive checks all agree: sum, count, min, max, histogram, whole-array checksum.
- First and last elements always land correctly — a spot check confirms the bug.
- Measured: diagonal agrees on a 3 × 3; a symmetric matrix reads identically transposed.
- Third instance of the module's pattern — the common case is incapable of failing (21.2 §5, 21.5 §6).
- Ask per dimension; let the accessor compute the address. Hand-written flattening asserts four things at once.
- A fixed-size contract must record the dimension order, not just the sizes.
- Discriminating stimulus:
r * 100 + c, and a non-square shape where possible. Test the test by transposing its input.
Dimensions, bounds and major order are now things to ask about rather than assume. What remains is the case these techniques exist for: a real payload, of a length decided at run time, that the model has to keep after the call returns — where the array questions meet the ownership question Chapter 20.8 raised for a string.
Standards & specifications
- Governing standard
- IEEE Std 1800 (SystemVerilog)(opens IEEE in a new tab)
Defines SystemVerilog language semantics — syntax, data types, scheduling and the behaviour a conforming simulator must produce. It does not define tool-specific synthesis support or vendor methodology.
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.
