Skip to content
VLSI Mentor

C Part II · Module 21 · Arrays and Open Arrays in DPI-C

Fixed-Size Arrays Across the Boundary

The easiest array case: both sides know the element count before anything runs. What is left is stating the contract and knowing who holds anyone to it — and the measurement is blunt. Three wrong calls in one file, a four-element array where eight are required, and exactly one is diagnosed: the form written with a keyword almost nobody uses. The runtime picture inverts it, with every over-read caught and every wrong-but-inside length caught by nothing. The chapter ends on the limit that matters at a DPI boundary, where the one check that works is not checking the caller you actually have.

Advanced22 min readCPart IIVerificationDPI-C

Part II · Module 21 · Chapter 21.3 · Fixed-Size Arrays Across the Boundary

1. The Case Where Both Sides Already Know

Chapter 21.2 split arrays into packed and unpacked and sent the packed ones to Chapter 20.6's specified chunk representation. This chapter takes the easiest of the unpacked cases: the element count is fixed and known on both sides before anything runs.

A frame of eight 32-bit words. A header of four bytes. An eight-element coefficient table. The number is in the SystemVerilog declaration, it is in the C source, and it never changes at run time.

From the LRM, such an array arrives in C as a pointer to its first element — the ordinary C array-parameter form. Measured, the contract and the three ways to write the function:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  the contract: 8 elements of 4 bytes = 32 bytes
  sumA(frame)     = 36
  sumB(frame)     = 36
  sumC(frame, 8)  = 36

All three agree, which is the point: when everything is correct, the forms are indistinguishable. The chapter is about what happens when something is not.

2. The Three Forms

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#define FRAME_WORDS 8

/* A — the usual one. The 8 is documentation. */
uint32_t sumA(const uint32_t a[FRAME_WORDS]);

/* B — a minimum the compiler checks. */
uint32_t sumB(const uint32_t a[static FRAME_WORDS]);

/* C — the size travels, so the function cannot be wrong about it. */
uint32_t sumC(const uint32_t *a, size_t n);

Form A is what almost everyone writes, and Chapter 21.1 measured what the bracket is worth:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  a parameter declared [8] is still just a pointer:
     sizeof(const uint32_t *) = 8, not 32

Form B adds one C99 keyword. It declares that the argument points at at least that many elements — a minimum, not a size, so the callee still cannot recover a count from it.

Form C is Chapter 14.7's shape, and it alone can express something the others cannot. Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  sumC(frame, 4) = 10   (a deliberate partial sum, not a bug)
  sumA has no way to express 'four of them' at all

3. What Each One Is Actually Worth

Three wrong calls, in one translation unit: a four-element array given to each function where eight are required.

Measured, at -Wall -Wextra -Wpedantic, at -O0 and again at -O2:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  toosmall.c:11:13: warning: array argument is too small; contains 4 elements,
                    callee requires at least 8 [-Warray-bounds]
  toosmall.c:5:37:  note: callee declares array parameter as static here
  [warning count: 1]

One warning for three wrong calls. Form A says nothing. Form C says nothing — the count is a value, and no compiler checks that the number you passed matches the array you passed it with. Only form B is diagnosed, and it is diagnosed at both optimisation levels, which matters because -Warray-bounds sometimes depends on optimisation and here it does not.

Now the same three at run time, under AddressSanitizer. Measured, all three:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  ==77850==ERROR: AddressSanitizer: stack-buffer-overflow
  READ of size 4 at 0x00016b95a690 thread T0

Every one caught. The runtime picture is the exact inverse of the compile-time one — at compile time the forms differ sharply, at run time they do not differ at all, because by then they are the same machine code reading past the same buffer.

And the case the whole module keeps returning to. Measured, a length that is wrong but stays inside the buffer:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  sum over 8 = 36
  sum over 4 = 10   <- the wrong length, every byte owned
  [ASan reports: 0]

Nothing. Not a memory error, not undefined behaviour, no diagnostic available in principle — Chapter 21.1 §6's boundary, and the reason the contract has to be stated rather than discovered.

