C Part II · Module 23 · Calling SystemVerilog from C
Callback-Style Integration Patterns
Two callback designs put through the same testbench with two agent instances. The first resolves a name on every call and has nothing to say about what it did; measured, both callbacks landed in the top-level variable and both agents were left untouched. The second issues a handle, resolves once, checks every call and traces each one; measured, the right instance every time, a per-instance count, and a callback with a handle nobody issued rejected by name and timestamp. The two properties the chapter is named for turn out to be one decision seen twice.
Expert24 min readCPart IIVerificationDPI-C
Part II · Module 23 · Chapter 23.6 · Callback-Style Integration Patterns
1. Two Properties, One Decision
The purpose asks for callbacks that stay predictable and debuggable, and it is natural to read those as separate goals — one about correctness, one about tooling, probably addressed at different times.
The measurement says otherwise, and that is this chapter's argument. Both properties come from the same decision, and a design that lacks one lacks the other for the same reason.
The setup: one testbench, two instances of an agent, each wanting a callback into its own variable. Two designs, the same callbacks.
2. Design A: Resolve on Every Call
The version written first, and it is not unreasonable. The C looks up the target by name whenever it needs it and writes:
vpiHandle t = lookup("tb.sink");
if (t) write(t, v);Measured, two callbacks from two different agents:
[SV] tb.sink=22 a1.sink=0 a2.sink=0Both landed in the top-level variable. a1.sink and a2.sink — the variables the two agents actually care about — were never touched.
That is Chapter 23.3's failure in its natural habitat: a full path names one object, there are two agents, and the path does not know which one is calling. The second callback also overwrote the first, so even the single value that did land is only the last one.
And the second half of the measurement is that there is nothing to say about any of it:
what a log would tell you: nothing -- there is no logNot "the log was not enabled". There is nothing a log could usefully print, because the function never established who the callback was for or where it resolved to. It has no facts to report.
3. Design B: Issue a Handle, Resolve Once, Check, Trace
Each agent opens a handle when it starts. The C resolves the target then, keeps it, and from that point every callback is a use rather than a search.
Measured:
[C] open: handle 0 -> "tb.a1.sink" (resolved in 1 hop(s))
[C] open: handle 1 -> "tb.a2.sink" (resolved in 1 hop(s))
[C] callback #1 t=15 handle 0 -> tb.a1.sink value 100
[C] callback #1 t=20 handle 1 -> tb.a2.sink value 200
[C] callback #2 t=25 handle 0 -> tb.a1.sink value 101
[SV] a1.sink=101 a2.sink=200
[SV] a1 received 2 callback(s), a2 received 1Everything the previous design could not provide:
The right object, every time — a1.sink=101, a2.sink=200, Chapter 23.3's outward walk resolving each in one hop.
A trace with all four facts — which callback, at what time, through which handle, to which resolved target, with what value. Chapter 23.4 §4's point applies: because a call is one instant, the order of these lines is control-flow order, which is exactly the information a reader wants.
A per-instance count, queryable from the testbench — 2 and 1.
And the misuse case. Measured, a callback carrying a handle nobody issued:
[C] REJECTED callback: bad handle 99 at t=30
[SV] a1.sink is still 101Named, timestamped, refused, and the target left alone — Chapter 20.10 §8's discipline, now on the callback path.
4. The Four Rules
Nothing in design B is new. That is the point — it is this track's discipline assembled, and each rule has a measurement behind it.
One — issue the identity from C. The handle comes from open, not from a number the caller picked. Chapter 22.7 §5 measured why: a caller-chosen identifier can be chosen twice, silently, and an index is just a number. Measured here: per-instance counts of 2 and 1, which exist only because each instance has an identity.
Two — resolve once, at open. Chapter 23.3 §5's rule. Measured: one hop outward per instance, at set-up, after which every callback is a use rather than a search. The second benefit matters more than the first: a resolution failure surfaces when the instance opens, not in the middle of a run.
Three — check every handle and refuse. Measured: REJECTED callback: bad handle 99 at t=30, with the target unchanged. Chapter 20.10 §7 measured the alternative — a validity check that dereferences the caller's value is itself the bug.
Four — trace who, where, when and what. The four facts in design B's log lines. Chapter 23.4 §4 established why the ordering is trustworthy: a call is one instant, so the sequence of lines is the sequence control actually took.
5. The Misconception: Add Logging Later If We Need to Debug It
"Get it working first. If it turns out to be hard to debug, we add logging then — that is a half-hour change."
Measured false as a plan, and the reason is the chapter's argument.
Design A has nothing to log. Not "no logging code" — no facts. It never decided who the callback was for, so it cannot name an instance. It resolves a name on every call and discards the result, so it cannot name a target. It issues no handle, so it cannot report a bad one. Adding a print statement to design A yields a line that says a callback happened and a value was written somewhere, which is close to what you already knew.
To make design A debuggable you have to give it an identity and a resolved target — at which point you have written design B. The logging is not an add-on to the design; it is a readout of information the design either has or does not.
And the sharper half: the information it lacks is the same information that made it wrong. Design A wrote to tb.sink instead of a1.sink because it had no per-instance identity, and it cannot tell you so because it had no per-instance identity. One missing decision, two symptoms — which is why "predictable" and "debuggable" are not two goals to be traded off.
The accurate position:
Decide the identity before the first callback is written. It costs one call per instance at set-up, it makes the callback land in the right place, and the log falls out of it. Retrofitting it is not adding logging; it is the design change you were avoiding, done later and with a bug to reproduce.
6. What You Should Be Able to Say Now
- Predictable and debuggable are one decision, not two goals.
- Measured, design A: both callbacks landed in the top-level variable;
a1.sinkanda2.sinkboth stayed 0. - And it has nothing to log — no identity, no resolved target, no handle.
- Measured, design B:
a1.sink=101,a2.sink=200— the right object every time, each resolved in one hop. - A trace with four facts per callback: which, when, through which handle, to which resolved target, with what value.
- Per-instance counts, queryable — measured, 2 and 1.
- A handle nobody issued is refused — measured,
bad handle 99 at t=30, target unchanged. - Four rules: issue the identity from C (22.7) · resolve once at open (23.3) · check and refuse (20.10) · trace who, where, when, what (23.4).
- Resolving at open moves failures to set-up time instead of mid-run.
- The log is a readout of the identity, not an add-on — design A cannot be logged into debuggability.
- Record the counts in pass/fail, not just the log: a path that stopped firing looks exactly like one with nothing to report (18.2).
7. Exercises
- Build a callback that resolves a full path on every call, and instantiate the component twice. Where do the callbacks land?
- Add a print statement to that design. Write down everything the line can truthfully say. What is still unknown?
- Convert it to issue a handle at open. How many lines changed, and which of §4's four rules did each line serve?
- Print the resolved full name rather than the name that was asked for. Construct a case where the two differ.
- Pass a handle that was never issued. What does your implementation do, and what does the target hold afterwards?
- Query the per-instance callback counts at the end of a test. Make one of them zero and describe how the failure presents.
- §5 says the missing information is the same information that made design A wrong. State that in one sentence for a design you have seen.
- Why does resolving at open rather than per call change when a failure is discovered? Which is cheaper to diagnose?
8. Summary
- Predictable and debuggable are the same decision seen twice.
- Measured, design A: both callbacks into the top-level variable, both agents left at 0, and no log is possible — it has no facts.
- Measured, design B:
101and200, right instance every time, one hop each. - Four facts per callback — which, when, which handle, which resolved target, what value — in control-flow order (23.4).
- Counts queryable: 2 and 1.
- A handle nobody issued is refused:
bad handle 99 at t=30, target unchanged. - Four rules: issue the identity from C · resolve once at open · check and refuse · trace.
- Resolving at open moves failures to set-up.
- The log is a readout of the identity, not something to add later.
- Put the counts in pass/fail — a silent path looks like an idle one.
9. Module 23 in One Page
Six chapters from one question: what does it mean for C to call back into the simulator?
- Most of the time it should not. Measured, a C function that wrote three findings into a simulator variable during one call left a watching process waking once, having seen only the last — so "the testbench can react as each thing happens" is not available. The fetch loop, which costs three extra calls and looks clumsy, was the only design that let the testbench act on every event (23.1).
- Two kinds of subroutine, one load-bearing difference: from the LRM a function may never consume simulation time and a task may. A kind mismatch is caught at run time — compiler exit 0, simulator exit 1 (23.2).
- A callback lands where you decided, or nowhere. Measured, the scope a call reports is the task, not the module, and a bare name that the SystemVerilog beside the call reads without qualification failed to resolve from it. Walking outward one hop found the right object in each of two instances, from identical C (23.3).
- Control is in exactly one place. Measured, every step of a crossing at t=5, and a second process that ran at t=1 and t=3 and never inside the call. That single fact explains 23.1's result and 23.3's scope (23.4).
- Work is unlimited and free in simulated time; waiting is impossible. Measured, 200 million iterations with a delta of 0, and a callback registered at 10 for +20 that fired at 30 from a call that returned immediately. You do not wait — you arrange to be called again (23.5).
- And an identity makes the callback both correct and visible — measured, the design that landed in the wrong object is the design that cannot say where it landed (23.6).
The thread is the one Module 22 ended on, pointed the other way. There, the test that confirms a thing was usually the test that could not distinguish it from its most likely failure. Here, the design that cannot tell you what it did is usually the design that did the wrong thing — and in every chapter the fix was the same shape: give the thing an identity, decide where it lands, check what you are handed, and say so out loud.
Both directions of the boundary are now settled — what crosses, how it crosses, who calls whom, and what each side may do while the other waits. All of that machinery exists to serve one architecture, and the next module finally builds it: stimulus drives the design to an actual result, the same stimulus drives a C model to an expected one, and something compares them and reports honestly. The hardest part is not the connection. It is that a model written in C is not golden because it is written in C — a sentence this track has been circling since Chapter 18.2.
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.
