Skip to content
VLSI Mentor

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

Why Array Passing Is the Hardest Part of DPI

One language's array knows how long it is and the other's does not, and almost every silent DPI array bug is that sentence collecting interest. Measured here: the element count computed inside a function whose parameter is declared as eight elements comes out as two, a two-element array passed to that parameter produces no warning at all, and three checksums over the same buffer at three different lengths are all equally plausible. The chapter ends on where each tool draws its line — every one of them at the edge of the allocation, which is exactly where the expensive array bugs are not.

Advanced22 min readCPart IIVerificationDPI-C

Part II · Module 21 · Chapter 21.1 · Why Array Passing Is the Hardest Part of DPI

1. One Sentence, and Then the Consequences

Module 20 settled how values cross: integers at fixed widths, four-state vectors in two planes, reals exactly, strings as pointers with a lifetime, structs on terms nobody specifies. Arrays are harder than any of those, and the reason fits in a sentence.

A C array is bare storage with no size. A SystemVerilog array is an object whose type includes its bounds.

That asymmetry is not a detail of the DPI interface — it is a difference between the two languages that existed long before anyone connected them, and the interface has to bridge it every time an array crosses. This chapter establishes the asymmetry and measures it on the C side. What to do about it is the other seven chapters.

2. What a C Array Loses When You Pass It

Chapter 6 established that an array name in an expression becomes the address of its first element. The consequence is measurable and larger than it sounds.

Measured, an eight-element int array in the scope that declares it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  sizeof(a)              = 32
  sizeof(a)/sizeof(a[0]) = 8   <- the count, and it is right here

Now the same array, one function call later, in a function whose parameter is written int a[8]:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  the parameter is declared int a[8]; inside, sizeof a / sizeof a[0] = 2
  sizeof(int*) = 8, sizeof(int) = 4  ->  8/4 = 2

Two. Not a wrong count of elements — the size of a pointer divided by the size of an int. The 8 in the parameter declaration was discarded; the parameter is an int *, and sizeof on it reports what a pointer weighs.

This is worth sitting with, because it is the whole problem in one line. The information existed in the caller. It was not corrupted or converted. It simply did not travel, and there is no expression the callee can write to get it back.

And the declared bound binds nothing. Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  int a[8] accepted a 2-element array: 2 elements, 0 warnings

A two-element array passed to a parameter declared as eight, compiled under -Wall -Wextra -Wpedantic, with no diagnostic. The 8 is documentation that the compiler reads and then throws away.

3. What a SystemVerilog Array Keeps

The other side of the asymmetry, from the LRM and the language definition: a SystemVerilog array's size is part of its type. Passing one does not strip that; a size mismatch where the language checks types is a type error, not a silent coercion.

And the bounds are not merely a count. A SystemVerilog array can be declared over any range, so [3:10] is an ordinary declaration with eight elements whose first index is 3. A C array's indices start at zero and there is no other option.

So the two languages disagree about three separate things — whether the size travels, whether a declared bound is binding, and where the indices start — and only one of them has an answer to any of them.

The two languages' array models compared, which is the mismatch the rest of the module is about. A C array name denotes storage and decays to the address of its first element; a SystemVerilog array is an object whose type includes its bounds. The element count does not travel with a C array: measured, computing it from sizeof inside a function that declares its parameter as an 8-element array yields 2, because the parameter is a pointer. The bound written in a C parameter declaration binds nothing: measured, a two-element array passed to a parameter declared as eight produced zero warnings. C indices always start at zero, while a SystemVerilog range such as 3 to 10 is perfectly legal. And nothing catches a length that is merely wrong: measured, AddressSanitizer reports a read past the end of the buffer but has nothing to say about a read that stops short, because stopping short is not a memory error at all.The questionasked of an array argumentA C arraybare storageA SystemVerilog arrayan object with a typeWhat the name denoteswhen you pass itan address, nothing moreit decays to a pointera value whose typeincludes its boundsnothing is strippedDoes the count travel withit?can the callee recover itnomeasured: sizeof in the calleegives 2 for 8 elementsyesthe size is part of the typeDoes a declared parameterbound bind?the 8 in int a[8]nomeasured: 2 elements into aparameter declared 8, zerowarningsyesa size mismatch is a type errorWhere do the indicesstart?the question with no C analoguealways at zerothere is no other optionwherever you declare thema range of 3 to 10 is legalWhat catches a length thatis merely wrong?the organising factnothingmeasured: the sanitizer sees pastthe end, never short of itthe question does notarisethe bounds came with the array12
Figure 1 — the asymmetry, question by question, with the C column measured. The top two rows are the familiar part: a C array name becomes an address and the count does not go with it. The third row is the one that surprises people, because the bound written in the parameter declaration looks like a contract and is not — a two-element array went into a parameter declared as eight with no diagnostic at all. The fourth row is the one that has no C analogue to reason from: a SystemVerilog range need not begin at zero, so an index that is correct on one side can be correct and different on the other. The last row is the chapter's organising fact, and section 6 measures it: nothing detects a length that is merely wrong, because a short read is not a memory error.

