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.

The run log. Verilator refuses the index, the accessor returns a null pointer, and the model turns that into status 3.
%Warning: DPI svOpenArrayHandle function index 1 out of bounds; 0 outside [3:5].
status=3 total=0

The 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.

A handle describes the arrayArrows show what each side passes or reads. The handle is a descriptor; the bytes stay in the simulator's storage and C reads them one at a time through the accessor.
A handle describes the arrayArrows show what each side passes or reads. The handle is a descriptor; the bytes stay in the simulator's storage and C reads them one at a time through the accessor. SystemVerilog main.sv byte unsignedascending[3:5] = '{1,128, 255}; descriptor svOpenArrayHandle dimensions 1 · left 3 ·right 5 · size 3 simulator storage elements [3] = 1 [4] = 128 [5]= 255 C sum_bytes asks the handle, thenreads [3], [4], [5] output total 384, written throughthe output pointer passes a handledescribessvLeft, svRight,svSizesvGetArrElemPtr1(h,i)writes
  1. 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.

  2. 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
    1. call svLeft(h, 1): sum_bytes → svOpenArrayHandle
    2. return left 3, right 5, size 3: svOpenArrayHandle → sum_bytes
  3. 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
    1. data 1, 128, 255: elements → sum_bytes
  4. 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
    1. 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 arraysvLeftsvRightsvSizesvLow / svHighWalk (k = 0, 1, 2)
ascending[3:5]3533 / 5index 3, 4, 5
descending[5:3]5333 / 5index 5, 4, 3
ascending, count 03533 / 5no 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.

  1. Ascending bounds

    Three reads at indexes 3, 4 and 5.

    values1, 128, 255
    status / total0 / 384
  2. Descending bounds

    Three reads at indexes 5, 4 and 3; the order changes nothing for a sum.

    values2, 3, 4
    status / total0 / 9
  3. Empty packet

    A count of zero is legal: the loop runs zero times and the initialised total stays 0.

    count0
    status / total0 / 0
  4. Count beyond the buffer

    A count of 4 on a three-element array is refused before any read.

    count4
    status / total2 / 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.

RUNVerilator + C/C++ over DPI · simulator service
Edit the code, then run it
Expected: ascending=384 descending=9 empty=0
Read 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

Predict the outputThe example's sum_bytes is called on byte unsigned d[5:3] = '{2, 3, 4} with a count of 2. What line does the program print?
`timescale 1ns/1ps
module tb;
  import "DPI-C" function int sum_bytes(input byte unsigned data[], input int count, output int unsigned total);
  byte unsigned d[5:3] = '{2, 3, 4};
  int unsigned total;
  initial begin
    void'(sum_bytes(d, 2, total));
    $display("%0d", total);
    $finish;
  end
endmodule
Read 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;
}
Spot the bugThe bridge sums p[3:5] = '{1, 2, 3} four ways: sum_left walks from svLeft, sum_right from svRight, sum_high from svHigh downward, and sum_zero from index 0 upward. Each option is the call. Which one prints FAIL?
Read included file: bridge.cpp
#include "Vtb__Dpi.h"
#include "svdpi.h"
#include <cstdint>
#include <climits>
#include <cstring>
// One walker, four starting rules. Each reads count elements and adds them up.
static int walk(const svOpenArrayHandle d, int count, unsigned* total, int first, int step) {
  *total = 0;
  if (svDimensions(d) != 1) return 1;
  if (count < 0 || count > svSize(d, 1) || count > 1024) return 2;
  for (int k = 0; k < count; ++k) {
    const unsigned char* p = static_cast<const unsigned char*>(svGetArrElemPtr1(d, first + k * step));
    if (!p) { *total = 0; return 3; }
    *total += *p;
  }
  return 0;
}
static int dir(const svOpenArrayHandle d) { return svLeft(d, 1) <= svRight(d, 1) ? 1 : -1; }
extern "C" int sum_left(const svOpenArrayHandle d, int c, unsigned* t)  { return walk(d, c, t, svLeft(d, 1), dir(d)); }
extern "C" int sum_right(const svOpenArrayHandle d, int c, unsigned* t) { return walk(d, c, t, svRight(d, 1), -dir(d)); }
extern "C" int sum_high(const svOpenArrayHandle d, int c, unsigned* t)  { return walk(d, c, t, svHigh(d, 1), -1); }
extern "C" int sum_zero(const svOpenArrayHandle d, int c, unsigned* t)  { return walk(d, c, t, 0, 1); }
ConceptMay a C implementation of an open-array import assume that index 0 exists in the actual array?
ConceptWhy does sum_bytes take a count argument when the handle already reports svSize?
ConceptThe model wants to keep the packet for a report at the end of the test. What should it do with the pointer it got from svGetArrElemPtr1?

Remember

  • An open-array formal gives C a handle, a descriptor of the actual array, never a byte pointer. Ask svDimensions, svSize, svLeft and svRight before reading.
  • The actual array keeps the bounds the caller declared, [3:5] or [5:3]. Walk from svLeft toward svRight and read each element with svGetArrElemPtr1 by 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.