Three ways to write a C function that receives a fixed-size array, measured against four questions. None of the three lets the callee recover the element count except the one that is handed it. Given a four-element array where eight are required, the plain bracket-N form produces no diagnostic, the static-N form is caught by the array-bounds warning at both no optimisation and dash O 2, and the pointer-plus-length form produces nothing because the count is a value rather than part of a type. All three are caught by AddressSanitizer at run time when the read goes past the end of the buffer. None of the three is caught by anything when the length is merely wrong but stays inside the buffer: measured, summing four elements of an eight-element array gives 10 instead of 36 with zero sanitizer reports. Only the pointer-plus-length form can express a deliberate partial operation at all.The questionasked of a fixed-sizeargumentPlain bracket formconst uint32_t a[8]Static bracket formconst uint32_t a[static8]Pointer and lengthconst uint32_t *a, size_tnCan the calleerecover the count?from the parameter alonenomeasured: the parameteris 8 bytes, not 32noit declares a minimum,not a sizeyesit was handed the numberToo small, atcompile time4 elements where 8 arerequiredno diagnosticmeasured: 0 warningscaughtmeasured: array boundswarning, at both levelsno diagnosticthe count is a value, nota typeReading past theend, at run timeunder the sanitizercaughtmeasured: buffer overflowreportedcaughtmeasured: buffer overflowreportedcaughtmeasured: buffer overflowreportedA length that staysinside the bufferthe row with no greencellnothingnot a memory errornothingnot a memory errornothingmeasured: 10 instead of36, 0 reportsCan it express apartial operation?the other half of thetradenothe count is baked innothe count is baked inyesmeasured: four of eightgives 10, deliberately12
Figure 1 — the three ways to receive a fixed-size array, measured against four questions. Read the second and fourth rows together: at compile time the static form is the only one of the three that catches a too-small argument, while at run time the three are identical because by then they are the same code reading past the same buffer. The fifth row is the one with no green cell anywhere, and it is the row that matters — a length that is simply wrong but stays inside the buffer is invisible to every tool, measured here as 10 where 36 was correct with no sanitizer report at all. The last row is the other half of the trade: only the pointer-and-length form can express a partial operation deliberately, which is also what makes it the form that can be wrong about the count.

4. Stating the Contract

The purpose asks for the contract to be stated, and §3 is why: of the six cells in the two compile-time rows, one is green. Most of the contract will never be checked by anything, so the question is where to put it so that a person checks it.

Four places, in descending order of how much good they do.

In the signature. const uint32_t a[static 8] says "at least eight" in a form a compiler reads. It costs one keyword over what you were going to write anyway. Use it on every fixed-size DPI entry point.

In a _Static_assert on your own side, so that your half of the contract cannot drift silently:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
_Static_assert(sizeof(frame_t) == FRAME_WORDS * sizeof(uint32_t),
               "frame size contract — check the SystemVerilog declaration");

Chapter 20.9 §5 made the same move for a struct and noted the same honest limit: it checks your side, not the agreement. It fires when somebody changes the C; it is silent when somebody changes the SystemVerilog.

In a comment naming the other side's declaration, because that is the thing a later reader cannot otherwise find:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
/* DPI import. SystemVerilog side: int frame [0:7]  (unpacked, 8 elements)
   The 8 below is that 8. If the declaration changes, this must change —
   nothing will tell you. */
uint32_t frame_checksum(const uint32_t a[static 8]);

In a runtime check, when the count can be passed. If the interface can afford an extra argument, form C plus a guard turns an undetectable bug into a reported one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (n != FRAME_WORDS) { report("frame length %zu, expected %d", n, FRAME_WORDS); return 0; }

That is the only one of the four that catches §3's last measurement — the wrong-but-inside length — and it catches it because you compared two numbers, not because a tool did.

5. Where [static N] Stops Working

The recommendation in §4 needs its limit stated plainly, because it is easy to over-read.

-Warray-bounds fires at a C call site, where the compiler can see both the function's declaration and the argument's array type. Measured, that is exactly the situation in toosmall.c: one file, three calls, the array declared four lines above.

The simulator's call into your function is not a C call site the compiler sees. Chapter 21.1 §7 established the structural reason: there is no translation unit containing both sides, and no compiler that parses both languages. So [static 8] does not police the DPI boundary. Nothing does.

What it does police is your own C-side callers — and that is an argument for having some.

6. The Misconception: Both Declarations Say 8, So the Sizes Are Checked

