Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • SystemVerilog Language Foundations34
  • Object-Oriented SystemVerilog for Verification33
  • Constraint Randomization — Interview-Critical Deep Dive56
  • SystemVerilog Assertions — Interview-Critical Deep Dive54
  • Functional Coverage in SystemVerilog28
  • Testbench Architecture in SystemVerilog32
  • Advanced Language & Integration30
    • DPI-C: Calling C from SystemVerilog
      • DPI Imports
      • Exports & Context Calls
      • Passing Arrays & Structs
      • C Reference Models
      • DPI Pitfalls & Debug
    • bind: Attaching Verification to RTL
    • Macros, Defines & Compilation
    • Plusargs, Files & Runtime Configuration
    • Simulation Performance & TB Memory
    • RTL/TB Boundaries, Races & Timing
  • Senior & Interview Preparation29
Loading lesson

‹DPI-C: Calling C from SystemVerilog
  1. Topics
  2. SystemVerilog
  3. Advanced Language & Integration
  4. DPI-C: Calling C from SystemVerilog
  5. Passing Arrays & Structs

Part 7 · Advanced & Integration · Intermediate

Passing Arrays & Structs

Move arrays, packed structs, and strings across DPI with open-array handles and ownership rules.

On this page

    Open arrays

    An open array argument lets one C function accept SV arrays of any size. On the C side the argument arrives as an opaque handle.

    The svdpi API queries the handle geometry and element pointers at run time.

    The next two listings show one import serving two packet sizes. They need their C halves linked through DPI-C, so they are not run in this course's checks.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // not-run: needs its C half linked, and Verilator 5.048 rejects dynamic-array actuals for open arrays (Icarus 13 has no DPI)
    // Open array import: works for any length byte array
    import "DPI-C" function int unsigned
      crc32_buf(input byte data[]);
    
    module tb_crc_demo;
      byte pkt4[] = '{8'h11, 8'h22, 8'h33, 8'h44};
      byte pkt2[] = '{8'hAA, 8'hBB};
    
      initial begin
        $display("crc4 = %h", crc32_buf(pkt4));   // same import,
        $display("crc2 = %h", crc32_buf(pkt2));   // different sizes
      end
    endmodule

    The C half walks the handle with low and high bounds. It needs its C half linked through DPI-C, so it is not run in this course's checks.

    c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // not-run: C half of a DPI pair; the course runner builds SV only (Verilator 5.048 compiles it as C++, so wrap it in extern "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;
    }
    • svSize, svLow, and svHigh report the element count and index bounds of one dimension.

    • svGetArrElemPtr1, 2, and 3 return a pointer to one element of a 1, 2, or 3-dimensional array.

    • svGetArrayPtr returns a whole-block pointer only when the layout is C-contiguous, and returns NULL otherwise, so the caller checks for NULL before use.

    • Element access is by pointer. Writes through the pointer update the SV array for inout and output arguments.

    Check yourself A C function reads past the last element of an open array. Which query prevents that?

    The svLow and svHigh bounds of the dimension. The loop runs from low to high inclusive and never assumes a fixed length.

    Packed structs and strings

    A packed struct crosses DPI as a plain bit vector, not a C struct. The C side sees the SV bit layout and unpacks fields deliberately.

    The next listing maps a 32-bit header onto one vector word. It needs its C half linked through DPI-C, so it is not run in this course's checks.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // not-run: needs its C half compiled and linked; the course runner builds SV only (Verilator 5.048 links DPI-C, Icarus 13 has no DPI)
    typedef struct packed {
      bit [7:0]  opcode;
      bit [15:0] addr;
      bit [7:0]  len;
    } hdr_t;  // 32 bits total -> one svBitVecVal word
    
    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)

    Input strings are simulator-owned and valid only for the duration of the call. Copy the bytes when longer storage is needed.

    The next listing returns a string from a buffer that outlives the call. It needs its C half linked through DPI-C, so it is not run in this course's checks.

    c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // not-run: C half of a DPI pair; the course runner builds SV only (Verilator 5.048 compiles it as C++, so wrap it in extern "C")
    #include <stdio.h>
    
    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 */
    }

    Common pitfall

    Returning a stack buffer as a string from C hands SV a dangling pointer. Use a static buffer or heap memory that C manages, and never a local array.

    Memory ownership

    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*)                                 — copy bytes 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".

    Crashes at the boundary are almost always ownership bugs. Each side frees only what it allocated.

    The next listing drives a packet to a C model through caller-allocated buffers. It needs its C half linked through DPI-C, so it is not run in this course's checks.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // not-run: needs its C half linked, and Verilator 5.048 rejects dynamic-array actuals for open arrays (Icarus 13 has no DPI)
    // 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
      if (n < 0) $fatal(1, "request longer than the rsp buffer");
      rsp = new[n](rsp);               // shrink to actual size
    endtask

    The C half fills the caller buffer and returns the count. It needs its C half linked through DPI-C, so it is not run in this course's checks.

    c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // not-run: C half of a DPI pair; the course runner builds SV only (Verilator 5.048 compiles it as C++, so wrap it in extern "C")
    #include "svdpi.h"
    
    int model_process(const svOpenArrayHandle req, svOpenArrayHandle rsp)
    {
        int n_req = svSize(req, 1);
        int cap   = svSize(rsp, 1);       /* size of the caller's buffer */
        int n_out = 0;
        if (n_req > cap) return -1;       /* never write past rsp */
        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 */
    }

    Common pitfall

    Storing an open-array handle or element pointer past the import call keeps a dangling pointer. Later use corrupts memory far from the original call.

    Testbench

    The next listing proves the byte transform and the header unpacking in plain SystemVerilog. It runs in this course's checks.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    module tb_bytes_struct;
      logic [7:0] req [0:3];
      logic [7:0] rsp [0:3];
      logic [7:0] expv [0:3];
      logic [31:0] hdr;
      logic [7:0] opcode;
      logic [15:0] addr;
      logic [7:0] len;
      int bad;
      int i;
      initial begin
        bad = 0;
        req[0] = 8'h11; req[1] = 8'h22; req[2] = 8'h33; req[3] = 8'h44;
        expv[0] = 8'h4B; expv[1] = 8'h78; expv[2] = 8'h69; expv[3] = 8'h1E;
        for (i = 0; i < 4; i++) rsp[i] = req[i] ^ 8'h5A;
        for (i = 0; i < 4; i++) if (rsp[i] !== expv[i]) bad++;
        hdr = {8'hA5, 16'h1234, 8'h07};
        opcode = hdr[31:24]; addr = hdr[23:8]; len = hdr[7:0];
        if (opcode !== 8'hA5) bad++;
        if (addr !== 16'h1234) bad++;
        if (len !== 8'h07) bad++;
        if (bad == 0) $display("PASS: byte transform matched on 4 bytes and header gave 3 fields");
        else $fatal(1, "FAIL: byte transform gave a wrong value");
        $finish;
      end
    endmodule
    Check yourself The SV side allocates the response buffer and the C side only fills it. Which side frees that buffer?

    The SV side, since it allocated the buffer. Each side frees only its own allocations, so C never frees it.

    Interview

    Expect to state the ownership table from memory: handle and string-input lifetimes, who frees chandle memory, and why a stored element pointer is a delayed crash.

    Related topics

    • C Reference Models & Debug

    Previous

    240 / 296

    Next: C Reference Models

    Up next

    Up nextC Reference Models

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • DPI Imports
    • DPI Pitfalls & Debug