Skip to content
VLSI Mentor

C++ Part II · Module 28 · C/C++ and SystemVerilog DPI

Importing a C or C++ Function into SystemVerilog

This is the chapter that builds one binding completely before anything is layered on top, because almost every DPI problem is easier to recognise once you have seen the simplest case work. An import is three things: a declaration on the SystemVerilog side saying what the function looks like, a definition on the C++ side exposed under C linkage, and a call site that looks like any other function call. The declaration is where the frozen example for this chapter lives, and it is worth reading closely because every token in it is a commitment — the direction of each argument, its exact type, and the return type all become part of a contract nothing will check. The C++ half is executed here against a pure C caller, which is precisely what the simulator's generated marshalling code amounts to, and it returns the answer worked out by hand. The SystemVerilog half is published as reference syntax, because no simulator on this machine can compile it.

Advanced20 min readC++Part IIVerificationDPI

Part II · Module 28 · Chapter 28.3 · Importing a C or C++ Function into SystemVerilog

1. The Declaration

The frozen example for this chapter, and the shape to memorise:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import "DPI-C" function int crc32(input int data);

Reference syntax — not executed here, for the reason Chapter 28.1 §5 measured. Read left to right, every token is a commitment:

tokenwhat it commits you to
importSystemVerilog calls C; the other direction is export
"DPI-C"the C-compatible DPI layer, as opposed to the never-implemented "DPI"
(no qualifier)neither pure nor context — Chapter 28.11
functionit returns a value and cannot consume simulation time
intthe return type, which the C side must define as C int
crc32the symbol the linker will look for
input int dataone argument, direction in, type int
;it is a declaration — there is no body here

Two notes on things that are easy to get wrong.

"DPI-C" rather than "DPI". The original "DPI" layer was specified and never implemented by anyone; every real flow uses "DPI-C". If you see "DPI" in old code it is a latent portability problem.

function, not task. A function returns a value and must return in zero simulation time. A task may consume time and returns an int status instead. Chapter 28.4 §3 covers the distinction properly; the rule here is that a model computing an answer is a function.

2. The Complete Binding

Three files, and the whole mechanism.

The C++ definition — an ordinary function, exposed under C linkage:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#include "rmodel.hpp"                    // classes, templates, STL: all fine in here

extern "C" int model_step(const unsigned char* in, unsigned len)
{
    rm::Accumulator a;
    for (unsigned i = 0; i < len; ++i) a.feed(in[i]);
    return (int)a.checksum();
}

The SystemVerilog declaration — reference syntax:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import "DPI-C" function int model_step(input byte unsigned in[], input int unsigned len);

And the call site, which is unremarkable and is the point:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
initial begin
  byte unsigned payload[4] = '{10, 20, 30, 40};
  automatic int sum = model_step(payload, 4);
  $display("model_step = %0d", sum);
end

Now the half that can be executed. The simulator's generated marshalling code amounts to a C caller, so a C caller is what I used:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#include <stdio.h>
extern int model_step(const unsigned char* in, unsigned len);   /* C linkage */
int main(void)
{
    const unsigned char d[4] = { 10, 20, 30, 40 };
    printf("  model_step returned %d\n", model_step(d, 4));
    return 0;
}

Compiled with a C compiler, linked against the C++ object, run:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  with extern "C": link exit=0
  model_step returned 100

100 is 10 + 20 + 30 + 40, which is what the model computes, worked out by hand before the program was run. That is a complete crossing: a C-linkage call from code that knows nothing about C++ into a function whose body uses a class, a std::vector and std::accumulate.

And the one thing it depends on, shown here because it is what makes the binding exist at all:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
0000000000000000 T __Z14model_step_cppPKhj
0000000000000210 T _model_step

Two functions with identical bodies and two completely different symbols. The one without extern "C" is __Z14model_step_cppPKhj; the one with it is _model_step, which is exactly the name a DPI import would look for. Chapter 28.5 is that measurement in full, including what the link failure looks like.

3. Where the Declaration May Appear

A detail that costs people an afternoon, because the error message is not obvious.

An import declaration is a scope-level declaration. It may appear in a module, an interface, a program, a package, or at $unit scope — the same places a function declaration may appear. It may not appear inside a function, a task, or an initial block.

The practical arrangement for anything beyond a toy:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// dpi_pkg.sv -- reference syntax
package dpi_pkg;
  import "DPI-C" function int  model_step(input byte unsigned in[], input int unsigned len);
  import "DPI-C" function void model_reset();
endpackage

Putting every import in one package is the arrangement to default to, for three reasons. The declarations are in one place, so the cross-check in Chapter 28.12 §4 has one file to read. Importing the package makes them available everywhere without repetition. And — the reason that matters most — a declaration written twice can be written differently twice, which is a disagreement nothing will catch.

4. The Argument Directions

SystemVerilog argument directions mean something specific at a DPI boundary, and they map onto C in a way worth stating explicitly.

