Your first DPI-C call

Call a C function from SystemVerilog, see which file supplies each half of the call, and check the answer in the testbench.

Lesson
D1.1
Time
20 min
Runs on
Verilator with C/C++
Check
5 questions

The architecture team hands over their checksum routine as crc.cpp, and your import line compiles without a complaint. Then the link step stops: undefined reference to crc_byte, although the function is right there in the file. The compiler treated it as a C++ function and gave it a mangled name, so nothing called crc_byte exists for the simulator to find. This lesson shows which file supplies each half of a DPI call, and why that name has to stay plain C.

The build output. Verilator accepted the import and the C++ compiler accepted the file; the linker found no symbol with the plain name.
Vtb__ALL.cpp:(.text+0xc4): undefined reference to `crc_byte'
collect2: error: ld returned 1 exit status
make: *** [Vtb.mk:67: Vtb] Error 1

Three files, one call

The Direct Programming Interface (DPI) is the part of SystemVerilog that lets a testbench call C, and C call back. An import "DPI-C" line gives SystemVerilog the name, the argument directions and the types of one C routine. It is a declaration only: the body lives in a C file that is compiled and linked into the same simulation. The simulator keeps time, the clock and the end of the run; a call into C starts no new simulator and no new thread.

One call, three filesArrows between files show which file produces or checks which. The bottom arrow is the call at run time: the request goes right, the result comes back.
One call, three filesArrows between files show which file produces or checks which. The bottom arrow is the call at run time: the request goes right, the result comes back. SystemVerilog main.sv import "DPI-C"function intadd_small(input int a,b); generated Vtb__Dpi.h extern "C" intadd_small(int a, intb); C bridge.cpp int add_small(int a,int b) { return a + b;} binary Vtb one program: thesimulation and thebridge run time add_small(7, 5) the testbench calls, Creturns 12 Verilator writesincluded and comparedcompiled incompiled incallruns the body
  1. Declare

    The import line names the function, its result type and its two int inputs. Nothing in SystemVerilog says what it does.

  2. Generate

    Verilator reads every import and writes Vtb__Dpi.h: one C prototype per import, with C linkage.

  3. Define

    The bridge includes that header and defines the function. The compiler compares the definition with the prototype and stops on any difference.

  4. Link

    The generated simulation code and the bridge are compiled into one program. The linker joins the call to the C definition.

  5. Call

    At run time the testbench calls add_small(7, 5). C adds, returns 12, and the testbench checks the value before printing it.

    Follow the calls, values and waits
    1. call add_small(7, 5): main.sv → add_small(7, 5)
    2. call add_small(7, 5): add_small(7, 5) → bridge.cpp
    3. return 12: bridge.cpp → add_small(7, 5)
    4. return 12: add_small(7, 5) → main.sv
Read the diagram as text
  • main.sv: import "DPI-C" function int add_small(input int a, b);
  • Vtb__Dpi.h: extern "C" int add_small(int a, int b);
  • bridge.cpp: int add_small(int a, int b) { return a + b; }
  • Vtb: one program: the simulation and the bridge
  • add_small(7, 5): the testbench calls, C returns 12
  • main.sv → Vtb__Dpi.h: Verilator writes
  • Vtb__Dpi.h → bridge.cpp: included and compared
  • main.sv → Vtb: compiled in
  • bridge.cpp → Vtb: compiled in
  • main.sv → add_small(7, 5): call
  • add_small(7, 5) → bridge.cpp: runs the body

Read the three files as one unit: the import declares, the generated header carries the prototype, the bridge defines.

Keep the generated prototype as your reference when you change an argument type. The bridge includes Vtb__Dpi.h, so the C++ compiler compares every definition with the prototype Verilator wrote from your import. Change the import to input byte a, b and the build stops in the bridge with conflicting declaration: the header now says char, the definition still says int. Without the include, that same mismatch would build cleanly and hand C the wrong bytes.

Why the name has to stay plain C

C++ gives every function a mangled name that encodes its argument types, so that two overloads can live side by side. C has no overloads: a C function's symbol is its name, and that plain name is what the simulator looks for. The generated header wraps its prototypes in extern "C", and a definition that follows such a declaration keeps the C linkage. Writing extern "C" on the definition as well keeps the file right even when someone drops the include.

A C++ function with a C++ name

// crc.cpp, no header, no linkage
int crc_byte(int a) { return a * 7; }

Compiles, but the symbol is _Z8crc_bytei. The link step looks for crc_byte and stops.

The same body with C linkage

#include "Vtb__Dpi.h"
extern "C" int crc_byte(int a) { return a * 7; }

The header checks the types, the symbol is crc_byte, and the link succeeds.

ImportWhat the C code may doUse it for
import "DPI-C" functionRead and write its own C state, call other C code; it must return without consuming simulation timeMost models and wrappers
import "DPI-C" pure functionCompute from its arguments only, changing nothing anyone can observeArithmetic, lookups, format conversions
import "DPI-C" context functionEverything above, plus call exported SystemVerilog routines and ask which scope called itCallbacks into the testbench (D1.7)

Three kinds of import. The default is the middle ground; pure promises more, context asks for more.

Mark a function pure only when it is

add_small depends on its two inputs alone and changes nothing, and its documented operand range keeps the sum inside a C int. That makes it a fair pure import. The simulator may then drop a pure call whose result is unused, and reuse a result when the arguments repeat. A file writer, a random generator, a counter or anything that calls back into SystemVerilog must stay a plain import, or its side effects may silently vanish.

A plain import function still returns before the next statement runs, on the simulator's own thread. No simulation time passes inside it: a sleep in C stalls the wall clock and the whole simulation with it, while the SystemVerilog clock stays still.

Check the answer where you can see it

The testbench asserts the hand-calculated result, 7 + 5 = 12, before it prints anything. Keep the check in SystemVerilog: the C side computes, the testbench decides. A number that matches the number the model printed proves nothing when both came from the same code.

  1. Resolve the symbol

    The generated prototype and the linked definition agree on int add_small(int, int) with C linkage.

    SystemVerilogint add_small(int, int)
    C symboladd_small
  2. Check a known answer

    The caller compares the returned value with a literal it worked out by hand, then prints.

    inputs7, 5
    expected12

Industrial reference models usually arrive as C libraries. Keep the wrapper thin, write down the valid input ranges, and test the wrapper as carefully as the model behind it.

Try it

Run it as it is. Then change the inputs to -8 and 3 and add a second assertion for 0 + 0, keeping both operands inside the documented range where the C addition cannot overflow.

Then change the import to input byte a, b and run again. The build stops in bridge.cpp with conflicting declaration: the header Verilator wrote now says char, while the definition still says int.

RUNVerilator + C/C++ over DPI · simulator service
Edit the code, then run it
Expected: sum=12
Read included file: bridge.cpp
#include "Vtb__Dpi.h"
#include "svdpi.h"
#include <cstdint>
#include <climits>
#include <cstring>
// The generated header above declares: extern "C" int add_small(int a, int b);
// The definition below must agree with it, or the build stops here.
extern "C" int add_small(int a, int b) { return a + b; }

Check yourself

Predict the outputThe bridge defines sub as a - b. What line does this program print?
`timescale 1ns/1ps
module tb;
  import "DPI-C" function int sub(input int a, input int b);
  initial begin
    $display("%0d %0d", sub(9, 4), sub(4, 9));
    $finish;
  end
