Skip to content
VLSI Mentor

C Part II · Module 25 · Debugging DPI-C and Simulator Integration

Signature Mismatches Between SystemVerilog and C

A declaration lives in one file and its definition in another, and no compiler reads both — so every disagreement between them is caught late or not at all. Five of them measured: two that refuse to run with the identical message from completely different causes, one that aborts on an assertion naming the simulator's own source file rather than your call, one caught only when the program is elaborated, and one that is never caught and returns a number. The defence for the worst of them is three lines, because a called function can count its own arguments.

Expert23 min readCPart IIVerificationDPI-C

Part II · Module 25 · Chapter 25.2 · Signature Mismatches Between SystemVerilog and C

1. Why No Compiler Catches These

An import has two halves written in two languages and compiled by two tools. The SystemVerilog declaration is parsed by the simulator; the C definition is parsed by a C compiler. Neither one ever sees the other.

Chapter 21.1 §7 established the consequence and it is worth restating because this chapter is entirely about it: there is no translation unit containing both sides. A mismatch between a declaration and a definition is not something a compiler is failing to check — there is no compiler positioned to check it.

So every disagreement in this chapter is caught by the running simulation, or by nothing. That is why all five messages below arrive at run time.

2. The Five Disagreements

Nothing registered that name. Measured:

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

Refuses to run, names your testbench line, names the function. The best diagnostic in the module.

Registered under a different name. One letter out. Measured:

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 identical. §3.

Wrong number of arguments. The caller passes one; the glue reads two. Measured:

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

Loud, and pointing at the simulator's own source file. §4 is the fix.

A kind mismatch — a function used as a statement, or a task in an expression. Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  iverilog exit: 0
  vvp exit: 1
  tb_misuse.v:4: Error: $c_fn() is a system function, it cannot be called as a task.

The compiler exits 0; the refusal comes at elaboration. It names your line and the exact problem, which is good — just later than people expect.

The result is never written back. Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  exit 0, and the testbench receives 0

Never caught, by anything, and the only one of the five that produces a number rather than an error — Chapter 22.1 §6's rule, which is why a new import must never be confirmed with a test whose right answer could be zero.

Five ways a declaration and a definition can disagree, with what each one does when it is the only fault. A name that nothing registered refuses to run, exits 3 and names the testbench line. A name registered with one letter different gives the identical message and the identical exit code, so the diagnostic covers two quite different fixes. A wrong argument count aborts mid-run with exit 134 on an assertion inside the simulator's own source file, which names the simulator rather than the call that caused it. Using a function where a task is expected, or the reverse, compiles with exit 0 and then refuses to run with exit 1, naming the line and saying that a system function cannot be called as a task. And a result the glue never writes back is the one that exits 0 and returns zero, which is the only one of the five that produces a number rather than an error. The bottom row is the defence: the callee can count its own arguments, measured at 1, 2 and 4, so an arity check is three lines and turns the loudest misdirected message into a precise one.The disagreementdeclaration againstdefinitionWhen it is caughtmeasuredExitmeasuredWhere the messagepointsNothing registeredthat namethe library never loaded,or never registeredbefore anysimulationit refuses to run3measuredyour testbench linethe best case in themoduleRegistered under adifferent nameone letter outbefore anysimulationthe same point3measured: identicalthe same message,word for wordtwo causes, onediagnosticWrong number ofargumentsthe caller passes fewerthan the glue readspart-way throughthe runwhen the call is reached134measured: abortedthe simulator's ownsource filenot the call that causeditA kind mismatcha function used as atask, or the reverseat elaboration, notcompilationmeasured: the compilerexits 01measured: refuses to runyour line, and theexact kindcannot be called as ataskThe result is neverwritten backthe glue computes anddrops itnevernothing checks it at anypoint0measured: a clean runreturning 0nowherethe only one of the fivethat returns a numberThe defence is three lines: the callee can count its own argumentsmeasured — a call declared with 1, 2 and 4 arguments reported 1, 2 and 4, so an arity check turns the misdirectedassertion into a message that names your call12
Figure 1 — five ways a declaration and a definition can disagree, with when each is caught and where its message points. The second row is the one that costs time: a misspelled registration produces the identical message and exit code as a library that never loaded, so the diagnostic covers two quite different fixes and editing a name is as likely to be wrong as right. The third row is loud and misdirected — an assertion inside the simulator's own source, caused entirely by the call. The last row is the quiet one, caught by nothing and returning a plausible zero. The bar at the bottom is the defence for the misdirected case, and it is three lines: a called function can count the arguments it was actually given.

3. Two Causes, One Message

The most expensive row in the figure is the second, because the message is perfect and tells you the wrong thing.

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

That sentence is produced when:

  • the library was never loaded — a missing -m flag, a wrong path, a build that did not run
  • or the library loaded fine and registered the name as $c_addd

Measured identical, word for word, same exit code 3. And the two fixes are entirely different: one is a command line, the other is a line of C.

So the first move is not to edit either name. It is to establish what the library actually exports, which costs one command:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
nm -gU libmodel.dylib | grep -i mysym     # 25.6