4. Why This Makes Arrays the Silent Case

Module 20's hazards all had a tell. The wrong signedness produced a different number at the top of the range (20.4). A collapsed X produced a value where a check could have found one (20.5). A dangling string produced a sanitizer report, at least sometimes (20.8).

A wrong array length produces a correct computation over the wrong number of elements, and there is nothing distinctive about the result.

Measured, the same 16-byte buffer, three lengths:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  buffer of 16 bytes
  checksum over 16 (correct) = 0xB4459108
  checksum over 15 (short)   = 0xE4C87008   <- a plausible number
  checksum over  8 (half)    = 0xD706E884   <- also plausible
  do any of these look wrong on their own? no

Three eight-digit hex values. Nothing about 0xE4C87008 says "I am one byte short". If a model returns it and a design returns something else, the symptom is a mismatch, and the search starts in the design.

That is what the module blurb means by "arrays are where DPI most often fails silently", and it is why the module spends eight chapters on what Module 20 did in one.

5. The Shape of Every Fix

Every answer in this module is a variation on one move, and Chapter 14.7 already made it for buffers generally: if the size does not travel with the storage, make it a separate argument.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
static size_t count_right(const int *a, size_t n) { return n; }

Measured, count_right(a, 8) returns 8 — the only version that can be right, because the number came from somewhere that knew it.

Three forms of the same move appear later in the module, named here so the shape is recognisable when each arrives:

  • A fixed size both sides agree on, stated in the interface — Chapter 21.3.
  • A size passed alongside the data as an ordinary argument — this section's count_right, and Chapter 21.7's packet case.
  • A handle that carries the bounds with it, which is how the standard solves it when the size is not known when the C is written — Chapters 21.4 and 21.5.

What never works is recovering the size from the pointer. §2 measured the attempt.

6. Where Each Tool Draws Its Line

This is the chapter's most useful measurement, because it says exactly how much help to expect.

The classic sizeof mistake is caught. Writing sizeof a / sizeof a[0] on an array parameter produces two diagnostics:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  warning: sizeof on array function parameter will return size of 'int *'
           instead of 'int[8]' [-Wsizeof-array-argument]
  warning: 'sizeof a' will return the size of the pointer, not the array
           itself [-Wsizeof-pointer-div]

Good news, and narrower than it looks: the diagnostic fires on the syntax, so computing the same wrong number in two steps, or in another function, produces nothing.

A too-small argument to a plain array parameter is not caught. Measured, int a[4] given a two-element array: 0 warnings at -O0 and at -O2.

