Skip to content
VLSI Mentor

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

Integrating a C++ Reference Model with SystemVerilog

This closes the module and the track, and it does it by building one integration completely rather than by summarising. The pieces are all in place: a model that is ordinary C++ with a class and an STL container, a boundary of five extern "C" entry points that validate and delegate and never let an exception escape, an opaque handle that carries the model's identity across the crossing so that state survives between calls, and a report that says how much work was checked beside its verdict. What this chapter adds is the assembly and the proof. The model is validated first against an authority outside the project — the published CRC-8/SMBUS check value — and only then allowed to judge anything, which is the ordering Module 27 argued for. Then the same model and the same boundary are driven twice: once from a pure C program, which is what a simulator's generated marshalling amounts to, and once live inside the simulator, where the handle is held across nine separate crossings from Verilog and the model reports its own accumulated state from inside the simulation.

Expert22 min readC++Part IIVerificationDPI

Part II · Module 28 · Chapter 28.13 · Integrating a C++ Reference Model with SystemVerilog

1. The Architecture

Three layers and one handle, which is the shape every chapter in this module has been building toward.

An integration drawn as three columns. On the left, level with the middle row, a block labelled SV testbench with the caption drives and compares. In the middle column, stacked vertically, three blocks for the three entry points: rm underscore create, captioned returns a handle; rm underscore step, captioned per byte; and rm underscore destroy, captioned at the end. On the right, level with the middle row, a block labelled C plus plus model with the caption class plus std map. Arrows run from the testbench to each of the three entry points, and from each entry point on to the model, so every crossing goes through one of the three. The three entry points and the model are highlighted because they are the C plus plus side of the boundary.SVtestbenchdrives,comparesrm_createreturns ahandlerm_stepper byterm_destroyat the endC++ modelclass +std::map12
Figure 1 — three layers and one handle. Every crossing goes through one of three C-linkage entry points, and the C++ model on the right knows nothing about any of it. The handle returned by rm_create is what connects a later call to the right model instance, which is how state survives between crossings.

Every crossing goes through one of three entry points, and the model on the right is ordinary C++ that knows nothing about any of this — Chapter 28.6 §2's separation. The handle is what connects a call to the right model instance, and §2 is why that matters more than it looks.

2. The Boundary, With Its Defences

Five entry points, which Chapter 28.6 §3 measured as exactly five C-visible symbols:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#include "rmodel.hpp"

extern "C" {

void* rm_create(unsigned char poly)
{
    try { return new rm::Model{(std::uint8_t)poly}; }
    catch (...) { return nullptr; }              // no exception may cross
}

int rm_step(void* h, unsigned char data)
{
    if (!h) return -1;
    try { return (int)static_cast<rm::Model*>(h)->step((std::uint8_t)data); }
    catch (...) { return -1; }
}

int rm_count(void* h)    { return h ? (int)static_cast<rm::Model*>(h)->count()    : -1; }
int rm_distinct(void* h) { return h ? (int)static_cast<rm::Model*>(h)->distinct() : -1; }

void rm_destroy(void* h) { delete static_cast<rm::Model*>(h); }

}

Four properties, each of which earlier chapters argued for and this one depends on.

The handle carries the model's identity. rm_create returns it, every other call takes it first. That is what makes state survive between calls — and it is also Chapter 28.4 §5's answer to the scope problem, because the identity travels in the call rather than beside it, so there is no ambient state to get wrong.

Every entry point validates the handle. if (!h) return -1; — and the measured behaviour:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  a call on a null handle returns -1

That is not politeness. Chapter 28.2 §5 established that a dereference here terminates the simulation with no waveform and no flushed log, so the check converts a lost regression run into a diagnosable return value.

Every entry point catches everything. An exception escaping into the simulator's frames is undefined behaviour, and catch (...) is the one place where the unselective catch is correct.

And the entry points do no work. Validate, cast, delegate. Chapter 28.6 §5's argument: the boundary is the only layer that cannot be unit-tested, so it should contain nothing worth testing.

