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:
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 3Refuses to run, names your testbench line, names the function. The best diagnostic in the module.
Registered under a different name. One letter out. Measured:
tb_add.v:4: Error: System task/function $c_add() is not defined by any module.
exit 3Word for word identical. §3.
Wrong number of arguments. The caller passes one; the glue reads two. Measured:
Assertion failed: (expr), function vpi_get_value, file vpi_priv.cc, line 1027.
exit 134Loud, 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:
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:
exit 0, and the testbench receives 0Never 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.
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.
Error: System task/function $c_add() is not defined by any module.That sentence is produced when:
- the library was never loaded — a missing
-mflag, 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:
nm -gU libmodel.dylib | grep -i mysym # 25.6If 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
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:
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:
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
- No compiler sees both sides — there is no translation unit containing the declaration and the definition (21.1 §7).
- Nothing registered that name: exit 3, names your testbench line — the best diagnostic in the module.
- A name registered differently: the identical message, identical exit 3.
- So the first move is
nm, not editing a name — establish what the library actually exports. - When one message covers several causes, split them with a cheaper measurement rather than guessing.
- Wrong argument count: exit 134, an assertion naming
vpi_priv.cc, caused by the call. - The callee can count its own arguments — measured, 1, 2 and 4 — so an arity check is three lines.
- A kind mismatch is caught at elaboration: compiler exit 0, simulator exit 1, naming your line.
- A result never written back is caught by nothing: exit 0, returning a plausible zero.
- Four catch times, with usefulness falling: before the run · at elaboration · part-way · never.
- "It builds" says almost nothing about an import; the first run says most of it.
- One confirming call with an unmistakable value exercises registration, name, argument count and write-back together.
7. Exercises
- Produce the "not defined by any module" message two ways — a missing library and a misspelled registration. Are the messages distinguishable?
- Run
nmon your DPI library and find the symbol you expect. What does its absence mean, and what does its presence rule out? - Pass fewer arguments than the glue reads. Which file does the message name, and which file is at fault?
- Add an argument count check to a C function. Produce the same failure and compare the two messages.
- Use a function where a task is expected. Record both the compiler's and the simulator's exit codes.
- Make a glue function compute a result and not write it back. What does the testbench receive, and what would have caught it?
- For each of the five disagreements, say when it is caught and what you would have lost by then.
- 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
nmfirst, 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.
