Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
vpiHandle t = lookup("tb.sink");
if (t) write(t, v);

Measured, two callbacks from two different agents:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  [SV] tb.sink=22  a1.sink=0  a2.sink=0

Both 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  what a log would tell you: nothing -- there is no log

Not "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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  [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 1

Everything 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  [C] REJECTED callback: bad handle 99 at t=30
  [SV] a1.sink is still 101

Named, timestamped, refused, and the target left alone — Chapter 20.10 §8's discipline, now on the callback path.

Two callback designs measured against the same testbench with two agent instances. The first resolves a name on every call and writes to whatever it finds from the top; the second resolves once per instance when the instance opens, keeps the handle, checks it on every callback, and traces each one. Where the value landed: the first design put both callbacks into the top-level variable, leaving both agents' own variables at zero, which is the wrong object entirely; the second put 101 into the first agent and 200 into the second, which is correct. What the log says: the first produces no log at all, while the second names the resolved target, the handle, the simulation time and the value for every callback. How many callbacks each instance received: unknown in the first design, and measured as 2 and 1 in the second. A callback carrying a handle nobody issued is accepted silently by the first design and rejected by name and timestamp by the second, with the target left unchanged. The cost of the second is one extra call per instance at set-up.The questiontwo agent instances, samecallbacksResolve by name on everycallthe version written firstResolve once, check, andtraceone extra call at set-upWhere the value landedwith two instances runningthe top-level variable,both timesmeasured: both agents left at 0each agent's own variablemeasured: 101 and 200What resolution happenedand how oftena search on every singlecalland the result is never reportedone search per instance, atopenmeasured: 1 hop outward eachWhat the log sayswhen something looks wrongnothing — there is no logthe callback is invisibletarget, handle, time andvalue, per callbackmeasured: callback 2, t=25, handle0, tb.a1.sink, 101How many callbacks eachinstance gotasked afterwardsunknownnothing counts them2 and 1measured, and queryable from thetestbenchA callback with a handlenobody issuedthe misuse caseaccepted silentlythere is no handle to be wrongrejected, by name andtimestampmeasured: bad handle 99 at t=30,target unchangedWhat it coststhe price of the right columnnothing extraand nothing recoverable eitherone call per instance, oncethe per-callback path is a use,not a searchA callback is debuggable exactly to the extent that it says who it was for and where it wentmeasured — and the design that says nothing also silently wrote to the wrong object, which is the same decisionshowing up twice12
Figure 1 — the same callbacks through two designs, with two agent instances in the testbench. The first two rows are the ones to read together, because they are the chapter's argument: the design that landed in the wrong object is the design that cannot tell you where it landed, and both follow from never having decided whose callback it was. The lower rows are what the identity buys once it exists — a per-instance count the testbench can query, a trace naming the handle and the resolved target and the time, and a refusal when a handle arrives that was never issued. The last row is the price, which is one extra call per instance, once, at set-up.

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

  1. Predictable and debuggable are one decision, not two goals.
  2. Measured, design A: both callbacks landed in the top-level variable; a1.sink and a2.sink both stayed 0.
  3. And it has nothing to log — no identity, no resolved target, no handle.
  4. Measured, design B: a1.sink=101, a2.sink=200 — the right object every time, each resolved in one hop.
  5. A trace with four facts per callback: which, when, through which handle, to which resolved target, with what value.
  6. Per-instance counts, queryable — measured, 2 and 1.
  7. A handle nobody issued is refused — measured, bad handle 99 at t=30, target unchanged.
  8. 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).
  9. Resolving at open moves failures to set-up time instead of mid-run.
  10. The log is a readout of the identity, not an add-on — design A cannot be logged into debuggability.
  11. 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

  1. Build a callback that resolves a full path on every call, and instantiate the component twice. Where do the callbacks land?
  2. Add a print statement to that design. Write down everything the line can truthfully say. What is still unknown?
  3. Convert it to issue a handle at open. How many lines changed, and which of §4's four rules did each line serve?
  4. Print the resolved full name rather than the name that was asked for. Construct a case where the two differ.
  5. Pass a handle that was never issued. What does your implementation do, and what does the target hold afterwards?
  6. Query the per-instance callback counts at the end of a test. Make one of them zero and describe how the failure presents.
  7. §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.
  8. 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: 101 and 200, 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.