If the symbol is absent, the problem is the build. If it is present, the problem is the loading or the registration — and Chapter 25.7's loader distinguishes those two in turn.

The general shape is worth naming because it recurs: when one message covers several causes, the next step is a cheaper measurement that splits them, not a guess at which one it is.

4. The Assertion That Names the Wrong File

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

A reasonable first reading is "the simulator has a bug". It does not. The glue asked for an argument that the caller did not pass, and the simulator asserted rather than returning something meaningless — which is the right behaviour and the wrong message for your purposes.

The fix is three lines, and it is available because the callee can count its own arguments. Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  declared 1 -> [C] this call passed 1 argument(s)
  declared 2 -> [C] this call passed 2 argument(s)
  declared 4 -> [C] this call passed 4 argument(s)

So:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int n = 0;
vpiHandle it = vpi_iterate(vpiArgument, sys);
if (it) while (vpi_scan(it)) n++;
if (n != 2) { vpi_printf("myfn: expected 2 arguments, got %d\n", n); return 0; }

That converts an abort inside vpi_priv.cc into a message naming your function and the actual count. Under DPI the compiler generates the argument handling from the declaration, so this specific shape does not arise — but the principle does, and it is Chapter 23.6's: check what you were handed, and say so.

5. The Misconception: A Mismatch Would Be Obvious

"The declaration and the C function are both in front of me. If the argument lists disagreed I would see it."

They are never both in front of the toolchain, which is what matters, and §1 is why.

Three reasons the human version of the check is weaker than it feels:

The two halves are written in different notations. A SystemVerilog output int argument and a C int * parameter correspond, and checking the correspondence means holding a mapping in your head — which is Module 20's entire subject.

They live in different files, usually different directories, often different repositories. Nothing puts them side by side at the moment either is edited.

And the failure is asymmetric in time. Adding an argument to the C and forgetting the declaration gives §2's abort; adding one to the declaration and forgetting the C gives a different one. Neither edit is flagged at the moment it is made.

The accurate position:

Nothing compares the two sides but a running simulation, so make the simulation do it early and deliberately. One confirming call with an unmistakable value, run as the first thing after any signature change, catches four of the five disagreements in this chapter — and an arity check in the C turns the fifth from an abort in somebody else's file into a sentence naming yours.

6. What You Should Be Able to Say Now

  1. No compiler sees both sides — there is no translation unit containing the declaration and the definition (21.1 §7).
  2. Nothing registered that name: exit 3, names your testbench line — the best diagnostic in the module.
  3. A name registered differently: the identical message, identical exit 3.
  4. So the first move is nm, not editing a name — establish what the library actually exports.
  5. When one message covers several causes, split them with a cheaper measurement rather than guessing.
  6. Wrong argument count: exit 134, an assertion naming vpi_priv.cc, caused by the call.
  7. The callee can count its own arguments — measured, 1, 2 and 4 — so an arity check is three lines.
  8. A kind mismatch is caught at elaboration: compiler exit 0, simulator exit 1, naming your line.
  9. A result never written back is caught by nothing: exit 0, returning a plausible zero.
  10. Four catch times, with usefulness falling: before the run · at elaboration · part-way · never.
  11. "It builds" says almost nothing about an import; the first run says most of it.
  12. One confirming call with an unmistakable value exercises registration, name, argument count and write-back together.

7. Exercises

  1. Produce the "not defined by any module" message two ways — a missing library and a misspelled registration. Are the messages distinguishable?
  2. Run nm on your DPI library and find the symbol you expect. What does its absence mean, and what does its presence rule out?
  3. Pass fewer arguments than the glue reads. Which file does the message name, and which file is at fault?
  4. Add an argument count check to a C function. Produce the same failure and compare the two messages.
  5. Use a function where a task is expected. Record both the compiler's and the simulator's exit codes.
  6. Make a glue function compute a result and not write it back. What does the testbench receive, and what would have caught it?
  7. For each of the five disagreements, say when it is caught and what you would have lost by then.
  8. Write the single confirming call you would run after any signature change. Which of the five does it not cover?

8. Summary

  • Nothing compares the two sides but a running simulation — no compiler sees both (21.1 §7).
  • Nothing registered → exit 3, names your line. A misspelled name → the identical message.
  • So run nm first, and split a one-message-many-causes diagnostic with a cheaper measurement.
  • Wrong argument count → exit 134, assertion in vpi_priv.cc, caused by your call.
  • The callee can count its arguments — measured 1, 2, 4 — so the check is three lines.
  • A kind mismatch → compiler 0, simulator 1, your line named.
  • A result never written back → exit 0 and a plausible zero, caught by nothing.
  • Four catch times, usefulness falling: before · at elaboration · part-way · never.
  • "It builds" says little; one confirming call with an unmistakable value says most of it.

A signature can agree on names, counts and kinds and still be wrong about what the arguments mean. That disagreement produces no message at any of the four times in this chapter — it produces correct-looking numbers, right up to the value where it does not.

Where this fits

Part of the C Programming curriculum.