The SystemVerilog side, as reference syntax — the declarations a DPI flow would use, which Chapter 28.1 §5 measured as not compilable on this machine:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Reference syntax -- not executed here.
package rm_pkg;
  import "DPI-C" function chandle rm_create  (input byte unsigned poly);
  import "DPI-C" function int     rm_step    (input chandle h, input byte unsigned data);
  import "DPI-C" function int     rm_count   (input chandle h);
  import "DPI-C" function void    rm_destroy (input chandle h);
endpackage

chandle is the SystemVerilog type for an opaque C pointer, and it maps to void*. It can be stored in a class member, passed around, and compared against null — which is how a testbench holds one model per agent.

3. The Call Sequence

What a run looks like over time, which is the part a block diagram cannot show:

A sequence diagram with three actors: the SystemVerilog testbench, the extern C boundary, and the C plus plus model. The testbench calls rm underscore create with polynomial 0x07 on the boundary; the boundary constructs a new Model; the model returns itself as a pointer; and the boundary returns an opaque chandle to the testbench. The testbench then calls rm underscore step with a data byte, nine times in total; each call is forwarded to the model's step method, which returns the running C R C. After nine bytes the accumulated value is 0xF4, matching the published check value. The testbench then calls rm underscore count, which the boundary forwards and which reports nine bytes. Finally the testbench calls rm underscore destroy, and the boundary deletes the model.One model, created, stepped nine times, and destroyedSV testbenchextern "C"C++ modelrm_create(0x07)new Modelpointerchandlerm_step(data) x9step(data)running crc0xF4 after 9rm_count(h)9rm_destroy(h)delete
Figure 2 — the same integration over time, which a block diagram cannot show. The handle is created once, returned to the testbench, and handed back on every subsequent call; the boundary itself holds no state, which is why two models could coexist with no change to the entry points.

Note what the handle does in that sequence. It is created once, returned to the testbench, and handed back on every subsequent call. The boundary itself holds no state at all — which means two models can coexist, each with its own handle, and the entry points need no change.

4. Validate First, Then Judge

Chapter 27.11 §1's ordering applies unchanged, and the first step is the one that cannot be skipped: the model is validated against an authority outside the project before it is allowed to judge anything.

Here that authority is the published CRC-8/SMBUS check value — polynomial 0x07, initial value 0x00, no reflection, no final XOR — which for the ASCII string "123456789" is 0xF4. Driven from a pure C program, which is what a simulator's generated marshalling amounts to:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  handle created: yes
  after "123456789": crc=0xF4 count=9 distinct=9
  published CRC-8/SMBUS check value is 0xF4 -> MATCH
  a call on a null handle returns -1

Four things in four lines.

The handle was created and used. Nine calls to rm_step through a void*.

The model reproduces 0xF4. An answer computed by someone else, from the specification, with no knowledge of this project — Chapter 27.11 §2's step, and the only evidence in the whole flow that does not depend on something else inside it being right.

The STL state survived the crossings. count=9 and distinct=9 are the model's own counter and the size of its std::map after nine separate calls from C.

And the null handle returned −1, so §2's defence is live rather than theoretical.

The exit status is 0, and the validation program is a gate: if it fails, nothing else runs, because a mismatch downstream would be uninterpretable.

5. Live Inside the Simulator

The second proof, and the one that required a decision. DPI cannot run here — Chapter 28.1 §5's exhaustive probe — so the integration is demonstrated through VPI, which Chapter 27.1 §4 measured as the working co-simulation route on this machine.

The model and the boundary are byte-for-byte the ones a DPI flow would use. The only difference is the glue: about forty lines that fetch arguments and return values, which DPI would have generated instead. Running the same nine bytes from a Verilog testbench:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  model handle created: yes
  crc after "123456789" = 0xf4
  published check value  = 0xf4
  bytes fed through the handle: 9
  handle destroyed

