Skip to content
VLSI Mentor

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

Boundary Failures and How to Debug Them

Every failure in this module falls into one of two populations, and only one of them is worth a chapter. The loud ones announce themselves — an undefined symbol, a missing export, a simulator that will not load your object — and they are fixed within minutes of appearing because the error message names the problem. The quiet ones link cleanly, run to completion, exit zero, and return a number that is in range and plausible, and they are the reason this module exists. This chapter collects them. A signature mismatch that the linker accepts because C linkage discarded the types. A width or signedness error that is invisible for every small positive input. An array read with the wrong element type that returns the correct answer for the data you tested with. Storage that was freed and still holds the right text. A purity claim that is false, where the answers stay right and the model's own state silently stops updating. Each is measured, each gets a symptom-to-cause path, and the chapter ends with the tool that catches the mechanical half of them before any of it runs.

Expert21 min readC++Part IIVerificationDPI

Part II · Module 28 · Chapter 28.12 · Boundary Failures and How to Debug Them

1. Two Populations

loud failuresquiet failures
when they appearthe first build or runwhenever the data changes
what you getan error naming the problema plausible number
how long they live in a codebaseminutesmonths
examplesundefined symbol, missing export, object not loadedeverything in §§2–3

The loud population is self-limiting. Chapter 28.5 §3's undefined-symbol error names the symbol it wanted, and the fix is one keyword. Chapter 28.4 §6's missing export is an undefined symbol too. These are worth recognising on sight and are not worth worrying about, because they cannot be ignored.

The quiet population is the subject. All of them share one cause: the disagreement is between two sides, so no single compiler can see it. C++ linkage would have caught some of them, and Chapter 28.5 §5 established that extern "C" discards exactly that information, by design and in exchange for the reuse that makes DPI worth having.

The worst one, because it has no symptom at all except the answer.

A C++ definition and a C declaration that disagree about a parameter type:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// the C++ side
extern "C" int scale(double factor, int value) { return (int)(factor * value); }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
/* the caller -- standing in for the simulator's generated marshalling */
extern int scale(int factor, int value);              /* int first */

Both compile. And then:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  == 28.12 a signature mismatch LINKS and returns a plausible number ==
  link exit=0
  calling scale(3, 7), expecting 21
  got 0
  run exit=0

It links with exit 0, runs, exits 0, and returns 0 instead of 21.

The mechanism on this platform: the caller placed the integer 3 in an integer register because that is what its declaration said, and the callee read a floating-point register because that is what its declaration said. The floating-point register held whatever it held, and 0.0 × 7 is 0.

Two things about that explanation matter for how you use it.

The value is ABI-dependent and the shape is not. On a different calling convention the garbage would be different — possibly a huge number, possibly something plausible. What is portable is that it links, runs, exits 0 and is wrong. So never reason from the specific wrong value; reason from the fact that there is one.

And the symbols are identical. nm shows _scale defined and _scale wanted. Chapter 28.5 §4's diagnostic — which answers the loud failure in one command — says nothing here, because from the linker's point of view nothing is wrong.

3. The Rest of the Quiet Population

Each of these was measured in its own chapter; collected here as a symptom table.

symptomcausemeasured
a plausible wrong answer, exit 0, clean linksignature mismatch§2 — got 0 for 21
correct for positive values, wrong for negativesignedness28.7 §4 — −1 became 65535
correct for small values, wrong for largewidth28.7 §4 — 0x00012345 became 0x2345
correct for your fixture, wrong for real trafficarray element type or layout28.8 §3 — four ints as eight shorts gave 10, the right answer
a checksum matches, element-wise comparison fails2D transposition28.8 §6
the value is right with its halves exchangedchunk order28.8 §4
text is right at first and garbage laterstring lifetime28.10 §2 — ASan stack-use-after-scope, exit 134
answers are right, the model's state never updatesfalse pure claim28.11 §3 — 1000 calls became 0
the wrong checker instance is creditedwrong scope on a callback28.4 §4 — 2 hits and 3 hits
a definite value where an X was expectedfour-state read as two-state28.2 §3 — bval discarded

Read the left column as a diagnostic index. The pattern of when the answer is wrong identifies the cause faster than any amount of stepping through the model, because each cause has a characteristic data dependence:

Wrong only for negatives → signedness. Wrong only for large values → width. Wrong with real traffic but right with your fixture → array layout. Right then wrong over time → lifetime. Right answers but stale state → a false purity claim.