directionC parameterwho may write it
inputby value, or const T* for arraysC may read only
outputT*C writes it; the incoming value is meaningless
inoutT*C reads and writes

Two consequences.

An output argument is a pointer, so the C side must write through it — and if it does not, SystemVerilog sees whatever was there. There is no "unset" state and no diagnostic.

And const on the C side is the honest expression of input. Declaring const unsigned char* in for an input byte unsigned in[] makes the compiler enforce the direction on the C++ side, which is the only place it can be enforced at all. Omitting it compiles and leaves the model free to modify the simulator's storage — one of the ways a DPI model corrupts the simulation rather than merely returning a wrong answer.

The rule: write const on every input pointer parameter. It costs nothing and it is the only check available.

5. Return Types, and the One That Is Not a Value

SystemVerilog returnC returnnote
int, byte, shortint, longintthe mapped scalarChapter 28.7 §2
real, shortrealdouble, float
voidvoidthe import is called as a statement
stringconst char*ownership matters — Chapter 28.10 §2
chandlevoid*an opaque handle — Chapter 28.13 §2

A void import is called as a statement, not in an expression — model_reset(); on its own line. It is the right choice for anything whose purpose is a side effect on the model's state.

And a string return is the one to be careful with, because the C side must return a pointer to storage that outlives the call, and Chapter 28.10 measures both ways of getting that wrong.

6. Building It

The build has two halves and the second is simulator-specific.

Compile the C++ into a position-independent object or shared library, exactly as you would any library. Nothing DPI-specific is required for a binding that uses only scalars and plain arrays — no special header, no macros. (svdpi.h becomes necessary only for open arrays and four-state values, and Chapter 28.1 §5 measured that it is absent on this machine, which is why the models in this module compile here at all.)

Then tell the simulator to load it, with a command-line switch whose name differs between simulators. That is the one step this module cannot demonstrate, and the reason Chapter 28.1 §8's first instruction is to compile a two-line test on your own tools before planning anything.

A detail worth knowing because the error it produces is confusing: the C++ object must be linked with the C++ runtime. If your model uses the standard library — and rm::Accumulator above uses std::vector and std::accumulate — then linking the shared object with a C driver rather than a C++ one produces undefined symbols for library internals rather than for your function. Linking with clang++ or g++ rather than clang/gcc is what fixes it, and that is exactly what the experiment in §2 did.

7. Reading the Declaration Back

A short discipline that catches most hand-written mismatches before any tool runs: read the SystemVerilog declaration and the C definition side by side, token by token, in this order.

The name. Byte for byte. This is the one thing the linker checks, so a typo here gives you a clear error — the best outcome available.

The arity. Count the arguments on both sides. A mismatch links and corrupts, which Chapter 28.12 §2 measures.

Each type, against the mapping table. Not "an integer" — the exact C type from Chapter 28.7 §2. This is where a shortint meets an int and nothing objects.

Each direction, against const. Every input pointer should be const on the C side.

The return type. Including void, and including whether a string return's storage outlives the call.

And the qualifier. pure and context are promises about the C function's behaviour, not about its type, so no amount of signature checking validates them — Chapter 28.11 §3 measures what a false one costs.

Six checks, a minute per binding, and the first three are exactly what Chapter 28.12's tool automates.

8. The Table

the questionthe answermeasured
the canonical declarationimport "DPI-C" function int crc32(input int data);§1
"DPI-C" or "DPI"always "DPI-C" — the other was never implemented§1
function or taskfunction returns a value in zero time; a task may consume time§1
did the C++ half workyes — a C caller linked and returned 100§2
what makes the symbol findableextern "C" — _model_step against __Z14model_step_cppPKhj§2
where may an import be declaredmodule, interface, program, package, $unit — not inside a function§3
where should it be declaredone package, so it cannot be written twice differently§3
what is an output argument in Ca pointer the C side must write through§4
what expresses input in Cconst on the pointer — the only enforceable check§4
what does a void import look likea statement, not an expression§5
is svdpi.h needed for a scalar importno — only for open arrays and four-state§6
what if the model uses the STLlink with clang++/g++, not clang/gcc§6
why build the toy firstit separates mechanism failures from contract failures§6 callout

9. What to Actually Do

  • Build the smallest possible import first — one scalar in, one scalar out, an answer you computed by hand.
  • Keep that toy as a permanent self-test, asserted at the start of every run.
  • Write "DPI-C", never "DPI".
  • Use function for anything that computes an answer; reach for a task only when the call must consume time.
  • Declare every import in one package, so no declaration exists twice.
  • Write const on every input pointer parameter. It is the only direction check that can be enforced.
  • Write the exact C type from the mapping table, not an approximately right integer.
  • Make sure an output argument is actually written by the C side on every path.
  • Link the shared object with a C++ driver if the model uses the standard library.
  • Read the two declarations side by side, token by token — name, arity, types, directions, return, qualifier.

10. Mistakes Worth Making Once

Writing "DPI" instead of "DPI-C". It is the layer nobody implemented.

Declaring an import inside an initial block. It is a scope-level declaration.

