Whole packets through open arrays
Pass a whole packet to C through an open array, read it through the handle with the bounds the caller chose, and return a bounded sum.
- Lesson
- D1.4
- Time
- 25 min
- Runs on
- Verilator with C/C++
- Check
- 5 questions
The software team's sum_bytes loops from 0 to count - 1 and reads each byte through the DPI element accessor. Your packets are declared byte unsigned pkt[3:5], numbered like the datasheet's byte map. Every call comes back with status 3 and a total of zero, and the log fills with index out of bounds; 0 outside [3:5]. The array kept the bounds you gave it, and nothing on the C side asked what they were.
%Warning: DPI svOpenArrayHandle function index 1 out of bounds; 0 outside [3:5].
status=3 total=0The caller keeps the shape
The formal input byte unsigned data[] is an open array: an unpacked array whose size the import leaves open. C receives an svOpenArrayHandle, a descriptor of the actual array, and the DPI C layer answers questions about it. svDimensions gives the number of dimensions, and svSize, svLeft and svRight describe one dimension; svLow and svHigh give the same bounds sorted. A legal actual array may start at 3, or at 5 and count down. Nothing promises an index 0.
- Pass the handle
The testbench calls
sum_bytes(ascending, 3, total). C gets a handle to the array, with its bounds, and a count of valid bytes. - Ask the shape
Before reading anything, C asks the handle for its dimension count and bounds: one dimension, left 3, right 5, size 3.
Follow the calls, values and waits
- call svLeft(h, 1): sum_bytes → svOpenArrayHandle
- return left 3, right 5, size 3: svOpenArrayHandle → sum_bytes
- Walk from the left bound
Three reads through the accessor, at the array's own indexes 3, 4 and 5. Each pointer is used at once and never kept.
Follow the calls, values and waits
- data 1, 128, 255: elements → sum_bytes
- Return the sum
The sum 1 + 128 + 255 = 384 goes out through the output pointer, and status 0 says it is valid.
Follow the calls, values and waits
- data 384: sum_bytes → total
Read the diagram as text
- main.sv: byte unsigned ascending[3:5] = '{1, 128, 255};
- svOpenArrayHandle: dimensions 1 · left 3 · right 5 · size 3
- elements: [3] = 1 [4] = 128 [5] = 255
- sum_bytes: asks the handle, then reads [3], [4], [5]
- total: 384, written through the output pointer
- main.sv → svOpenArrayHandle: passes a handle
- svOpenArrayHandle → elements: describes
- sum_bytes → svOpenArrayHandle: svLeft, svRight, svSize
- sum_bytes → elements: svGetArrElemPtr1(h, i)
- sum_bytes → total: writes
The handle is not a byte pointer. Ask it for the bounds, then read each element by the index the caller used.
| Actual array | svLeft | svRight | svSize | svLow / svHigh | Walk (k = 0, 1, 2) |
|---|---|---|---|---|---|
ascending[3:5] | 3 | 5 | 3 | 3 / 5 | index 3, 4, 5 |
descending[5:3] | 5 | 3 | 3 | 3 / 5 | index 5, 4, 3 |
ascending, count 0 | 3 | 5 | 3 | 3 / 5 | no reads |
What the handle answers for the example's arrays, and the indexes the walk visits. The literal '{2, 3, 4} fills from the left index: descending[5] is 2.
For a sum the order makes no difference. For a CRC, a parser or a byte-stream decoder it is part of the format, so decide which end is the first byte and read in that order on purpose.
Read through the accessor
svGetArrElemPtr1(h, i) returns a pointer to element i of a one-dimensional array, using the array's own index. Outside the bounds it returns null, and Verilator also prints a warning. The formal type byte unsigned makes each element an unsigned char, so the model casts the pointer to that and reads one byte. Never cast the handle itself to an array, and never assume the elements sit next to each other in memory.
Index from zero
for (int k = 0; k < count; ++k) {
const unsigned char* p =
(const unsigned char*)svGetArrElemPtr1(d, k);
*total += *p; // null for pkt[3:5]
}
Works only for arrays that happen to start at 0. For pkt[3:5] the first read is null, and a model that skips the check crashes.
Walk from the left bound
const int left = svLeft(d, 1);
const int step = left <= svRight(d, 1) ? 1 : -1;
for (int k = 0; k < count; ++k) {
const unsigned char* p =
(const unsigned char*)svGetArrElemPtr1(d, left + k * step);
if (!p) { *total = 0; return 3; }
*total += *p;
}
Reads the bytes the caller numbered 3, 4 and 5, in either direction, and turns a null pointer into a status instead of a crash.
A pointer from the accessor is borrowed for the length of the call. The simulator may move or reuse that storage once the call returns, so a model that needs the packet later copies the bytes into memory of its own. svGetArrayPtr can hand out the whole block at once when the layout is supported, but only behind a check with the element-by-element walk as the fallback.
Bound the work
The count argument says how many bytes are valid. The handle knows how big the buffer is; it cannot know how much of it the packet filled. A fixed buffer of 64 bytes may carry a 12-byte packet, and the sum must stop at 12. The model refuses a negative count, a count beyond svSize, and a count above 1024, so the largest sum, 1024 times 255, stays far inside a 32-bit unsigned total.
Ascending bounds
Three reads at indexes 3, 4 and 5.
values 1, 128, 255status / total 0 / 384Descending bounds
Three reads at indexes 5, 4 and 3; the order changes nothing for a sum.
values 2, 3, 4status / total 0 / 9Empty packet
A count of zero is legal: the loop runs zero times and the initialised total stays 0.
count 0status / total 0 / 0Count beyond the buffer
A count of 4 on a three-element array is refused before any read.
count 4status / total 2 / 0
One call per packet is the natural size. It keeps the pointer rules simple, and the transfer cost is easy to see in a profile. A call per bit or per byte buries the same work in thousands of crossings. Measure the crossing with real packets before you claim that fewer calls made anything faster.
Try it
Run it, then add a fourth array with the bounds [7:8] holding two bytes of 255 and assert a total of 510. A byte-sized accumulator would have lost the carry; the 32-bit total keeps it.
Then ask for a count of 4 on ascending and check that the status is 2 and the total 0: the model refuses before it reads anything.
ascending=384 descending=9 empty=0Read included file: bridge.cpp
#include "Vtb__Dpi.h"
#include "svdpi.h"
#include <cstdint>
#include <climits>
#include <cstring>
// status 0: summed. 1: wrong shape. 2: count outside the buffer or above 1024. 3: element missing.
extern "C" int sum_bytes(const svOpenArrayHandle data, int count, unsigned int* total) {
*total = 0;
if (svDimensions(data) != 1) return 1;
if (count < 0 || count > svSize(data, 1) || count > 1024) return 2; // 1024 x 255 fits a 32-bit sum
const int left = svLeft(data, 1);
const int step = left <= svRight(data, 1) ? 1 : -1; // walk from left toward right
for (int k = 0; k < count; ++k) {
const unsigned char* p = static_cast<const unsigned char*>(svGetArrElemPtr1(data, left + k * step));
if (!p) { *total = 0; return 3; } // outside the bounds: null
*total += *p; // read now; never keep p
}
return 0;
}
Check yourself
Remember
- An open-array formal gives C a handle, a descriptor of the actual array, never a byte pointer. Ask
svDimensions,svSize,svLeftandsvRightbefore reading. - The actual array keeps the bounds the caller declared,
[3:5]or[5:3]. Walk fromsvLefttowardsvRightand read each element withsvGetArrElemPtr1by its own index. - A pointer from the accessor is borrowed for the call. Copy the bytes if the model needs them later.
- Carry the valid length as a count, check it against
svSize, and cap it so the sum cannot overflow. A zero count is a legal, empty packet. - One call per packet keeps the hand-off simple and measurable; a call per byte hides the same work in thousands of crossings.