Skip to content
VLSI Mentor

C Part II · Module 23 · Calling SystemVerilog from C

Scope Control: Setting and Getting the Context

A callback has to land somewhere, and the measurement here is sharper than the usual warning. Called from inside a task in one instance, the lookup of a bare name did not land in the wrong scope — it failed outright, for a variable the SystemVerilog at that exact point can see perfectly well. The scope a call reports is the innermost one, the task rather than the module, and a scope handle names one scope without searching the ones enclosing it. Walking outward a single level found the right object in each instance, from identical C and an identical name.

Expert24 min readCPart IIVerificationDPI-C

Part II · Module 23 · Chapter 23.3 · Scope Control: Setting and Getting the Context

1. A Callback Has to Land Somewhere

Chapter 23.2 settled what kind of subroutine is being called. This chapter is about which one — because in a testbench with two instances of the same component, the name of an exported subroutine is not unique, and neither is the name of anything it touches.

From the LRM, that is what a DPI context is: a function declared context can find out which SystemVerilog scope it was called from, and can set the scope that its own calls back into SystemVerilog will use. The two operations are get where I am and make this the current one.

The labelling rule from Chapter 22.1 §2 applies: the export "DPI-C" declaration, context functions and the scope-setting calls are from the LRM; everything measured below is a real lookup in a running simulation through VPI, where asking "what scope was I called from" and "find this name from here" are directly available.

The setup for every measurement: a top module tb with a variable counter, and two instances u1 and u2 of a module that also has a counter, each with a task that calls into C passing the bare name "counter".

2. Full Hierarchical Names Work

The simplest thing first. Measured, looking up complete paths from the top:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  [C] found "tb.flag", writing 42
  [SV] after the call, tb.flag = 42

  [C] vpi_handle_by_name("tb.nosuchthing", NULL) -> NOT FOUND
  [SV] return -1

  [C] found "tb.u1.counter", writing 100
  [C] found "tb.u2.counter", writing 200
  [SV] u1.counter = 100, u2.counter = 200

Everything behaves. A real path resolves, a bogus one returns a null handle rather than misbehaving, and the two instances are reached distinctly.

Note the one genuinely good property in that block: a failed lookup is a null handle, not a crash and not a wrong object. That is Chapter 20.10's discipline arriving from the other side — the handle is checked, never assumed — and §5 is about making sure your code actually checks it.

3. The Bare Name, From Three Places

Now the realistic case: the C does not want to know which instance it is serving. It passes "counter" and expects the context to resolve it.

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  (1) called from tb itself:
      [C] called from scope "tb"
      [C] found "tb.counter" relative to that scope, writing 11
      [SV] tb.counter=11 u1.counter=0 u2.counter=0

  (2) called from inside u1:
      [C] called from scope "tb.u1.poke_me"
      [C] vpi_handle_by_name("counter", <that scope>) -> NOT FOUND

  (3) called from inside u2:
      [C] called from scope "tb.u2.poke_me"
      [C] vpi_handle_by_name("counter", <that scope>) -> NOT FOUND

  -- and the same bare name looked up ABSOLUTELY (no scope) --
      [C] vpi_handle_by_name("counter", NULL) -> NOT FOUND

Two surprises, and the second is the chapter.

The scope reported is the task, not the module. tb.u1.poke_me — the innermost enclosing scope at the point of the call. Not tb.u1, which is what a reader of the SystemVerilog would name if asked "where is this call?"

And the lookup failed. Not landed somewhere wrong — failed, returning a null handle, for a variable that the SystemVerilog statement three characters away can read and write without qualification.

4. The Finding: Two Different Name Resolutions

A SystemVerilog statement inside poke_me resolves counter by looking in the task, then the module, then outward — the ordinary scoping rules of the language.

A lookup given a scope handle searches that scope. One. It does not walk outward.

So:

"Visible at the call site" and "findable from the scope that call reports" are different predicates, and nothing in either language tells you so. The gap shows up as a null handle, which is a value, which means your code decides what happens next.