That index is the single most useful thing in this chapter, because the alternative is debugging a model that is not broken.

4. The Cross-Checker

Half of that table is mechanically detectable from the two declarations, and nothing in the toolchain does it — so the module builds one. It parses the SystemVerilog imports, parses the C++ definitions, applies Chapter 28.7 §2's mapping, and reports disagreements.

On a correct pair:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  == 28.12 the static cross-checker on a correct pair ==
  imports found: 3   C-visible definitions: 3
    crc32: OK
    rm_create: OK
    rm_step: OK
  imports with problems: 0
  exit=0

And — the part that makes it worth trusting — proven able to fail, on six deliberately broken pairs covering a different defect class each:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  == 28.12 and on six deliberately broken ones ==
  imports found: 6   C-visible definitions: 4
    missing_fn: FAIL
        - no definition found
    cpp_linkage: FAIL
        - defined with C++ linkage -- needs extern "C"
    wrong_arity: FAIL
        - arity: SV has 2, C has 1
    wrong_width: FAIL
        - arg 0: SV shortint needs C short, found int
    wrong_sign: FAIL
        - arg 0: SV byte unsigned needs C unsigned char, found char
    wrong_ret: FAIL
        - return: SV real needs C double, found float
  imports with problems: 6
  exit=1

Six of six, each named with the specific disagreement, exit 1.

Three points about building one of these.

Proving it can fail is not optional. A checker that silently finds nothing is indistinguishable from a clean codebase, which is the false-green shape this track has recorded repeatedly. The six broken pairs are kept in the repository next to the tool, and they are the tool's own test suite.

It catches exactly the mechanical half. Missing definitions, linkage, arity, widths, signedness, return types. It cannot check a pure claim — Chapter 28.11 §5's callout explains why that is impossible rather than merely unimplemented — and it cannot check a string's lifetime or an array's layout convention.

And it is cheap. A few hundred lines of a scripting language, run in the build. Against the alternative — a width error discovered in a month's regression triage — that is a trivial investment, which is the argument Chapter 28.2 §5's callout made for putting the evidence somewhere rather than relying on care.

5. The Three Defences, in Order

Given §3's table, the practical question is where to spend effort. Three defences, cheapest first, and they cover different parts of the table.

One source instead of two. Generate both declarations from a single description. There is then nothing to disagree, which removes every row of §3 that is a declaration mismatch — the first three and the fourth. Chapter 27.7 §6 made the same argument for register maps.

A build-time cross-check. §4's tool, for codebases where the declarations are hand-written. Same coverage, after the fact rather than by construction.

And a known-value call per argument, asserted at the start of every run. This is the one that catches what static checking cannot, and Chapter 28.7 §5's callout gives the recipe: for each scalar argument, the most negative value, the most positive, one needing the full width, and for arrays a fixture with every element distinct, full-width, signed and non-square. Four values per argument closes the width, signedness, layout and marshalling rows at once, because each is precisely a case where a specific input makes the failure visible.

What none of the three catches: the lifetime and purity rows. Those need ASan on the C++ side — Chapter 28.10 §2's stack-use-after-scope with exit 134 — and, for purity, a careful one-time decision, since Chapter 28.11 §3 measured that the return values stay correct while only the side effect disappears.

6. Working One From Symptom to Cause

A procedure, given a wrong answer from a flow that builds and runs cleanly.

Confirm the model is right on its own. Run the model's unit tests with no simulator — Chapter 27.11 §5's set, including the known external vector. If they pass, the model is not the problem and you have halved the search space in seconds. This is the step to do first, every time, because the alternative is debugging working code.

Characterise the data dependence. Does it fail for negatives? Large values? Real traffic but not the fixture? Over time? §3's index turns that answer into a cause.

Diff the two declarations token by token. Chapter 28.3 §7's six checks — name, arity, each type against the mapping, each direction against const, return type, qualifier. Or run §4's checker, which does the first four.

Call across the boundary with a known value. One call, one hand-computed answer. If that is wrong, the seam is wrong; if it is right, the seam is right for that value, and the next step is a boundary value rather than a typical one.

Then ASan for the lifetime rows, with detect_stack_use_after_return=1 since it is off by default.

And check the qualifier last, because a false pure is the one failure whose symptom is correct answers plus stale state — so it looks like nothing at all until you inspect the model's own counters.

7. What You Cannot Debug From Inside

Three things worth knowing before you spend an afternoon.