endmodule
Read included file: bridge.cpp
#include "Vtb__Dpi.h"
#include "svdpi.h"
#include <cstdint>
#include <climits>
#include <cstring>
extern "C" int sub(int a, int b) { return a - b; }
ConceptIn the example, which file holds the body of add_small, the code that does the addition?
Spot the bugThis bridge leaves out Vtb__Dpi.h, so nothing compares the import with the definition int add_small(int a, int b). Each option is the import line. Which one makes the check print FAIL?
Read included file: bridge.cpp
// This bridge leaves out Vtb__Dpi.h on purpose: nothing checks the import against it.
extern "C" int add_small(int a, int b) { return a + b; }
ConceptWhat does the pure qualifier on an import promise the simulator?
ConceptThe link step reports undefined reference to crc_byte, although crc.cpp defines int crc_byte(int). What is the most likely cause?

Remember

  • An import "DPI-C" line declares a C routine: name, directions and types. The body is a C file compiled and linked into the same simulation.
  • Verilator writes Vtb__Dpi.h from your imports. Include it in the bridge, so a definition that disagrees with an import stops the build instead of passing wrong values.
  • The C function keeps a plain C name. Use extern "C" on the definition; a mangled C++ name shows up as undefined reference at link time.
  • Declare pure only for a function that depends on its arguments alone and changes nothing; the simulator may skip or reuse such calls.
  • Keep the check in SystemVerilog: the C side computes, the testbench compares against a value it worked out by hand.