"The SystemVerilog side declares eight elements and the C side declares eight elements. If they ever disagreed, something would say so."

Measured false in three separate places.

The C bracket is discarded. Chapter 21.1 measured sizeof on the parameter as 8 bytes, not 32, and a two-element array going into a parameter declared as eight with 0 warnings. Form A's 8 is not a declaration of anything the compiler keeps.

Only [static N] is diagnosed, and only at a C call site. Measured: one warning for three wrong calls, and §5 established that the DPI call is not such a site.

No compiler sees both sides. The SystemVerilog 8 lives in a type the C compiler never parses; the C 8 lives in a file the simulator never reads. There is nothing in the toolchain positioned to compare them.

And a fourth, which is the one that actually bites in practice: the sizes agreeing is not the same as the sizes being right. Both sides can say eight while the protocol has moved to twelve, and every check in §4 passes.

The corrected position:

The size contract is prose with a compiler-readable fragment. [static N] holds up your C callers, a _Static_assert holds up your own declaration, a comment carries the other side's number, and only a runtime comparison catches a length that is wrong at the moment it is used.

7. What You Should Be Able to Say Now

  1. A fixed-size unpacked array arrives as a pointer to element 0 — the ordinary C array-parameter form.
  2. Three forms: plain [N], [static N], and pointer-plus-length. Measured, all three give 36 when everything is right.
  3. The bracket is not a size — measured, sizeof on the parameter is 8, not 32.
  4. [static N] declares a minimum, not a size; the callee still cannot recover a count.
  5. Compile-time: 1 of 3 wrong calls diagnosed — only [static N], measured at -O0 and -O2.
  6. Run time inverts it: all three over-reads reported by ASan, because by then they are the same code.
  7. The row with no green cell: a length that stays inside the buffer — measured, 10 instead of 36, 0 reports.
  8. Only pointer-plus-length can express a partial operation — measured, four of eight gives 10 deliberately.
  9. State the contract in four places: the signature ([static N]), a _Static_assert on your side, a comment naming the other side's declaration, and a runtime guard when the count can be passed.
  10. [static N] checks C call sites, not the DPI boundary — so write C-side tests, which make it live and bring the sanitizer with it.

8. Exercises

  1. Write the three forms and call each correctly. Confirm they agree. Then confirm sizeof on the parameter is the size of a pointer.
  2. Pass a 4-element array to all three where 8 is required, in one file. How many diagnostics, and from which? Repeat at -O2.
  3. Run the same three under AddressSanitizer. Which are reported now, and why did the picture change?
  4. Sum four elements of an eight-element array through the pointer-and-length form. What does ASan say? Why can nothing say anything?
  5. Add the _Static_assert from §4 and then change the element type to uint16_t. Which assertion fires, and which side of the contract did it fail to check?
  6. Write the §4 comment for a fixed-size DPI function you have seen, naming the SystemVerilog declaration it depends on.
  7. Write a C main that calls one of your DPI functions directly with a too-small array. Does [static N] fire now? What else does the test build give you for free?
  8. §6 says agreeing is not the same as being right. Describe a change to a protocol that would leave both declarations agreeing and both wrong.

9. Summary

  • A fixed-size unpacked array arrives as a pointer to element 0.
  • Three forms, indistinguishable when correct — measured, all 36.
  • The bracket is not a size — measured sizeof 8, not 32; [static N] is a minimum.
  • Compile time: 1 of 3 wrong calls caught — only [static N], at -O0 and -O2.
  • Run time: all 3 over-reads caught by ASan — the inverse picture.
  • A wrong length that stays inside is caught by nothing — measured 10 instead of 36, 0 reports.
  • Only pointer-plus-length expresses a partial operation — and that is also what lets it be wrong about the count.
  • State the contract: [static N] · _Static_assert · a comment naming the other declaration · a runtime guard.
  • [static N] checks C callers, not the simulator — so write C-side tests and get the keyword and the sanitizer working for you.
  • Agreeing is not being right: both sides can say 8 while the protocol says 12.

A fixed count is the comfortable case because the number exists before anything runs. The uncomfortable one is an argument whose size is decided by the caller at the moment of the call — where there is no number to bake in, and the C function has to be handed something that can answer the question instead.

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.