Pass arrays between SystemVerilog and C through DPI

How-to Last tested 24 October 2026

Match the generated DPI header before interpreting a C-side result as a hardware result. A packed vector, a fixed unpacked array and an open array have different calling contracts; preserve their element types and ask an open-array handle for its bounds. First test that transport with known values, then compare the reference model’s behavior with the RTL.

Treat the generated header as the contract

The SystemVerilog declaration determines the foreign function’s interface. Verilator emits a Vtb__Dpi.h header containing the prototypes for this example. The generated header’s own comment, printed below, instructs the implementation to include it where the imported C functions are declared so their signatures match the DPI imports. That catches an important class of mistakes before a simulation result exists. The example’s C-compatible functions are implemented in C++ with C linkage; it does not use a VPI substitute.

There are three array shapes in the main comparison. int values[0:2] is a fixed-size unpacked array. bit [63:0] values is one packed vector, delivered through the DPI packed-vector type. int values[] leaves the unpacked bound open, so the foreign side receives a handle and obtains the actual dimensions and indices at the call. Calling all three “an array of numbers” hides the layout information the implementation needs.

The files below are the complete original test, its implementation, the actual generated declarations and the deliberately mismatched implementation. Their download folder instructions give exact build commands. The simulator’s own svdpi.h comes from the pinned installation; it is not copied into this package. Do not replace it with a header from another simulator merely because the names look familiar.

Define the SystemVerilog side

// SPDX-License-Identifier: MIT
module tb;
  import "DPI-C" function int fixed_sum(input int values[0:2]);
  import "DPI-C" function int packed_sum(input bit [63:0] values);
  import "DPI-C" function int open_sum(input int values[], input int expected_size);
  import "DPI-C" function int byte_sum(input byte values[]);
  int fixed_values[0:2] = '{10, -2, 7};
  int ascending[5:7] = '{10, 20, 30};
  int descending[7:5] = '{10, 20, 30};
  int short_values[0:1] = '{4, 5};
  byte signed_values[2:3] = '{-1, 2};
  bit [63:0] packed_values = {32'd11, 32'd7};
  int fixed_result;
  int packed_result;
  int ascending_result;
  int descending_result;
  int short_result;
  int byte_result;

  initial begin
    fixed_result = fixed_sum(fixed_values);
    packed_result = packed_sum(packed_values);
    ascending_result = open_sum(ascending, 3);
    descending_result = open_sum(descending, 3);
    short_result = open_sum(short_values, 3);
    byte_result = byte_sum(signed_values);
    $display("RESULT fixed=%0d packed=%0d ascending=%0d descending=%0d short=%0d bytes=%0d",
      fixed_result, packed_result, ascending_result,
      descending_result, short_result, byte_result);
    if (fixed_result != 15) $fatal(1, "Fixed array transport failed");
    if (packed_result != 18) $fatal(1, "Packed vector transport failed");
    if (ascending_result != 60 || descending_result != 60)
      $fatal(1, "Open array traversal failed");
    if (short_result != -1) $fatal(1, "Expected bounded-size rejection");
    if (byte_result != 1) $fatal(1, "Signed byte transport failed");
    $finish;
  end
endmodule

The fixed array contains a negative element, which distinguishes its signed integer interpretation from a collection of unsigned bytes. The packed value contains two 32-bit words with deliberately small values so their sum fits an int. The open arrays contain the same numbers with ascending and descending bounds. The short array intentionally has two elements while the function requires three. A separate signed-byte call checks that a byte element is not interpreted as an int element.

These inputs are transport tests. A sum returning 60 is useful only because the source fixed the three values and the expected result. It says nothing about the correctness of a larger arithmetic reference model. Choosing small bounded values also avoids using this example to make an untested claim about overflow handling, saturation or floating-point rounding.

Implement bounds and element types explicitly

// SPDX-License-Identifier: MIT
#include "Vtb__Dpi.h"
#include <cstdio>

extern "C" int fixed_sum(const int* values) {
  return values[0] + values[1] + values[2];
}