Walking outward closes it. Measured, stepping to the enclosing scope until the name resolves:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  (called from inside u1's task:)
      [C] looking for "counter" in "tb.u1.poke_me"
      [C] looking for "counter" in "tb.u1"
      [C] found "tb.u1.counter" after 1 hop(s) outward, writing 22
      [SV] u1.counter=22 u2.counter=0

  (called from inside u2's task:)
      [C] looking for "counter" in "tb.u2.poke_me"
      [C] looking for "counter" in "tb.u2"
      [C] found "tb.u2.counter" after 1 hop(s) outward, writing 33
      [SV] u1.counter=22 u2.counter=33

The same C code and the same bare name reached two different objects, each the right one. That is what context is for, and it took one hop and an explicit loop.

The same bare name looked up four ways from C, measured. Called from the top module, the scope reported is tb and the name counter resolves to tb.counter, which is written correctly. Called from inside a task in instance u1, the scope reported is not the module but the task, tb.u1.poke_me, and the lookup fails outright even though the SystemVerilog code at that point can see the variable perfectly well. The same happens from instance u2. Looking the bare name up with no scope at all, from the top, also fails. And walking outward one enclosing scope at a time finds tb.u1.counter from u1 and tb.u2.counter from u2, after one hop in each case, so the same C code and the same bare name reach two different objects correctly. The conclusion is that C's name resolution is not SystemVerilog's: a scope handle names one scope and the lookup does not search the scopes that enclose it.Where the call wasmade fromthe same bare name everytimeThe scope the callreportsmeasuredDoes the nameresolve there?measuredWhat the call sitecan seeaccording toSystemVerilog's own rulesFrom the top moduledirectly in an initialblockthe top modulemeasured: the scope is tbyesmeasured: resolved andwrittenthe same objectthe two agree hereFrom a task insidean instancewhere you would actuallycall itthe task, not themodulemeasured: the innermostscopenomeasured: not foundthe variable,plainlyso the two disagreeThe bare name withno scope at allan absolute lookupnone is usedthe search starts at thetopnomeasured: not foundnot applicablethere is no call siteWalking outwarduntil it resolveswhat SystemVerilog doesfor itselfthe task, then themodulemeasured: one hop outwardyesmeasured: the right onein each instancethe same object atlastthe two agree againC name resolution is not SystemVerilog name resolutionmeasured — a scope handle names one scope, and the lookup does not search the scopes enclosing it, so a name thatis plainly visible at the call site is not necessarily findable from the scope that call reports12
Figure 1 — the same bare name looked up four ways. The first row is the comfortable case where the call is made directly in the module that owns the variable and everything agrees. The second row is the realistic one and the reason this chapter is rated critical: called from inside a task, the scope reported is the task rather than the module, and the lookup fails outright for a variable the SystemVerilog at that point reads without qualification — so the last column disagrees with the third. The third row shows that an absolute lookup of a bare name has nowhere to start. The fourth is the fix: stepping outward one enclosing scope at a time found the right counter in each instance after a single hop, from identical C and an identical name.

5. Writing C That Lands Where You Meant

Four rules, each from a measurement.

Check every handle. A failed lookup is a null handle, measured three times above. It is not an error, not an exception and not a crash — it is a value, and if your code dereferences it without asking, Chapter 20.10 §7's situation applies exactly.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
vpiHandle target = lookup(name, scope);
if (!target) { report("scope: could not resolve \"%s\"", name); return -1; }

Report the scope you were actually given, not the one you assumed. Every measurement above prints the scope's full name, and that is why the surprise in §3 was a two-line discovery rather than an afternoon. A callback that cannot find something should say where it looked.

Resolve once, not per call. A lookup is a search; the handle it returns is reusable. Resolve at set-up, store the handle alongside the per-instance state Chapter 22.7 established, and the per-call path becomes a use rather than a search — which also means a failure surfaces at set-up time instead of in the middle of a run.

And decide deliberately between a bare name and a full path. §6 is that decision.

6. The Misconception: Pass the Full Path and the Problem Goes Away

"All of this is avoidable. Pass tb.u1.counter instead of counter and there is no scope question at all."

Measured true, narrowly: every full-path lookup in §2 resolved and wrote correctly, including into both instances distinctly.

And it is the wrong design, for three reasons that only appear once the model is used twice.

The path hard-codes the instance. "tb.u1.counter" serves u1 and nothing else. The second instance needs a second string, from somewhere, chosen by someone — and §4's walking version served both from identical C with an identical name.

The path hard-codes the hierarchy. Rename the instance, move the component under another level, instantiate it inside a generate block, and every string is wrong. They are wrong silently, because a failed lookup is a null handle: the symptom is a callback that quietly stops doing anything, not a build error.

And it puts testbench structure inside the C. The C model now contains knowledge of where in the hierarchy it is used, which is exactly the coupling the reverse direction was supposed to avoid — Chapter 20.10 §5's argument about opacity, pointed at the testbench instead of at a data structure.

The accurate position:

Use the context to resolve a bare name relative to where you were called from, check the handle, and resolve once at set-up. A full path is reasonable for something genuinely singular — a top-level control block, a single global scoreboard — and even then the string belongs in one place, not scattered through the model.

7. What You Should Be Able to Say Now

  1. A callback has to land somewhere, and with two instances the name of everything it touches is ambiguous.
  2. From the LRM, a context function can learn the scope it was called from and set the scope its callbacks use.
  3. Measured: full hierarchical paths all resolve — tb.flag, tb.u1.counter, tb.u2.counter — and a bogus one returns a null handle, not a crash.
  4. The scope a call reports is the innermost one — measured, tb.u1.poke_me, the task, not the module.
  5. The finding: a bare name failed to resolve from that scope, for a variable the SystemVerilog at the call site reads without qualification.
  6. C's name resolution is not SystemVerilog's — a scope handle names one scope and the lookup does not search outward.
  7. Walking outward closes the gap — measured, one hop, finding tb.u1.counter and tb.u2.counter from identical C and an identical name.
  8. Check every handle, report the scope you were given, and resolve once at set-up rather than per call.
  9. A full path works and hard-codes the instance and the hierarchy — and when it breaks, it breaks silently into a null handle.
  10. Two instances is the test; one instance makes every one of these designs look correct.

8. Exercises

  1. Look up a full hierarchical path from C and write through it. Then look up a path that does not exist. What exactly does the second one return?
  2. Call into C from inside a task in an instance and print the scope's full name. Is it the module or the task? Was that what you expected?
  3. Try to resolve a bare name from that scope. Does it resolve? Can the SystemVerilog statement next to the call read the same name?
  4. Implement the outward walk. How many hops does it take, and what does it find in each of two instances?
  5. §5 says to resolve once at set-up. What changes about when a failure is discovered?
  6. Rename an instance in your testbench and re-run a model that uses full paths. What is the symptom, and how long would it take to attribute?
  7. Build a test with two instances of a component that calls back into C. What would a single-instance test have failed to show?
  8. §6 says a full path puts testbench structure inside the C. Name another place in this track where the same coupling was argued against.

9. Summary

  • Two instances make every name ambiguous — that is what context is for.
  • Measured: full paths resolve (tb.flag, tb.u1.counter, tb.u2.counter); a bogus path gives a null handle.
  • The scope reported is the innermost — measured tb.u1.poke_me, the task.
  • The finding: the bare name failed there, for a variable visible to the SystemVerilog beside the call.
  • C's lookup searches one scope; SystemVerilog searches outward.
  • Walking outward, one hop, found the right counter in each instance from identical C.
  • Check every handle · report where you looked · resolve once at set-up.
  • A full path hard-codes the instance and the hierarchy and fails silently into a null handle.
  • Two instances is the test — the fifth time in this track that one of something made a broken design look correct.

Scope answers where a callback lands. It says nothing about when, and the two questions have very different answers: by the time a callback has landed, control has crossed the boundary twice and the simulator has been waiting throughout. Following that path precisely is what makes the next chapter's constraints inevitable rather than arbitrary.

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.