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.

systemverilog
// 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
c
// 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.

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

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

diagram
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

systemverilog
// 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
endtask
c
int 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.