extern "C" int packed_sum(const svBitVecVal* values) {
  return static_cast<int>(values[0] + values[1]);
}

extern "C" int open_sum(const svOpenArrayHandle values, int expected_size) {
  const int left = svLeft(values, 1);
  const int right = svRight(values, 1);
  const int size = svSize(values, 1);
  std::printf("OPEN left=%d right=%d size=%d expected=%d\n",
              left, right, size, expected_size);
  if (svDimensions(values) != 1 || size != expected_size) {
    std::printf("REJECT_SIZE\n");
    return -1;
  }
  const int step = left <= right ? 1 : -1;
  int total = 0;
  for (int offset = 0; offset < size; ++offset) {
    const int index = left + step * offset;
    const int* item = static_cast<const int*>(svGetArrElemPtr1(values, index));
    if (!item) return -1;
    total += *item;
  }
  return total;
}

extern "C" int byte_sum(const svOpenArrayHandle values) {
  const int left = svLeft(values, 1);
  const int right = svRight(values, 1);
  const int size = svSize(values, 1);
  const int step = left <= right ? 1 : -1;
  int total = 0;
  for (int offset = 0; offset < size; ++offset) {
    const int index = left + step * offset;
    const signed char* item =
      static_cast<const signed char*>(svGetArrElemPtr1(values, index));
    if (!item) return -1;
    total += *item;
  }
  std::printf("BYTE left=%d right=%d size=%d sum=%d\n", left, right, size, total);
  return total;
}

fixed_sum follows the generated const int* declaration and reads exactly the three elements promised by that fixed signature. It cannot discover an arbitrary caller’s length from a raw pointer. That is why the declaration and its generated header belong in the same review as the loop. A later interface edit that changes the bound needs a rebuilt model and a rerun, not merely a new comment in the C implementation.

open_sum first reads the handle’s dimension count, size and declared endpoints. It rejects the wrong size before taking element pointers. It then walks from the left bound toward the right bound, selecting a positive or negative step from those endpoints. It does not assume that the first legal index is zero or that the right bound is larger. Each retrieved pointer is checked before dereference; the function returns its rejection sentinel if that retrieval fails.

The byte function uses the same bound traversal but reads a signed byte at each element. Sharing a handle type does not make the element representation interchangeable. Casting every returned pointer to int* would not be justified by the API’s generic pointer alone. The declared element type and the intended signedness must agree across both sides of the call.

packed_sum reads the two packed words through svBitVecVal. The example uses a two-state bit vector and known values. It does not demonstrate a four-state logic vector with separate known/unknown masks, and it does not establish host-independent interpretation of a foreign struct overlaid on the vector. Keep explicit widths and field extraction when a real interface grows beyond these two words.

Inspect what the generator actually declared

The header below is the text emitted for these imports. It is generated interface evidence, not a third-party runtime library. The extern "C" section also prevents the C++ implementation from exporting a differently mangled name.

// Verilated -*- C++ -*-
// DESCRIPTION: Verilator output: Prototypes for DPI import and export functions.
//
// Verilator includes this file in all generated .cpp files that use DPI functions.
// Manually include this file where DPI .c import functions are declared to ensure
// the C functions match the expectations of the DPI imports.

#ifndef VERILATED_VTB__DPI_H_
#define VERILATED_VTB__DPI_H_  // guard

#include "svdpi.h"

#ifdef __cplusplus
extern "C" {
#endif


    // DPI IMPORTS
    // DPI import at example.sv:6:31
    extern int byte_sum(const svOpenArrayHandle values);
    // DPI import at example.sv:3:31
    extern int fixed_sum(const int* values);
    // DPI import at example.sv:5:31
    extern int open_sum(const svOpenArrayHandle values, int expected_size);
    // DPI import at example.sv:4:31
    extern int packed_sum(const svBitVecVal* values);

#ifdef __cplusplus
}
#endif

#endif  // guard