But [static N] is caught. C99 lets a parameter declare a minimum element count, and clang checks it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
static int sum4(const int a[static 4]) { return a[0]+a[1]+a[2]+a[3]; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  warning: array argument is too small; contains 2 elements, callee requires
           at least 4 [-Warray-bounds]

Measured at both -O0 and -O2. This is the one genuinely under-used tool in the chapter: the same declaration everyone writes as int a[4], with one keyword added, turns a silent acceptance into a diagnostic. For a DPI entry point with a fixed element count, write the static.

Reading past the end is caught at runtime. Measured, a checksum over 24 bytes of a 16-byte buffer: 0 compile warnings, but AddressSanitizer reports it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  ==73998==ERROR: AddressSanitizer: stack-buffer-overflow
  READ of size 1 at 0x00016d39a650 thread T0
      #0 in checksum overread.c:4

And reading less than the whole buffer is caught by nothing. The 15-byte checksum in §4 reads only memory it owns. It is not a memory error, it is not undefined behaviour, and there is no tool to report it — because from the machine's point of view nothing is wrong.

The pattern across all five: every tool here draws its boundary at the edge of the allocation. The array bugs that cost days live inside it — a length one too small, an index base that should have been 3, a stride for the wrong element size. The defence against those is the interface design in §5, not a flag.

7. The Misconception: It Is Just a Pointer Like Any Other

"An array argument is a pointer. I pass pointers around in C all day without trouble. Why should a DPI boundary be different?"

The premise is correct — §2 measured it — and the conclusion does not follow, for a reason that is about where the code is rather than what it is.

In ordinary C, the contract is readable. The caller and the callee are in the same program, usually the same translation unit or one header away. When a function takes a pointer and a length, a reviewer can look at both sides. When the length is wrong, the test that exercises that path tends to exercise it with an array whose real length is in view.

At a DPI boundary, one side cannot see the other at all. The size exists on the SystemVerilog side, in a type the C compiler never parses, in a file a C reviewer may never open. There is no translation unit containing both. There is no header the two share. The contract is not something a compiler is failing to check — there is no compiler that sees both sides.

And the stakes are different. An ordinary C array bug fails in a program whose job is to be right. A DPI array bug fails in a reference model whose job is to disagree with a design when the design is wrong — so a model reading the wrong number of elements does not merely produce a wrong answer, it produces a verdict, and Chapter 18.2 measured what a verdict is worth when the model cannot disagree.

The accurate position:

It is just a pointer, and that is the problem. In ordinary C, the missing size is recoverable by reading the other side. At a DPI boundary, the other side is a different language and the size must be carried deliberately — which is why the rest of this module is about exactly how.

8. What You Should Be Able to Say Now

  1. The whole asymmetry in one sentence: a C array is bare storage with no size; a SystemVerilog array's type includes its bounds.
  2. An array name becomes an address, and the count does not travel. Measured: sizeof(a)/sizeof(a[0]) is 8 in the declaring scope and 2 inside a function whose parameter is written int a[8] — sizeof(int*)/sizeof(int).
  3. A declared parameter bound binds nothing — measured, a 2-element array into int a[8], 0 warnings.
  4. SystemVerilog indices need not start at zero; C's always do. [3:10] is an ordinary declaration.
  5. A wrong length produces a plausible answer — measured, 0xB4459108 / 0xE4C87008 / 0xD706E884 over 16, 15 and 8 bytes of the same buffer.
  6. The classic sizeof mistake is caught by -Wsizeof-array-argument and -Wsizeof-pointer-div — on the syntax, so computing it in two steps is not.
  7. [static N] is caught — -Warray-bounds, measured at -O0 and -O2. Write it on fixed-size DPI entry points.
  8. Past the end is caught at runtime — ASan reported a stack-buffer-overflow for a 24-byte read of a 16-byte buffer, with 0 compile warnings.
  9. Short of the end is caught by nothing, because it is not a memory error — and that is where the expensive bugs live.
  10. Every fix is the same move: carry the size deliberately — as a fixed contract (21.3), an argument (21.7), or a handle (21.4, 21.5).

9. Exercises

  1. Print sizeof(a)/sizeof(a[0]) for an array in its own scope and inside a function taking int a[8]. Explain the second number from sizeof(int *) and sizeof(int).
  2. Pass a 2-element array to a parameter declared int a[8] at -Wall -Wextra -Wpedantic -O2. How many diagnostics? Now change the parameter to int a[static 8].
  3. Checksum the same buffer at three lengths. Could you tell which result is correct from the values alone? What would you need in order to tell?
  4. Write the sizeof mistake in two statements instead of one. Does the diagnostic still fire? What does that tell you about relying on it?
  5. Read 8 bytes past the end of a buffer under AddressSanitizer, then read 1 byte short of its end. Which run reports anything, and why is the asymmetry inevitable?
  6. For an interface you know that takes a pointer and a length, write down where the length comes from and who guarantees it. If the answer is "the caller", who guarantees the caller?
  7. §7 says the DPI case differs because no compiler sees both sides. Name two other places in your work where that is also true, and say how each one carries its size.

10. Summary

  • A C array is bare storage with no size; a SystemVerilog array's type includes its bounds. Everything else in this module follows.
  • The count does not travel — measured 8 in scope, 2 in the callee.
  • A declared parameter bound binds nothing — measured, 2 into int a[8], 0 warnings.
  • SystemVerilog indices need not start at 0.
  • A wrong length is plausible — measured, three equally ordinary-looking checksums over 16, 15 and 8 bytes.
  • The tool matrix: the sizeof mistake caught (syntactic) · a too-small plain array argument not caught · [static N] caught at -O0 and -O2 · past the end caught by ASan · short of the end caught by nothing.
  • Every tool draws its line at the edge of the allocation, and the costly array bugs are inside it.
  • Every fix carries the size deliberately: a fixed contract, an extra argument, or a handle.
  • It is just a pointer, and that is the problem — in ordinary C the missing size is recoverable by reading; here there is no compiler that sees both sides.

Carrying the size deliberately presupposes knowing what the size is a size of, and that turns out not to be one question. A SystemVerilog array can be two quite different things, and which one you have decides the C representation, the argument form and every rule after it.

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.