A crash takes the simulator with it. Chapter 28.2 §5: no waveform, no flushed log, no DPI error. So the model's defensive checks — Chapter 28.6 §5's handle validation on every entry point — are not politeness, they are the difference between a diagnosable failure and a lost run.

A memory error corrupts the simulator's heap, so the symptom may appear in the simulator rather than in your model. The instrument is ASan on the C++ side, run against the model's own unit tests where it owns all the allocations.

And the simulator's marshalling is not yours to inspect. You see what arrives and what you return. Which is why the known-value call is so valuable: it is the only way to observe the crossing itself.

8. The Table

the questionthe answermeasured
the two populationsloud (self-limiting) and quiet (the subject)§1
why are the quiet ones invisible to compilersthe disagreement is between two sides§1, §5 callout
what does a signature mismatch dolinks exit 0, runs exit 0, returns 0 for 21§2
is the wrong value portableno — only the shape is§2
does nm helpno — both sides say _scale§2
wrong only for negativessignedness§3
wrong only for large valueswidth§3
wrong only with real trafficarray layout§3
right then wrong over timelifetime§3
right answers, stale statefalse pure§3
did the checker work6 of 6 defect classes, exit 1§4
what can it not checkpure, lifetimes, layout conventions§4
the three defencesone source, a cross-check, known-value calls§5
what is the test fixture, reallythe instrument — small round numbers break it§5 callout
what to do first when debuggingrun the model's own tests§6

9. What to Actually Do

  • Run the model's unit tests first, with no simulator. It halves the search space in seconds.
  • Use §3's index: characterise when the answer is wrong and read off the cause.
  • Never reason from the specific wrong value of a signature mismatch — only from the fact that it links and runs.
  • Generate both declarations from one description where you can.
  • Run a cross-checker in the build where you cannot, and keep its broken-pair tests beside it so it is proven able to fail.
  • Assert a known value through the boundary at the start of every run, and use boundary values rather than typical ones.
  • Use a fixture with every element distinct, full-width, signed and non-square for arrays.
  • Run the model under ASan with detect_stack_use_after_return=1.
  • Validate the handle on every entry point, because a crash loses the whole run.
  • Check the qualifier last and most carefully — correct answers with stale state is its only symptom.
  • Expect no compiler to help, however many warnings you enable. -Weverything had nothing to say about a width mismatch.

10. Mistakes Worth Making Once

Debugging the model when the seam is wrong. Its own tests would have cleared it in seconds.

Reasoning from the garbage value of a signature mismatch. It is ABI-dependent; only "it links and is wrong" is portable.

Running nm on a signature mismatch. Both sides agree on the symbol.

Enabling more warnings. -Weverything does not see a disagreement with another language.

Testing with {1, 2, 3, 4}. The wrong element type returned the correct answer.

Testing with small positive integers. They agree under either signedness and either width.

Writing a cross-checker and never proving it can fail. A silent checker looks exactly like a clean codebase.

Trusting a clean ASan run without detect_stack_use_after_return=1. It is off by default.

Looking for a false pure claim in the answers. They are correct; the state is stale.

Omitting a handle check because the testbench "obviously" passes a valid one. The failure is a lost run, not an exception.

11. Interview Relevance

The frozen curriculum marks this Critical, and it is often asked as a scenario: your DPI call returns the wrong number and everything builds cleanly — what do you do?

Start by splitting the problem. Run the model's own unit tests with no simulator in the loop, including a known external vector. If they pass, the model is fine and the fault is in the seam — which halves the search space before any debugging starts.

Then characterise the data dependence, because it names the cause. Wrong only for negative values is a signedness mismatch; wrong only for large values is a width mismatch; wrong with real traffic but right with your fixture is an array layout or element-type error; right at first and wrong later is a string lifetime bug; correct answers with the model's own state never updating is a false pure claim.

Then the measurement worth quoting, because it explains why none of this was caught: a C++ definition taking (double, int) and a C declaration taking (int, int) link with exit 0, run, exit 0, and return 0 instead of 21 — because C linkage discarded the type information that C++ mangling would have used to reject it. And nm does not help, since both sides agree on the symbol name.

Then what you do about it, in order: generate both declarations from one description; failing that, run a build-time cross-checker — and say that yours is proven able to fail, catching six defect classes including wrong width and wrong signedness; and assert a known value through the boundary at the start of every run, using boundary values rather than typical ones.