The open integer and signed-byte functions both receive svOpenArrayHandle, while the fixed array and packed vector receive different pointer types. That similarity at the handle boundary makes the SystemVerilog source part of the contract: a generic handle does not convey permission to reinterpret its element storage arbitrarily. The generated header should travel with the exact declaration version used for a build, rather than being copied from an older integration.

Read the observed results case by case

The recorded Verilator 5.052 run returned the following values. The bounds printed by the C-side functions are part of that run, not bounds inferred from a language tutorial.

CaseObserved bounds or representationResultMeaning
Fixed int arrayThree fixed elements15Values 10, −2 and 7 crossed the interface.
Packed vectorTwo 32-bit words18Low word 7 and high word 11 were read.
Open ascendingLeft 5, right 7, size 360Nonzero ascending bounds were traversed.
Open descendingLeft 7, right 5, size 360Descending bounds were traversed.
Open wrong sizeLeft 0, right 1, size 2−1Rejected before the element loop.
Signed bytesLeft 2, right 3, size 21−1 and 2 retained signed-byte meaning.

The wrong-size result is expected rejection, not a failed test. It demonstrates one guard in this implementation; it does not prove that every malformed handle or every possible element type is safe. The code is called by the generated simulation model with valid handles. Fabricating foreign handles or retaining their pointers beyond the call is outside this example’s contract.

Keep a deliberate type mismatch in the check

// SPDX-License-Identifier: MIT
#include "Vtb__Dpi.h"

// Deliberate transport-contract mismatch; compile rejection is expected.
extern "C" int fixed_sum(const short* values) {
  return values[0] + values[1] + values[2];
}

This separate source includes the generated header and then changes the fixed-array element pointer from int to short. Apple clang 21 rejected it with error: conflicting types for 'fixed_sum'. The negative file was checked for syntax only and was never linked into a model or executed. The full diagnostic, including the previous generated declaration, remains in the public run record.

A build failure at that point is a transport-contract finding. It is not evidence that the RTL algorithm produced a bad result. The distinction saves time when debugging a co-simulation failure: check declaration agreement, linkage, bounds and known-value round trips before investigating a disagreement in the modeled computation. The integration handoff guide extends that approach to parameters and register descriptions.

Agree on model behavior after transport passes

A reference model and RTL need a shared reset contract. Specify which transaction state is cleared, whether an in-flight request is cancelled or replayed, and which outputs remain meaningful during recovery. This array-only example has no persistent model state or reset sequence, so a passing sum cannot validate any of those choices. Add explicit before-reset and after-reset transactions when the model gains state.

Ordering is a separate agreement. Associate each request and result with a transaction identity when operations can overlap; do not assume that callback order necessarily means architectural completion order. Numeric precision is another: define signedness, widths, overflow policy, rounding and tolerated error before comparing values. A reference computation using a wide integer or floating-point type can disagree with correct narrow RTL if its comparison rule ignores those differences.

Keep a small transport test in the regression even after the behavioral comparison exists. It can identify a broken foreign-function connection without relying on the behavior of the design under test. Then use independent expected outcomes for the real algorithm. Agreement between two implementations that share the same mistaken requirement remains agreement, not verification of that requirement.

Record the unsupported tool outcome

Icarus 13.0 was given the same SystemVerilog file. It reported syntax errors and Invalid module item at the four DPI-C imports, so no Icarus executable or foreign-function result was produced. This pinned Icarus release has no DPI-C path for these imports; substituting VPI would exercise a different interface. The simulator observation reference keeps other engine-specific observations separate from this DPI result.

Verilator documents DPI-C imports and exports, generated headers and header-isolation rules1. Its release and command documentation identify the tested 5.052 tool and build options2. The results here remain limited to that version, the recorded compiler and these exact source bytes; a second simulator, different data layout or additional language construct needs its own run.

Not covered on this page

Sources

  1. Verilator: connecting to models, DPI and header-isolation sections, guide 5.052; read 24 October 2026. ↩

  2. Verilator v5.052 release and command arguments, release 5 September 2026; read 24 October 2026. ↩

Code on this page is original and MIT licensed.

How we test: methodology. Found a mistake? Corrections.