C Part II · Module 21 · Arrays and Open Arrays in DPI-C
Open Array Elements and Bounds
Chapter 21.4's mistake was loud: cast the handle and an address turns up where an element should be. This one is quiet. Index from zero while the counter is under the size — the most ordinary loop in C — and against an array declared over the range 3 to 10 it asks for three indices that do not exist, never touches three that do, and files the five it did read under the wrong numbers. Through the API, one of those three failures is reported. Through a bare pointer, none is, and five genuine values come back with nothing anywhere to complain about.
Expert23 min readCPart IIVerificationDPI-C
Part II · Module 21 · Chapter 21.5 · Open Array Elements and Bounds
1. What to Ask, and in What Order
Chapter 21.4 established that an open array arrives as an opaque handle with a set of functions on it. This chapter uses them, and the order is the content.
From the LRM, the queries answer four things, and a C function that is going to touch elements needs three of them before it touches any:
- the low index of the dimension
- the high index
- the number of elements
- the bytes per element
Measured on an array declared over the range 3 to 10:
low = 3
high = 10
size = 8 elements
bytes per element = 4Note that size and low are independent facts. Eight elements, and the first one is number 3. A C array cannot be like that, which is why this is the chapter where C habits stop transferring.
2. The Loop That Is Correct
for (int i = oaLow(h); i <= oaHigh(h); i++) {
uint32_t v = *(uint32_t *)oaElemPtr(h, i);
…
}Two details, both load-bearing.
The bound is <=, not <. high is the index of the last element, not one past it — the opposite of every C convention, including the size the same API will hand you.
The index comes from the array, not from a counter. Nothing in the loop assumes anything about where the indices begin.
Measured:
for (i = low; i <= high; i++): 0x100 0x101 0x102 0x103 0x104 0x105 0x106 0x107All eight, once each. And the round trip:
8 elements read back: exact3. The Loop Written by Reflex
for (int i = 0; i < size; i++) { … oaElemPtr(h, i) … }It is the right loop for a C array and it is correct for a great many SystemVerilog arrays too, which is exactly the problem. Measured against the array declared 3 to 10:
for (i = 0; i < size; i++): -- -- -- 0x100 0x101 0x102 0x103 0x104
indices 0,1,2 are OUTSIDE the declared range: 3 rejected, 5 returned
and the 5 that returned are elements 3..7, not 0..4 -- SHIFTED by 3Three separate things went wrong in one line.
It asked for three indices that do not exist. 0, 1 and 2 are outside the declared range. Asked through the API, they can be rejected.
It never read three that do. Elements 8, 9 and 10 were not touched. The loop stopped after eight iterations and it had started three too early.
It mislabelled the five it did read. Element 3 was recorded as element 0, element 4 as element 1, and so on — shifted by 3.
4. The Finding: Only One of the Three Is Detectable
Count what a tool could possibly have told you about §3.
The out-of-range indices can be reported — but only because they went through the API, which knows the bounds and can refuse. That is the entire value of asking rather than computing.
The three unread elements cannot be detected by anything. Not reading something is not an error. There is no sanitizer for insufficient curiosity.
The mislabelling cannot be detected either. Element 3's value stored under the name 0 is a correct read of a real element filed in the wrong place, and no tool has an opinion about filing.
Now remove the API. Measured, the same mistake with a raw pointer:
raw[0] = 0x100, but element 0 does not exist; element 3 is 0x100
the VALUES coincide here, because element 3 is stored first
so the reflex loop reads real data and is wrong about WHICH dataWith a bare pointer the first failure disappears too — not because it stopped happening, but because nothing is left to notice. raw[0] returns 0x100, which is a real element's real value. The loop runs to completion, reads five genuine values, and is wrong about every one of their identities.
Every byte touched belongs to the buffer, so this is Chapter 21.1 §6's boundary once more: no memory error, nothing to report, a plausible answer.
And it is the sharp contrast with Chapter 21.4. There, casting the handle produced an address where a value should be — strange, large, noticeable. Here the values are ordinary and correct and attached to the wrong indices. The loud mistake is the one people find in an afternoon; this is the other kind.
5. Writing It So the Question Cannot Arise
Four habits, each removing one way to get §3 wrong.
Take the bounds from the array, every time. Two calls at the top of the function, and no index literal anywhere below them.
const int lo = oaLow(h), hi = oaHigh(h);
for (int i = lo; i <= hi; i++) { … }Use <= with high, and < with size — never mix them. Pick the bounds form and use it throughout; the two idioms are both correct and produce different loops, and the hybrid for (i = low; i < size; i++) is wrong in a way that looks careful.
Get the element size from the array too. Chapter 21.2 measured what happens when a stride is assumed: the values decode to plausible wrong numbers. The handle carries it, so there is no reason to write a 4.
Convert to your own representation once, at the top. If the model's natural shape is a zero-based C array — and it usually is — copy into one immediately, and do the index translation in exactly one place:
for (int i = lo; i <= hi; i++) local[i - lo] = *(uint32_t *)oaElemPtr(h, i);The i - lo is the entire translation. Written once it is reviewable; spread through a function it is where §3 comes from.
6. The Misconception: low Is Always Zero in Practice
"Ranges that do not start at zero are a curiosity. Every array I have seen is
[0:N-1], sofor (i = 0; i < size; i++)is a safe default."
The first half is measured true — [0:7] gives low = 0 — and the conclusion still does not hold, for three reasons.
"Safe default" is the wrong category. A default is safe when being wrong about it is detectable. §4 measured that two of the three failures are invisible to every tool, and that with a bare pointer the third one is too. An assumption whose violation cannot be observed is not a default; it is a dependency.
You are not choosing the declaration. The array belongs to the SystemVerilog side, and a range is an ordinary thing for a verification engineer to write — a register map indexed by address, a channel array numbered from one, a window declared [START:END]. Nothing warns anybody that a C model elsewhere depends on the lower bound.
The alternative costs two calls. Not a design change, not a rewrite, not a performance trade — oaLow and oaHigh into two local variables, after which the loop body is unchanged.
The corrected position:
The reflex loop is correct for the common declaration, which is why it survives review and why its failure is not a thing you will be warned about. Taking the bounds from the array costs two calls and removes the dependency entirely — and the C you write below them is identical either way.
7. What You Should Be Able to Say Now
- Ask before touching: low, high, size and bytes per element. Measured on a
3 to 10array:3,10,8,4. sizeandloware independent — eight elements whose first index is 3, which no C array can be.- The correct loop is
for (i = low; i <= high; i++)—highis the last index, not one past it. Measured: all eight values, round trip exact. - The reflex loop does three wrong things at once — measured: 3 indices rejected, elements 8, 9, 10 never read, and the five that were read shifted by 3.
- Only the first is detectable, and only through the API, which knows the bounds and can refuse.
- Not reading an element is not an error, and mislabelling one is not either — no tool has an opinion about filing.
- With a bare pointer, even the first failure disappears — measured,
raw[0]returns0x100, a real element's real value, and nothing complains. - This is the quiet counterpart to Chapter 21.4's loud one: there, an address where a value should be; here, correct values under wrong names.
- Four habits: take the bounds from the array · never mix
<= highwith< size· take the element size from the array · translate the index in exactly one place,i - lo. - The reflex loop is correct for
[0:7]— measured — which is why it survives review, and why "safe default" is the wrong description of an assumption nothing can check.
8. Exercises
- Query low, high, size and element size for an array declared
3 to 10. Which two of those four would you have guessed wrong? - Write both loops and run them over the same array. Record what the reflex loop returns and, separately, what it claims each value's index is.
- Of the three failures in §3, which could a tool report, and why can it report only that one?
- Run the reflex loop through a bare pointer instead of the API. What changes about the output, and what changes about your ability to notice?
- Write
for (i = low; i < size; i++)and work out which elements it touches for a3 to 10array. Why is this the most dangerous of the three loops? - Rewrite a loop body that uses
a[i]so that the index translation happens exactly once. How many places did the translation appear before? - Query the bounds of an array declared
[0:7]. Explain, using §6, why the result is the reason the reflex loop keeps passing review. - Write the two-line change that would make an existing zero-assuming model correct for any range. Would it have failed any existing test?
9. Summary
- Ask first: low, high, size, bytes per element — measured
3,10,8,4for a3 to 10array. sizeandloware independent facts, and no C array can be numbered from 3.for (i = low; i <= high; i++)—highis the last index. Measured: all eight, round trip exact.- The reflex loop fails three ways at once — 3 rejected, 8, 9, 10 never read, five shifted by 3.
- Only the out-of-range asks are reportable, and only because they went through the API.
- Through a bare pointer nothing is reportable — measured,
raw[0]is a real value under the wrong name. - The quiet counterpart to Chapter 21.4's loud mistake.
- Four habits: bounds from the array · one bounds idiom, never a hybrid · element size from the array · translate the index once (
i - lo). - Measured, the reflex loop is correct for
[0:7]— the common case supplies the confirming evidence, as in 21.2 §5 and 20.9 §4. - Asking costs two calls, and the loop body is unchanged.
One dimension's bounds are now a solved problem, asked for rather than assumed. Adding a second dimension does not simply repeat the exercise, because the two languages disagree about something a single dimension never exposed: which index moves fastest through memory.
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.
