Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  $c_add(2, 3)        = 5
  $c_add(-7, 4)       = -3
  $c_add(2147483647,0)= 2147483647

Three 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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 3

Excellent 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  tb_add.v:4: Error: System task/function $c_add() is not defined by any module.
  exit 3

Word 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  Assertion failed: (expr), function vpi_get_value, file vpi_priv.cc, line 1027.
  exit 134

Loud — 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  result when the glue never writes one: 0 (0x00000000)
  exit 0

The variable had been set to 0xDEADBEEF immediately before the call. After it, 0 — and a clean exit.

The four pieces of a minimal call from a testbench into C, each broken on its own and the result measured. With everything connected the function returns its answers and the run exits 0. With the C module not loaded, the simulator refuses to run at all, names the testbench line and the function, and exits 3. With the function registered under a different name than the testbench calls, the message is identical to the previous row and the exit code is also 3, so the diagnostic cannot distinguish a missing module from a misspelled name. With the testbench passing the wrong number of arguments, the simulator aborts on an assertion inside its own source file and exits 134, pointing at the simulator's code rather than at the testbench line that caused it. And with the glue never writing a return value, the call succeeds, the variable that held a recognisable marker reads 0, and the run exits 0: the only one of the four failures that exits cleanly is the only one that produces a wrong answer.The piecebroken one at a timeWhat the simulatorsaysmeasuredExit codemeasuredHow findablethe column that mattersEverythingconnectedthe reference runthe answers5, then -3, then21474836470a clean run—nothing to findThe C module is notloadedthe run forgets to nameitnot defined by anymoduleand it names yourtestbench line3it refuses to run at allloud and specificthe best caseRegistered underanother nameone letter outthe identicalmessageword for word3also refuses to runloud, and the sameas the row abovetwo causes, one messageWrong argumentcountthe testbench passes one,the glue reads twoan assertion insidethe simulatorit names its own file andline, not yours134abortedloud, and pointingat the wrong filethe message is about thesimulatorThe glue neverwrites a resultthe value is computed anddropped0where a recognisablemarker had been0a clean runsilentthe only one that exits 0is the only one that iswrong12
Figure 1 — each piece broken on its own, with what the simulator said and what it exited with. The three middle rows are the good news: a missing module, a misspelled name and a wrong argument count all stop the run, and two of the three name your testbench line. The caveats are in the right-hand column — the second and third rows produce the identical message from two different causes, and the fourth points at the simulator's own source file rather than at the code that caused it. The bottom row is the one to remember. The glue computing a value and never handing it back is the only failure of the four that exits 0, and it is the only one that produces a number rather than an error. A function that is completely disconnected in this way returns zero, which is why the first test anybody writes for a new import matters so much.

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

  1. 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.
  2. DPI still does not run here — 0 hits for svdpi.h, 0 "dpi" in iverilog's manual — but the call does, through VPI. Declaration syntax is from the LRM; everything about the call being made is measured.
  3. Three pieces: the C function (yours, testable without a simulator), the glue, and the SystemVerilog declaration.
  4. The reference run, measured: 2 + 3 = 5, -7 + 4 = -3, 2147483647 + 0 = 2147483647, exit 0 — connection, sign, and full width.
  5. Module not loaded: names your testbench line, refuses to run, exit 3.
  6. Name mismatch: the identical message, exit 3 — so the diagnostic covers two different fixes.
  7. Wrong argument count: an assertion naming the simulator's own file, exit 134.
  8. The glue never writes the result: prints 0, exit 0.
  9. 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.
  10. The rule: never confirm a new import with a test whose correct answer could be zero — a disconnected function returns zero.
  11. Confirm in isolating order: the C function in a C main first, then the connection with a non-zero value, then one change at a time.

9. Exercises

  1. Write a two-argument C function and call it from a plain C main before involving a simulator at all. What have you already ruled out?
  2. Choose three confirming values for an import of your own, one each for connection, sign, and full width. Say what each would fail on.
  3. Make the glue compute the result and not write it back. What value reaches the testbench, and what does the run exit with?
  4. Write the first test you would naturally have written for add. Would it have passed case C?
  5. 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?
  6. Pass the wrong number of arguments. Which file does the message name? Describe how you would have reached the real cause from that message.
  7. §6 says a disconnected function returns zero rather than garbage. Explain why, in terms of what the simulator does before the call.
  8. 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.