The observation that marks a senior answer is that every quiet failure here is a disagreement between two independently compiled sides, where neither side read alone is wrong — so no compiler can possibly catch them, and only a tool that reads both sides or data chosen to expose the divergence can. Which makes the test fixture an instrument rather than a convenience, and a fixture of small round numbers a broken one.

12. What You Should Be Able to Say Now

  • That boundary failures split into loud ones, which are self-limiting because the error names them, and quiet ones, which link, run, exit 0 and return a plausible number.
  • That a signature mismatch links with exit 0 and returned 0 instead of 21, that the specific value is ABI-dependent while the shape is not, and that nm cannot see it.
  • The diagnostic index: negatives → signedness; large values → width; real traffic → array layout; over time → lifetime; right answers with stale state → false pure.
  • That the wrong array element type returned 10, the correct answer, for the obvious fixture.
  • That a cross-checker caught 6 of 6 defect classes — missing definition, C++ linkage, arity, width, signedness, return type — and exits 1, and that proving it can fail is part of building it.
  • That it cannot check pure, lifetimes or layout conventions.
  • The three defences: one generated source, a build-time cross-check, and known-value calls at boundary values.
  • That lifetimes need ASan with detect_stack_use_after_return=1, which is off by default.
  • That every quiet failure is a disagreement between two sides where neither side alone is wrong, which is why no compiler catches them and why -Weverything had nothing to say.
  • That the test fixture is the instrument, and small round numbers break it.
  • That the first debugging step is always to run the model's own tests, and that the last thing to check is the qualifier.

13. Exercises

Exercise 1 — the mismatch that links. Define extern "C" int scale(double, int) and declare int scale(int, int) in a C caller. Confirm it links, runs, exits 0, and predict the value before running it.

Exercise 2 — the useless diagnostic. Run nm on both objects and confirm the symbols agree.

Exercise 3 — the index. For each row of §3, construct an input that exposes it and one that hides it.

Exercise 4 — the checker. Write a cross-checker for your own imports, then write one deliberately broken pair per defect class and confirm each is caught.

Exercise 5 — proving it fails. Break your checker deliberately — remove one rule — and confirm its own test suite notices.

Exercise 6 — the fixture. Replace a {1,2,3,4} array fixture with a distinct, full-width, signed, non-square one and re-run every array test.

Exercise 7 — the false purity. Add a counter to a function declared pure, confirm the answers stay right, and write the assertion that catches it.

Exercise 8 — the lost run. Remove a handle check, pass a null, and record what you get. Then put it back.

14. Summary

  • Boundary failures split into two populations. The loud ones — undefined symbol, missing export, object not loaded — name themselves and live for minutes. The quiet ones link, run, exit 0 and return a plausible number.
  • Measured: a signature mismatch links with exit 0 and returns 0 where 21 was expected, because C linkage discarded the types C++ mangling would have used to reject it. The specific value is ABI-dependent; only "it links and is wrong" is portable, and nm cannot see it because both sides agree on the symbol.
  • The data dependence names the cause: negatives → signedness, large values → width, real traffic → array layout, over time → lifetime, correct answers with stale state → a false pure claim.
  • The static cross-checker caught 6 of 6 defect classes — missing definition, C++ linkage, wrong arity, wrong width, wrong signedness, wrong return type — with exit 1, and its six broken pairs are kept as its own test suite because a checker that cannot be shown to fail is indistinguishable from a clean codebase.
  • It cannot check a pure claim, a string's lifetime, or an array layout convention — the first being impossible rather than unimplemented.
  • Three defences, in order: generate both declarations from one description; run a build-time cross-check; and assert known values through the boundary at the type's boundaries rather than at typical values.
  • Lifetimes need ASan with detect_stack_use_after_return=1, which is off by default; purity needs a careful one-time decision, because its answers stay correct.
  • Every quiet failure is a disagreement between two independently compiled sides in which neither side, read alone, is wrong — which is mechanically why no compiler catches them, and why -Weverything had nothing to say about a width mismatch.
  • So the test fixture is the instrument, not a convenience: small round positive numbers agree under either signedness, either width and either element type, and a fixture made of them is a broken instrument.
  • And the first step in any boundary debug is to run the model's own tests with no simulator, because the alternative is debugging code that works.

Chapter 28.13 closes the module and the track by putting the whole thing together: a stateful C++ model behind a narrow boundary, a handle held across calls, and a testbench that compares against it and reports honestly.

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.