C Part II · Module 21 · Arrays and Open Arrays in DPI-C
Open Arrays and the Open Array Handle
When the element count is decided by the caller rather than written in both declarations, every technique from the fixed-size case stops working at once — there is no number to bake into a signature and none to assert. The standard's answer is to stop passing storage and start passing something that can be asked questions. Modelled and measured here: three objects with three different sizes — 8 bytes of handle, 24 of descriptor, 32 of data — and a cast that looks entirely reasonable and reads an address where an element should be. The opacity is the same design move chapter 20.10 made, pointed the other way.
Expert23 min readCPart IIVerificationDPI-C
Part II · Module 21 · Chapter 21.4 · Open Arrays and the Open Array Handle
1. When the Number Is Not Available
Chapter 21.3 worked because a number existed in two places before anything ran. Everything it recommended depended on that: [static 8] needs an 8, the _Static_assert needs an 8, the comment names an 8, and the runtime guard compares against an 8.
Now take the number away. A C function that must accept a payload of whatever length the caller has — a packet, a burst, a frame whose size the test decided at run time — has nothing to put in any of those places.
The usual C answer is Chapter 14.7's: pass a pointer and a length. That works when both sides are C. At a DPI boundary it leaves two questions unanswered, and Chapter 21.2 supplied both:
- How many bytes is an element? The count alone is not enough. Measured in 21.2: the same four values occupy 4 bytes or 16 depending on the element's storage.
- Where do the indices start? C's start at zero; SystemVerilog's start wherever the declaration says.
So the pointer-and-length pair would have to become a pointer, a length, an element size and an index base — four arguments whose mutual consistency nobody checks. From the LRM, the standard does something better: it passes one thing that already knows all four.
2. The Handle
From the LRM: an argument declared open on the SystemVerilog side arrives in C as an open array handle — a value of the type the header names svOpenArrayHandle, which is opaque. You do not read through it. You call functions on it, and those functions tell you the bounds, the element size, and the address of any element you ask for.
Chapter 20.2 §1 and Chapter 19.1 §4 established that svdpi.h does not exist on this machine, so everything below about the handle's existence, its opacity and its function set is from the LRM. What follows it is the layout modelled in C and measured, which is the same technique Chapter 20.6 used for the packed representation and for the same reason: a model you can round-trip is the strongest check available without the header.
/* a model of what the handle names, for measurement only */
typedef struct { void *base; int low, high; size_t elem; } OpenArray;
typedef OpenArray *Handle;3. Three Objects, Three Sizes
This is the measurement that makes the opacity non-negotiable.
sizeof(Handle) = 8 (a pointer, like any other)
sizeof(the descriptor) = 24 (what it points AT)
sizeof(the array data) = 32 (what you actually want)Three different numbers, and the C type system distinguishes none of them. The handle is eight bytes and looks exactly like a pointer to data; a pointer to data is eight bytes and looks exactly like a handle. Any cast between them compiles.
So the natural move — treat it as a pointer to the elements and index it — produces this. Measured:
the first 4 bytes reached that way are the low half of a POINTER,
not element data -- it is the descriptor's own first member
equal to the first element (0x100)? NO
and the value it does hold is an address, which is not data at allNote that this one does not fail silently, which is unusual for this module: an address interpreted as element data is a large, strange number, so a model that prints its inputs will notice. Contrast Chapter 21.2 §5, where the wrong model produced correct values. The dangerous open-array mistakes are the ones in Chapter 21.5, not this one.
What the accessors give instead. Measured:
the base reached through the API: first element = 0x100
bytes per element = 4, so element k is at base + k*4
the stride came WITH the handle; a bare pointer carries no strideThat last line is the argument in miniature. A bare pointer carries no stride — Chapter 21.2 measured what happens when the stride is assumed and wrong, and the handle is how the standard stops you having to assume it.
4. The Shape of the Function Set
From the LRM, the accessors fall into two groups, and knowing the shape matters more than memorising names.
Queries answer questions about the array: how many dimensions it has, the low and high index of a dimension, how many elements it holds, and how large an element is. These are the facts a C function cannot otherwise obtain, and Chapter 21.5 is about using them.
Element access converts an index into something you can read or write — the standard supplies both a pointer-returning form and typed accessors for the common element types.
Two properties of the set are worth stating now because they shape how you write the C.
Every answer comes from a call, not from arithmetic you do. The moment you compute base + i * 4 yourself, you have re-introduced the assumption the handle existed to remove.
The handle is valid for the call. Like Chapter 20.8's incoming string, it refers to storage the simulator owns, and the rule is the same: use it during the call, and if your model needs the data afterwards, copy it. Chapter 21.7 returns to this for payloads, where it matters most.
5. Why Opacity Is the Right Design
Worth making positively, because "you must call functions" sounds like ceremony until you ask what the alternative would be.
The representation belongs to the tool, not to the standard. A descriptor's layout is an implementation's business — how many fields, in what order, with what padding. That is Chapter 20.9's situation exactly: a layout no document fixes, where agreement between two tools would be a coincidence you were relying on. The difference is that here the standard supplied an API instead of leaving you to guess. The guess that 20.9 could only warn you against, 21.4 makes unnecessary.
And it is the same move as Chapter 20.10, pointed the other way. There, your C handed SystemVerilog a chandle it must not look inside, and the benefit was that you could change your structure freely without touching the other side. Here SystemVerilog hands you a handle you must not look inside, and the benefit is identical in the other direction: the simulator can change its descriptor between releases and your C keeps working.
Both arrangements work for one reason: the side that owns the representation is the side that defines the accessors. An opaque handle is not a restriction imposed on you — it is the same contract you would want if you were on the other end, and in Chapter 20.10 you were.
6. The Misconception: It Is Really Just a Pointer With Ceremony
"Underneath, the handle must point at the data somehow. The accessors are a wrapper. I can cast it and index it and skip the function calls."
Measured false. §3: three objects with sizes 8, 24 and 32, and the cast reads the descriptor's own first member — not equal to the first element, and holding an address rather than data.
But the measurement is the weaker half of the answer, because a reader can always reply "then I will cast it to the right thing". The real reply is structural.
There is no right thing to cast it to. The descriptor's layout is the tool's, and no document tells you its field order or its size — §5. Writing a matching struct is Chapter 20.9's guess, with Chapter 20.2 §5's property: it will compile. And if it happens to work with the simulator you have, it has told you nothing about the next one.
The arithmetic you would be skipping is the arithmetic that is wrong. base + i * elem needs an element size you do not have and an index base you have no reason to think is zero — Chapter 21.5 measures what that assumption costs.
And the cost you are avoiding is a function call, in a model that is already being called across a language boundary by an event-driven simulator.
The corrected position:
The handle names a descriptor whose layout nobody has specified to you, and the accessors are the only defined way to read it. Casting is not a shortcut past a wrapper — it is a guess at a layout, of exactly the kind Chapter 20.9 was about, in the one place where the standard has already removed the need to guess.
7. What You Should Be Able to Say Now
- The fixed-size techniques all need a number, and an open array does not have one — so
[static N], the_Static_assertand the comment from 21.3 all stop applying at once. - A pointer and a length is not enough at this boundary: the element size and the index base are also unknown (21.2).
- From the LRM: an open argument arrives as an opaque handle; you call functions on it rather than reading through it.
- Three objects, three sizes — measured: handle 8, descriptor 24, data 32 — and the C type system distinguishes none of them, so any cast compiles.
- Casting the handle reads the descriptor's first member — measured, not equal to the first element, and holding an address.
- This particular mistake is loud, unusually: an address read as data is an obviously strange value. The quiet open-array mistakes are in Chapter 21.5.
- The stride comes with the handle — measured, element k at base + k × 4, where a bare pointer carries no stride at all.
- The function set has two halves: queries (dimensions, bounds, size, element size) and element access.
- Every answer comes from a call. Computing
base + i * elemyourself re-introduces the assumption the handle removed. - The handle is valid for the call — copy anything your model needs afterwards, exactly as for an incoming string (20.8).
- Opacity is the same contract as
chandle(20.10), reversed: the side that owns the representation defines the accessors, and it removes the guess Chapter 20.9 could only warn about.
8. Exercises
- Model the descriptor as a struct and print the three sizes. Why does the C type system not help you tell the handle from a pointer to the data?
- Cast the handle to a pointer to the element type and read index 0. What value comes out, and what is it actually?
- §3 says this mistake is loud. Describe a model whose logging would hide it anyway.
- List the four facts a pointer-and-length pair would have to carry at a DPI boundary. Which two does ordinary C code normally take for granted, and why can it?
- Write down what your C would have to assume if you computed element addresses yourself. For each assumption, say what would have to change for it to become wrong.
- Chapter 20.10 handed an opaque handle in the other direction. Write the two-sentence argument for opacity that applies to both, without mentioning either language.
- An open array handle is valid for the call. For a model that must compare this call's payload with the previous one, say exactly what has to be copied and when.
9. Summary
- An open array has no compile-time count, so every technique in Chapter 21.3 stops applying.
- A pointer and a length is not enough here — the element size and the index base are also unknown.
- From the LRM: an opaque handle, with query and element-access functions.
- Three objects, three sizes — measured 8 / 24 / 32 — and the type system distinguishes none of them, so the cast compiles.
- The cast reads an address, not an element — measured, not equal to the first element.
- Loud, not silent — unusually for this module; the quiet failures are Chapter 21.5's.
- The stride arrives with the handle; a bare pointer carries none.
- Every answer comes from a call — your own address arithmetic restores the assumption the handle removed.
- Valid for the call — copy what you need to keep (20.8).
- Opacity is
chandlereversed (20.10): the owner of the representation defines the accessors, and the guess Chapter 20.9 warned about is removed rather than managed.
The handle is in hand and the function set has a shape. What is left is the part that goes wrong quietly: asking it for the bounds, and discovering that the loop every C programmer writes by reflex is not the loop this array needs.
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.
