C Part II · Module 22 · Calling C from SystemVerilog
A Minimal Imported Function End to End
Two modules of boundary rules, and now the call itself — running, in a real simulation, returning real answers. The chapter builds the smallest complete version and then breaks each piece on its own to see what the simulator says about it. Three of the four failures refuse to run or abort outright. The fourth exits 0, prints a plausible number, and is the only one of the four that is actually wrong — which is also why the first test everybody writes for a new import is the one test it would pass while completely disconnected.
Advanced23 min readCPart IIVerificationDPI-C
Part II · Module 22 · Chapter 22.1 · A Minimal Imported Function End to End
1. The Smallest Complete Thing
Modules 20 and 21 settled what happens to data at the boundary. This module is about the call, and it starts where every working import starts: with the smallest version that does something, confirmed piece by piece, before anything is added.
Smallest means a function with no state, no pointers, no arrays and no strings — two integers in, one integer out:
static int add_i32(int a, int b) { return a + b; }That is the whole C function, and the point of choosing it is that nothing in it can be wrong. Chapter 20.3 settled that SystemVerilog's int is 32 bits and maps to C's int32_t, so there is no width question; there is no signedness question past that, no lifetime, no layout. Anything that goes wrong in this chapter is therefore in the wiring, which is exactly what we want to look at.
2. What Runs Here, and What Does Not
This module can do something Modules 20 and 21 could not, and the limits need stating once, clearly, before any measurement is quoted.
Re-verified this session: there is no svdpi.h anywhere on this machine (0 hits), iverilog's manual contains the string "dpi" 0 times, and iverilog-vpi is present. So DPI itself still cannot run here — Chapter 19.1 §4's finding stands.
But the call can. Icarus supports VPI, the older C interface, and through it a plain C function can be registered, called from a running simulation, and have its result handed back. That is enough to measure everything this chapter is about.
So the labelling rule for all of Module 22, used without exception:
- From the LRM — the
import "DPI-C"declaration, its syntax, and the rules about what may be declared. - Measured — a C function actually called from a running simulation, what it returns, and what each failure mode looks like. The binding used for the measurement is VPI.
The reason this is worth doing rather than skipping is that the C side is the same either way. In both mechanisms you write a plain C function with plain C types, and some glue connects it to the simulator. The glue differs; the function, its arguments, its return value, and every way the connection can be wrong do not. This curriculum is about the C.
3. The Three Pieces
Every import, in either mechanism, is three things, and naming them is what makes §5 possible.
The C function. Plain C. No simulator types in it. This is the part you test, review and reuse, and it is the part that should contain all the logic:
static int add_i32(int a, int b) { return a + b; }The glue. Reads the arguments the simulator is holding, calls the function, and writes the result back. Under DPI the compiler generates most of it from the declaration; here it is explicit, which is useful, because it makes visible the step that §5 will show failing silently:
vpi_get_value(vpi_scan(args), &v); int a = (int)v.value.integer;
vpi_get_value(vpi_scan(args), &v); int b = (int)v.value.integer;
v.value.integer = add_i32(a, b); /* call */
vpi_put_value(sys, &v, NULL, vpiNoDelay); /* hand it back */The declaration on the SystemVerilog side, which tells the simulator the function exists and what its arguments are. From the LRM, under DPI that is:
import "DPI-C" function int add_i32(input int a, input int b);Keeping the three apart matters for a reason that outlives this chapter: the first is yours and is testable in C; the second and third are a connection. Chapter 21.3 §5 made the same argument for calling your model from a C main — everything in the first box can be checked without a simulator at all.
4. The Reference Run
Measured, the C function called from a running simulation:
$c_add(2, 3) = 5
$c_add(-7, 4) = -3
$c_add(2147483647,0)= 2147483647Three values chosen deliberately rather than for variety. 2 + 3 confirms it is connected at all. -7 + 4 confirms the sign survives — Chapter 20.4's territory, checked here with the cheapest possible case. 2147483647 + 0 confirms the full 32-bit width arrives intact, which is Chapter 20.3's mapping actually exercised rather than assumed.
The run exits 0. That is the reference; everything in §5 is a departure from it.
5. Breaking Each Piece
The purpose says confirm each piece, and the only way to know what a confirmation is worth is to see what its absence looks like.
A — the C module is not loaded. The glue exists and is compiled; the run simply does not load it.
tb_add.v:4: Error: System task/function $c_add() is not defined by any module.
tb_add.vvp: Program not runnable, 3 errors.
exit 3Excellent behaviour: it names your testbench line, names the function, and refuses to run.
D — registered under a different name. The module loads; the glue registers $c_addd and the testbench calls $c_add.
tb_add.v:4: Error: System task/function $c_add() is not defined by any module.
exit 3Word for word the same message. So the diagnostic tells you the name the simulator could not find and not whether the module is missing or the name is misspelled — two quite different fixes behind one sentence.
B — the wrong number of arguments. The testbench passes one argument; the glue reads two.
Assertion failed: (expr), function vpi_get_value, file vpi_priv.cc, line 1027.
exit 134Loud — an abort — and pointing at the simulator's own source file, not at the testbench line that caused it. A reasonable first reading of that message is "the simulator has a bug".
C — the glue never writes the result back. The arguments are read, the function is called, the value is computed, and vpi_put_value is simply not reached.
result when the glue never writes one: 0 (0x00000000)
exit 0The variable had been set to 0xDEADBEEF immediately before the call. After it, 0 — and a clean exit.
6. The Finding, and the One Rule It Gives You
Line the four failures up by exit code. Measured: 3, 3, 134, and 0.
Three of the four refuse to run or abort. One completes cleanly and returns a number. And the one that completes cleanly is the only one of the four that produces a wrong answer — the other three produce no answer at all, which is a far better outcome.
Now notice what the wrong answer is. It is zero, and it is zero for a structural reason rather than by chance: the simulator prepared a place for the result, nothing wrote to it, and what was there is what you get. A function whose result is never handed back does not return garbage — it returns the quietest possible value.
Which gives the chapter's practical rule, and it is the kind of thing that is obvious only after someone says it:
Never confirm a new import with a test whose correct answer could be zero.
add(0, 0) == 0 passes with the C function entirely disconnected. So does add(5, -5) == 0. So does any first test written around an identity, an empty payload, a cleared flag, or a "nothing happened" case — and those are exactly the cases people reach for first, because they are the simplest.
What to use instead, and all three are in §4's reference run:
- A value that cannot arise by accident.
2 + 3 = 5. Not 0, not 1, not the arguments themselves. - A value that exercises the sign.
-7 + 4 = -3, which also fails if the width or signedness is wrong (20.4). - A value that fills the width.
2147483647, which fails if anything truncates (20.3).
Three calls, and between them they confirm that the function is connected, that the result comes back, and that it comes back intact.
7. The Misconception: If It Runs Clean, the Binding Works
"The simulation compiled, it ran, it exited 0 and it printed a number. The import is connected."
Measured false. Case C exited 0, printed a number, and the result was never handed back at all.
And the subtler half, which is what makes this worth a section rather than a sentence: in case C the C function was genuinely called, and it genuinely computed the right answer. add_i32(2, 3) ran and returned 5. Nothing was wrong with the C, the arguments arrived correctly, and the logic was right.
The failure is in the last step only — the handing back — and no part of the run has an opinion about it. The simulator cannot know the glue meant to write something; the C function cannot know its return value was dropped; the testbench receives a perfectly ordinary zero.
So a clean exit is evidence about three of the four pieces and silent about the fourth, and the fourth is the one that decides whether any of the work reached you.
The corrected position:
A clean run confirms that the module loaded, the name matched and the arguments were readable. It says nothing about whether the result came back — and the value you get when it does not is zero, which is why the confirming test must never be one whose right answer is zero.
8. What You Should Be Able to Say Now
- Start with a function that cannot be wrong — two
ints in, one out, no state, no pointers, no arrays, no strings. Then anything that fails is the wiring. - DPI still does not run here — 0 hits for
svdpi.h, 0 "dpi" iniverilog's manual — but the call does, through VPI. Declaration syntax is from the LRM; everything about the call being made is measured. - Three pieces: the C function (yours, testable without a simulator), the glue, and the SystemVerilog declaration.
- The reference run, measured:
2 + 3 = 5,-7 + 4 = -3,2147483647 + 0 = 2147483647, exit 0 — connection, sign, and full width. - Module not loaded: names your testbench line, refuses to run, exit 3.
- Name mismatch: the identical message, exit 3 — so the diagnostic covers two different fixes.
- Wrong argument count: an assertion naming the simulator's own file, exit 134.
- The glue never writes the result: prints 0, exit 0.
- The finding: three of four failures refuse to run or abort; the one that exits 0 is the only one that returns a wrong number.
- The rule: never confirm a new import with a test whose correct answer could be zero — a disconnected function returns zero.
- Confirm in isolating order: the C function in a C
mainfirst, then the connection with a non-zero value, then one change at a time.
9. Exercises
- Write a two-argument C function and call it from a plain C
mainbefore involving a simulator at all. What have you already ruled out? - Choose three confirming values for an import of your own, one each for connection, sign, and full width. Say what each would fail on.
- Make the glue compute the result and not write it back. What value reaches the testbench, and what does the run exit with?
- Write the first test you would naturally have written for
add. Would it have passed case C? - Break the registration name by one letter and then remove the module entirely. Compare the two messages. Which file would you have opened first, and would that have been the right one?
- Pass the wrong number of arguments. Which file does the message name? Describe how you would have reached the real cause from that message.
- §6 says a disconnected function returns zero rather than garbage. Explain why, in terms of what the simulator does before the call.
- List every test in a suite you know whose expected result is 0, 1, or an empty value. Which of them could pass against a disconnected implementation?
10. Summary
- Smallest first: two
ints in, one out. Nothing in the function can be wrong, so every failure is wiring. - The call runs here through VPI — from the LRM for the DPI declaration, measured for everything about the call. DPI itself: 0
svdpi.h, 0 "dpi" in the manual. - Three pieces: the C function · the glue · the declaration. Only the first is testable without a simulator — so test it there.
- Reference run, measured:
5,-3,2147483647, exit 0 — connection, sign, width. - Module missing → names your line, exit 3. Name mismatch → the identical message, exit 3. Wrong argument count → an assertion in the simulator's file, exit 134.
- Result never written back → prints 0, exit 0.
- The finding: the only failure that exits cleanly is the only one that returns a wrong number.
- The rule: never confirm a new import with a test whose right answer could be zero.
- A clean run confirms load, name and arguments — and is silent about whether the answer came back.
The smallest import returns the sum of its arguments, which is the one case where the result's type is not in question. A verification-grade import returns something computed — a status, a width, an encoded field — and the moment the result stops being an obvious int, the question of what it should be declared as on each side stops answering itself.
Standards & specifications
- Governing standard
- IEEE Std 1800 (SystemVerilog)(opens IEEE in a new tab)
Defines SystemVerilog language semantics — syntax, data types, scheduling and the behaviour a conforming simulator must produce. It does not define tool-specific synthesis support or vendor methodology.
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.