The handle was held across nine separate crossings from Verilog, the accumulated CRC is 0xf4, and the model reported its own call count from inside the simulation before being destroyed.

What that does and does not establish, stated plainly:

It establishes that the model, the handle lifetime, the state persistence across crossings, the validation against an external authority and the defensive boundary all work inside a running simulator.

It does not establish that a DPI declaration's own marshalling is correct, because no DPI declaration was compiled. That is the one gap this module cannot close, and Chapter 28.1 §5's callout said so at the outset rather than at the end.

6. Comparing and Reporting

With the model trusted, the comparison is Chapter 27.6's. Two points specific to a DPI integration.

Compare after every item, not at the end. The model is a predictor — its state advances with each call — so a divergence at byte 7 should report at byte 7. Chapter 27.7 §4's callout measured why: state divergence is cumulative, and a comparison made only at the end reports a wrong final value with no indication of where it started going wrong.

And print the work count beside the verdict. The live transcript's bytes fed through the handle: 9 is that line. Chapter 23.8 §2's measured defect is a gate that reports 0 mismatches from a run that checked nothing, and the count is the one-line defence.

The report shape, from Chapter 27.6 §4:

linewhy
items comparedwithout it, an empty run reads as a pass
mismatchesone, half, or all classifies the fault
a per-bit XOR histogramnames a stuck bit against diffuse arithmetic
the first few in full, with the inputthe concrete case to debug from
the verdictlast, after the evidence

7. Lifetime, and Who Owns What

The handle introduces an ownership question that C does not help with, and it has three answers depending on the shape of the testbench.

patterncreate / destroywhen
one model per simulationin an initial block and at $finishthe simple default
one per agentin the agent's constructor and destructorseveral in flight
one per testat the start and end of each testmodels must not share state

Two hazards.

C has no destructors, so rm_destroy must actually be called. In a one-model-per-run flow the practical answer is that the model lives for the simulation; where models are created per test, an unpaired create is a leak that grows across a regression. Chapter 25.7's leaks route is how you would confirm it, run against the model's own unit tests where the C++ side owns every allocation.

And a void* is a void*. Passing the wrong handle to the wrong family of entry points compiles and corrupts. Where it matters, a magic number inside the object checked on entry turns that into a detected error:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
struct Header { std::uint32_t magic; };           // 0x524D4F44 == "RMOD"
// in rm_step: if (!h || static_cast<Header*>(h)->magic != 0x524D4F44) return -1;

That costs four bytes and one comparison, and it converts a corrupted simulation into a return value — the same trade as §2's null check.

8. The Checklist

The integration, as a sequence of things to have done:

The model is ordinary C++ with no interface headers in it, unit-tested with no simulator. It is validated against an authority outside the project before anything else runs, as an exit-coded gate. Determinism is tested — run it twice on the same input and diff, Chapter 27.3 §5. The boundary is one small file of extern "C" entry points that validate, catch and delegate. The handle is created once, passed on every call, and destroyed exactly once. The declarations are cross-checked against the definitions, by a tool proven able to fail. A known value is asserted through the boundary at the start of every run, at the type's boundaries rather than typical values. The comparison happens after every item, with the work count printed beside the verdict. A deliberately mutated DUT is run to prove the flow can fail at all — Chapter 27.4 §8. And the model's own tests run under ASan, with detect_stack_use_after_return=1.

Ten items, of which only two involve the simulator.

9. The Table

the questionthe answermeasured
how many entry pointsfive C-visible symbols§2, 28.6 §3
what carries the model's identityan opaque handle, passed in every call§2
why a handle rather than ambient scopethe identity travels in the call§2
what does a null handle give−1, not a terminated simulation§2, §4
what is the SystemVerilog type for itchandle → void*§2
what validates the modelthe published CRC-8/SMBUS 0xF4 for "123456789"§4
did STL state survive the crossingsyes — count=9, distinct=9§4
was it proven liveyes, via VPI — 0xf4, nine crossings, handle destroyed§5
what remains unproven herea DPI declaration's own marshalling§5
when should the comparison happenafter every item, because the model is a predictor§6
what goes beside the verdictthe work count§6
what catches a wrong handlea magic number checked on entry§7
what did the environment costthe easy half — the declaration syntax§7 callout
what did it improveevery claim is now measured or marked as quoted§7 callout