Declaring the same import in two modules, slightly differently. Nothing compares them.

Omitting const on an input pointer. The model may now modify the simulator's storage.

Forgetting to write an output argument on one path. SystemVerilog sees whatever was there.

Using function for something that must wait. A function cannot consume simulation time.

Linking a shared object that uses the STL with a C driver. Undefined symbols for library internals, not for your function.

Going straight to the full model. Mechanism failures and contract failures are now mixed together.

Deleting the toy once it worked. It was the cheapest permanent self-test you will ever write.

11. Interview Relevance

The frozen curriculum marks this Critical, and it is often asked as write me a DPI import — which makes it one of the few places in verification interviewing where the expected answer is a line of syntax. Produce it exactly: import "DPI-C" function int crc32(input int data); and then say what the matching C side is: extern "C" int crc32(int data).

What turns a correct line into a good answer is the commentary. "DPI-C" rather than "DPI", because the latter was specified and never implemented. function rather than task, because a function returns a value in zero simulation time, and a task is what you need only when the call must consume time. extern "C" on the definition, because otherwise the symbol is mangled and the linker will not find it — and the measured pair is worth quoting: _model_step with it, __Z14model_step_cppPKhj without.

Two further details carry weight. Declare all imports in one package, because a declaration written twice can be written differently twice and nothing compares them. And const on every input pointer, since that is the only way the argument direction is enforced anywhere in the flow.

If there is room for a habit rather than a fact, the one worth offering is to build the smallest possible import first — one scalar in, one known answer out — and then keep it as a permanent assertion at the start of every run, because it separates "the mechanism is broken" from "the contract is wrong", and those have completely different debugging paths.

12. What You Should Be Able to Say Now

  • The canonical declaration: import "DPI-C" function int crc32(input int data); and what every token commits you to.
  • That "DPI-C" is the only layer anyone implemented, and "DPI" is a latent portability problem.
  • That a function must return in zero simulation time while a task may consume time.
  • That the C++ half is an ordinary function behind extern "C", and that the measured symbols are _model_step with it and __Z14model_step_cppPKhj without.
  • That a pure C caller linked against it and returned 100, the hand-checked sum of 10+20+30+40.
  • That an import may be declared at module, interface, program, package or $unit scope but not inside a function, task or initial block — and that one package is the right home.
  • That output and inout arguments are pointers, that the C side must write an output on every path, and that const is the only enforceable expression of input.
  • That a void import is called as a statement, and a string return raises the ownership question Chapter 28.10 answers.
  • That svdpi.h is not needed for a scalar import — only for open arrays and four-state values.
  • That a model using the standard library must have its shared object linked with a C++ driver.
  • That the smallest working import separates mechanism failures from contract failures, and is worth keeping forever as a known-value assertion.

13. Exercises

Exercise 1 — the frozen line. Write the declaration from memory, then its C definition, then check every token against §1's table.

Exercise 2 — the C-caller stand-in. Write a C main that calls your extern "C" function and link it with clang++. Confirm the answer you computed by hand.

Exercise 3 — the two symbols. Compile the same function with and without extern "C" and compare nm output. Demangle the first.

Exercise 4 — the wrong place. Try declaring an import inside an initial block and read the error.

Exercise 5 — the output argument. Write a function with an output argument and deliberately leave one path without writing it. Decide how you would ever notice.

Exercise 6 — the const check. Omit const from an input pointer and write a line in the model that modifies the caller's buffer. Note that nothing objects.

Exercise 7 — the permanent self-test. Turn your toy import into an assertion that runs at the start of every simulation, and write down which failures it would catch.

14. Summary

  • An import is three things: a SystemVerilog declaration, a C-linkage definition, and an ordinary-looking call. The declaration is import "DPI-C" function int crc32(input int data); and every token in it is a commitment nothing will check.
  • Always "DPI-C", never "DPI"; function for anything returning an answer, since a function cannot consume simulation time.
  • The C++ half was executed here: a pure C caller — which is what the simulator's marshalling amounts to — linked against an extern "C" definition whose body uses a class, std::vector and std::accumulate, and returned 100, the hand-checked sum.
  • And it depends entirely on the linkage: the same body produced _model_step with extern "C" and __Z14model_step_cppPKhj without, which is Chapter 28.5's subject.
  • Declare every import in one package. A declaration written twice can be written differently twice, and nothing compares them.
  • input is const on the C side — the only enforceable direction check anywhere in the flow — while output and inout are pointers the C side must write through on every path.
  • No DPI header is needed for a scalar import; svdpi.h matters only for open arrays and four-state values, and it is absent on this machine.
  • Link the shared object with a C++ driver if the model touches the standard library, or the undefined symbols will name library internals rather than your function.
  • Build the smallest possible import first and keep it forever. It separates loud mechanism failures from silent contract failures, and as a permanent known-value assertion it catches every mismatch class for the price of one call.

Chapter 28.4 turns the arrow around: letting the C++ call back into the testbench, and the question of which scope the call lands in.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.