Part 7 · Advanced & Integration · Intermediate
Passing Arrays & Structs
Open arrays with svOpenArrayHandle, packed structs, string handling, and memory-ownership rules at the DPI boundary.
Open arrays: size-agnostic C code
Declaring an import argument as an open array — input byte data[] — lets one C function accept SV arrays of any size. On the C side the argument arrives as an opaque svOpenArrayHandle, and the svdpi API queries its geometry and element pointers at runtime.
// Open array import: works for any length byte array
import "DPI-C" function int unsigned
crc32_buf(input byte data[]);
initial begin
byte pkt4[] = '{8'h11, 8'h22, 8'h33, 8'h44};
byte pkt2[] = '{8'hAA, 8'hBB};
$display("crc4 = %h", crc32_buf(pkt4)); // same import,
$display("crc2 = %h", crc32_buf(pkt2)); // different sizes
end// crc.c
#include "svdpi.h"
unsigned int crc32_buf(const svOpenArrayHandle data)
{
unsigned int crc = 0xFFFFFFFFu;
int lo = svLow(data, 1); /* lowest index, dim 1 */
int hi = svHigh(data, 1); /* highest index */
for (int i = lo; i <= hi; i++) {
char *el = (char *)svGetArrElemPtr1(data, i);
crc ^= (unsigned char)(*el);
for (int b = 0; b < 8; b++)
crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u)));
}
return crc ^ 0xFFFFFFFFu;
}The open-array API
svSize(h, d) / svLow(h, d) / svHigh(h, d) — element count and index bounds of dimension d.
svGetArrElemPtr1/2/3(h, i, ...) — pointer to one element of a 1/2/3-dimensional array.
svGetArrayPtr(h) — pointer to the whole block if (and only if) the layout is C-contiguous; check svSizeOfArray first.
Element access is by pointer — writes through the pointer update the SV array for inout/output arguments.
Packed structs and strings
A packed struct crosses DPI as a plain bit vector — the C side sees an svBitVecVal/svLogicVecVal array with the SV bit layout, not a C struct. The robust portable pattern is to define a matching C struct only when fields are byte-aligned, and otherwise unpack fields explicitly on one side.
typedef struct packed {
bit [7:0] opcode;
bit [15:0] addr;
bit [7:0] len;
} hdr_t; // 32 bits total → one svBitVecVal
import "DPI-C" function void decode_hdr(input hdr_t h);
// C signature: void decode_hdr(const svBitVecVal* h);
// h[0] bits [31:24]=opcode, [23:8]=addr, [7:0]=len (SV packing order)Strings
string maps to const char*. Input strings are simulator-owned and valid only for the duration of the call — copy if you need them later. To return a string from C, return a pointer to memory that outlives the call (static buffer or heap that C manages) — never a stack buffer.
const char* model_version(void)
{
static char buf[32]; /* outlives the call */
snprintf(buf, sizeof buf, "model-%d.%d", 2, 7);
return buf; /* SV copies it on return */
}Memory ownership rules
Crashes at the DPI boundary are almost always ownership bugs. The rules are simple and absolute: each side frees only what it allocated, and pointers into the other side's memory have a strictly bounded lifetime.
OWNERSHIP RULES AT THE BOUNDARY
Who allocated it? Who frees it? Lifetime of pointer
───────────────────────────────────────────────────────────────────
SV array passed to C simulator duration of the call
(svOpenArrayHandle) — never store the ptr
string input to C simulator duration of the call
(const char*) — strdup() to keep
C heap returned via C (provide a until C frees it;
chandle free import!) SV must call free fn
C static buffer C (static) until next call that
returned as string overwrites the buffer
RULE: never free across the boundary; never store a pointer
whose lifetime is "duration of the call".Byte-buffer packet example
// SV drives a packet to a C model and reads back the response.
import "DPI-C" function int
model_process(input byte req[], output byte rsp[]);
task automatic send_pkt(input byte req[]);
byte rsp[];
int n;
rsp = new[256]; // SV allocates the output buffer
n = model_process(req, rsp); // C fills rsp, returns count
rsp = new[n](rsp); // shrink to actual size
endtaskint model_process(const svOpenArrayHandle req, svOpenArrayHandle rsp)
{
int n_req = svSize(req, 1);
int n_out = 0;
for (int i = 0; i < n_req; i++) {
char *in = (char *)svGetArrElemPtr1(req, svLow(req,1) + i);
char *out = (char *)svGetArrElemPtr1(rsp, svLow(rsp,1) + n_out);
*out = (char)(*in ^ 0x5A); /* model the transform */
n_out++;
}
return n_out; /* SV side shrinks rsp to this length */
}Key takeaways
Open arrays (svOpenArrayHandle + svSize/svGetArrElemPtr) let one C function handle any SV array size.
Packed structs cross as bit vectors in SV packing order — not as C structs; unpack deliberately.
Input strings and array handles are valid only for the duration of the call — copy to keep.
Each side frees only its own allocations; expose a C free function for any C heap handed to SV via chandle.
Common pitfalls
Storing an svOpenArrayHandle or element pointer past the import call — dangling pointer, delayed crash.
Casting a packed struct to a C struct and assuming field alignment matches — packing order differs.
Returning a stack buffer as a string from C — corrupt or garbage string in SV.
free()-ing simulator-owned memory in C (or letting SV trigger free of C memory it didn't allocate) — heap corruption.