10. What to Actually Do

  • Write the model as ordinary C++, with no interface headers, and unit-test it with no simulator.
  • Validate it against an outside authority first, as an exit-coded gate that blocks the rest of the flow.
  • Keep the boundary to one small file of entry points that validate the handle, catch everything, and delegate.
  • Carry the model's identity in a handle passed on every call, rather than in ambient state.
  • Return an error on a null handle on every entry point, because the alternative is a lost run.
  • Add a magic number to the object where handles of several types are in play.
  • Create once and destroy exactly once, and check for leaks if models are per test.
  • Cross-check the declarations with a tool proven able to fail, and assert a known value through the boundary every run.
  • Compare after every item and print the work count beside the verdict.
  • Run a mutated DUT to prove the flow can fail, and the model's tests under ASan.
  • And probe your own toolchain before planning the integration — then say which of your claims are measured and which are quoted.

11. Mistakes Worth Making Once

Skipping the external validation because the co-simulation passes. Agreement between two artefacts is not conformance.

Holding the model's identity in a global instead of a handle. Chapter 28.4 §4's two counters, 2 and 3, with no diagnostic.

Omitting the null check. A dead simulator with no waveform.

Letting an exception escape the boundary. Undefined behaviour in the simulator's process.

Putting logic in the entry points. It now lives in the layer that cannot be unit-tested.

Passing the wrong void* to the wrong entry point. It compiles and corrupts.

An unpaired create in a per-test flow. A leak that grows across the regression.

Comparing only at the end of the test. A wrong final value with no indication of where it started.

Reporting a verdict with no work count. A run that checked nothing reads as a pass.

Never running a mutated DUT. The green dashboard means nothing.

Planning the integration before probing the simulator. Here, DPI is a syntax error at every generation.

12. Interview Relevance

The frozen curriculum marks this Critical and Expert, and it is the closing question of a verification interview: walk me through integrating a C++ reference model with a SystemVerilog testbench.

Lead with the architecture and the handle. The model is ordinary C++ — a class with STL state, no simulator headers — and the boundary is a small file of extern "C" entry points: create, step, query, destroy. The create function returns an opaque pointer that SystemVerilog holds as a chandle and passes back on every call, which is what makes state survive between calls and what lets one testbench hold several model instances.

Then the boundary's three defences, since they are what distinguish a production integration: every entry point validates the handle and returns an error rather than dereferencing null, because the C++ runs in the simulator's process and a crash loses the run with no waveform; every entry point catches everything, because an escaping exception is undefined behaviour; and the entry points contain no logic, because the boundary is the one layer that cannot be unit-tested.

Then the ordering, which is the part most answers omit. The model is validated against an authority outside the project before it is allowed to judge anything — the measured example being the published CRC-8/SMBUS check value 0xF4 for "123456789", as an exit-coded gate — and only then is it wired into the simulation, where the comparison happens after every item with the work count printed beside the verdict.

And the detail that shows real integration experience is what you do first: compile a two-line import on your own simulator and read the exit status directly, because support varies. On the toolchain used for this module DPI is a syntax error at every language generation and svdpi.h is absent, so the same model and the same extern "C" boundary were driven through VPI instead — nine crossings, the handle held throughout, 0xf4 at the end. Saying plainly which parts of your knowledge are measured and which are from the standard is, in this subject, the strongest thing you can do.

13. What You Should Be Able to Say Now

  • The architecture: ordinary C++ model, five extern "C" entry points, an opaque handle passed on every call.
  • That the handle makes state survive between calls and lets several instances coexist, and that carrying identity in the call beats ambient scope.
  • That SystemVerilog holds it as a chandle, which maps to void*.
  • The boundary's defences: validate the handle (measured: a null returns −1), catch (...) everything, and no logic in the entry points.
  • That the model is validated against an outside authority first — the published CRC-8/SMBUS 0xF4 — as an exit-coded gate, because every later step's evidence is conditional on it.
  • That STL state survived nine crossings: count=9, distinct=9.
  • That the integration was proven live via VPI — 0xf4, nine crossings, handle created and destroyed — with the identical model and boundary, and that a DPI declaration's own marshalling remains unproven here.
  • That the comparison happens after every item, because a predictor's divergence is cumulative, with the work count beside the verdict.
  • That a magic number in the object catches a wrong handle, and that an unpaired create leaks in a per-test flow.
  • That the full checklist has ten items of which two involve the simulator.
  • And that the right first action in any integration is to probe the toolchain, then state clearly which claims are measured and which are quoted.

14. Exercises

Exercise 1 — the boundary. Write the five entry points over a model with STL state. Confirm with nm that exactly five symbols are C-visible.

Exercise 2 — validate first. Make the model reproduce a published check value in a program with no simulator, and make it exit non-zero on a mismatch.

Exercise 3 — the handle. Drive the model from a pure C program through a void*, nine calls, and confirm the state accumulated.

Exercise 4 — the defences. Call every entry point with a null handle. Then remove one check and run it under ASan.

Exercise 5 — two instances. Create two models with different polynomials, interleave calls to both, and confirm neither affects the other.

Exercise 6 — the magic number. Add a header word and pass a handle from a different family. Confirm the error is detected.

Exercise 7 — live. Get the same model running inside whatever simulator you have — through DPI if it supports it, VPI if not — and hold the handle across several calls.

Exercise 8 — prove it can fail. Mutate the DUT by one bit and confirm the flow reports it, with the work count printed.

15. Summary

  • The architecture is three layers and one handle: an ordinary C++ model, five extern "C" entry points, and an opaque pointer that SystemVerilog holds as a chandle and passes back on every call — which is what makes state survive the crossings and lets several instances coexist.
  • The boundary validates, catches and delegates. A null handle returns −1 rather than terminating the simulation; catch (...) stops an exception becoming undefined behaviour in the simulator's process; and no logic lives in the only layer that cannot be unit-tested.
  • The model is validated against an authority outside the project first. Measured: it reproduces the published CRC-8/SMBUS check value 0xF4 for "123456789", as an exit-coded gate — the only evidence in the flow that does not depend on something else inside it.
  • Driven from pure C, which is what a simulator's marshalling amounts to: handle created, nine steps, count=9 and distinct=9 showing STL state surviving the crossings, and a null handle returning −1.
  • And proven live inside the simulator through VPI — the working route on this machine — with the identical model and boundary: the handle held across nine separate crossings from Verilog, 0xf4 accumulated, the count reported from inside the simulation, and the handle destroyed.
  • What remains unproven here is a DPI declaration's own marshalling, which was stated at the start of the module rather than discovered at the end.
  • Compare after every item, because a predictor's divergence is cumulative, and print the work count beside the verdict so an empty run cannot read as a pass.
  • Create once, destroy exactly once, and add a magic number to the object wherever handles of several types are in play.
  • The checklist is ten items and only two involve the simulator — which is the module's thesis in one line: almost everything that decides whether a DPI integration works is ordinary C++, settled before the simulation starts.

That closes Module 28, Part II, and the C++ track. The through-line of all two hundred and fifty-five chapters, if it has one, is the one this final module kept measuring: a program's correctness is decided by properties you can establish before it runs — a type that makes the illegal case unrepresentable, an invariant checked where the object is created, a model whose authority is a document rather than another implementation, a boundary whose assumptions are written down — and the failures that cost the most are always the ones where two individually correct pieces disagreed and nothing in the toolchain could see it.

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.