Skip to content
VLSI Mentor

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

Pure and Context Imports

Two optional words may appear in an import declaration, and each is a claim about the C function rather than a description of its type — which makes them the only part of the DPI contract that no signature check could ever validate. pure says the function depends on nothing but its arguments and changes nothing, which licenses the simulator to call it fewer times than you wrote, or not at all. context says the opposite: it may reach back into the simulator, so the caller's scope must travel with the call and certain optimisations are off. The cost of a false pure claim is measured here, using the C++ attribute that carries the same promise, and the result is larger than people expect: two calls became zero, and a thousand loop iterations became zero. It also produced an anomaly that refuted a hypothesis of mine and is published alongside, because the explanation turned out to be the thing worth knowing — the claim only bites when the definition is in another translation unit, which is exactly the situation DPI is always in.

Expert20 min readC++Part IIVerificationDPI

Part II · Module 28 · Chapter 28.11 · Pure and Context Imports

1. Three Kinds of Import

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Reference syntax -- not executed here, per Chapter 28.1 §5.
import "DPI-C" function int        crc32(input int data);            // plain
import "DPI-C" pure function int   mix(input int a, input int b);    // pure
import "DPI-C" context function int model_step(input byte d);        // context
qualifierwhat you are claimingwhat the simulator may do
(plain)nothing in particularcall it exactly as written; no scope established
puredepends only on arguments, no side effectselide, cache or reorder calls
contextit may call back into SystemVerilogestablish the caller's scope; restrict optimisation

The crucial property, and the reason this chapter exists: these are promises about behaviour, not statements about types. Chapter 28.2 §2's table listed the qualifier as the one part of the contract that is checked by nothing — a signature cross-check like Chapter 28.12 §4's can compare types and arity, and it has no way to know whether your C function touches a global.

2. What pure Means, Precisely

The standard's requirements for a pure import, which are stricter than "no side effects" in casual use:

The result depends only on the argument values. No globals, no statics, no files, no time, no state of any kind. It has no side effects. No writes to anything that outlives the call, no I/O, no allocation that is observable. And it may not call back into SystemVerilog, nor use any DPI utility function.

Also, by construction: a pure import may have no output or inout arguments and must return a value — writing through a pointer is a side effect, and a void function with no side effects does nothing at all.

What that buys the simulator is the freedom to not call your function. If the arguments are the same as last time, the previous result will do; if the result is unused, the call can go; if the call is loop-invariant, it can be hoisted. Those are the same licences a C++ compiler takes from __attribute__((const)) — which is what makes the cost measurable here even though DPI is not.

3. The Cost of a False Claim

The experiment: a function that increments a counter — a side effect, so the purity claim is false — declared with the C++ attribute that carries the same promise, and called in three patterns.

With the definition in the same translation unit, first:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A. an honest function, called twice in one expression
   result = 84   calls actually made = 2

B. the SAME function with a false purity claim
   result = 84   calls actually made = 0

C. the claim also licenses hoisting out of a loop
   1000 iterations -> calls actually made = 1000
   the honest version                   -> calls made = 1000

Arm B is already striking: two calls became zero. Not one — zero. With the arguments constant and the function declared pure, the compiler computed the result at compile time and the side effect never happened at all. The answer, 84, is correct; the counter says the function was never entered.

And arm C refuted what I expected. I predicted the loop-invariant call would be hoisted to a single call, and it was called a thousand times — exactly like the honest version. That is a failed prediction, and chasing it produced the chapter's real content.

The explanation is the translation unit. In that build the definition sits in the same file as the call, so the compiler can see the real body — including the counter increment — and it inlined that body rather than reasoning from the attribute. The claim was available and the truth was also available, and for the loop the truth won.

Which is the wrong model of DPI, because a simulator never has your body. It has a declaration. So the experiment was rebuilt with the definition in a separate translation unit:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A. no purity claim, called twice
   result = 84   calls made = 2

B. a FALSE purity claim, the same two calls
   result = 84   calls made = 0

C. loop-invariant, 1000 iterations
   claimed pure -> calls made = 0
   honest       -> calls made = 1000

A thousand calls became zero. The honest version was called a thousand times; the one with the false claim was called not once, because across a translation-unit boundary the claim is the only information available and the compiler is entitled to act on it completely.

Three conclusions.

pure is a promise, not a hint. A false claim does not make the simulator slightly over-eager; it can remove every call.

The damage is to the side effect, not the answer. Both arms returned 84. So a model whose purity claim is false but which also happens to compute correct return values will produce right answers and silently never update its own state — the counter, the log, the histogram, the accumulated CRC.

And the single-TU anomaly is the generalisable part. An experiment about a cross-boundary claim has to put the two sides in different translation units, or the compiler's extra knowledge changes the answer. That is the same lesson Chapter 26.7 §4's callout recorded when a static-variable disassembly described a program other than the one under discussion.

4. When pure Is Actually Appropriate

Narrower than people assume, and worth being concrete.

functionpure?why
a CRC step over one word with no stateyesresult from arguments alone
a bit-reversal or an encoding conversionyes
a mathematical functionyes
a CRC accumulator holding statenothe result depends on history
anything with a handle argumentnothe handle's target is state
anything that logs or countsnothat is the side effect
anything with an output argumentno — and it is not allowed
anything that calls backno — use context

The test to apply: could you call this function a million times, or never, and would the simulation be identical? If the answer is anything other than an unqualified yes, the import is not pure.

Note what that excludes: most reference models are not pure, because a model with state is the normal case — Chapter 28.13's handle-based model is emphatically not. The pure cases are the small stateless helpers, and that is the right expectation to carry.

And the benefit of a correct pure claim is modest. It lets the simulator skip redundant calls; Chapter 27.10 §3's figures put one call's cost far below one simulation step. So the honest cost-benefit is a small saving against the risk in §3, which argues for declaring pure only where it is both true and obviously true.

5. What context Means

The mirror image: context tells the simulator the function may reach back, so the caller's scope must be established before the call.

What it enables:

Calling an exported function or task. Chapter 28.4 §3's rule — an exported task requires a context import, and an exported function needs the scope to land in. Using the DPI utility functions — svGetScope, svSetScope, svGetNameFromScope and the rest. And svScope-dependent behaviour generally, which is anything that needs to know which instance called.

What it costs:

The scope must be set up on every call, which is a small overhead the simulator pays whether or not you use it. And some optimisations are off, because the function is now known to be able to affect the simulator's state.

The rule is simple and the failure is not subtle: declare context when the function calls back, and not otherwise. Omitting it where it is needed gives you Chapter 28.4 §4's measured problem — a callback with no scope to land in — and adding it where it is not needed costs a little performance and nothing else. So when in doubt, context is the safe error and pure is the dangerous one.

6. Choosing, in Practice

A decision procedure:

Does the function call back into SystemVerilog, or use a DPI utility function? → context. Does it have state, a handle, an output argument, a log, or a counter? → plain. Could it be called a million times or never with an identical simulation? → pure is permitted, if you are sure. Otherwise → plain.

Plain is the right default, and the one to use when unsure. It makes no claim, so no claim can be false; the simulator calls it exactly as written.

A note on changing a qualifier later. Adding pure to an existing import is a behavioural change, not an optimisation, because it licenses the simulator to stop calling the function. If a model's state later gains a counter or a log line, an existing pure claim becomes false at that moment — and nothing will say so. So a pure import deserves a comment at the C definition saying it is claimed pure, which is the only thing that will remind the next person editing the body.

7. The Table

the questionthe answermeasured
the three kindsplain, pure, context§1
are these type claimsno — behavioural promises, checked by nothing§1
what does pure requireresult from arguments only, no side effects, no callbacks§2
may a pure import have output argsno, and it must return a value§2
what does a false pure claim costtwo calls became zero; 1000 became zero§3
was the answer wrongno — 84 both times; only the side effect vanished§3
what did the same-TU build show1000 calls still made — my prediction refuted§3
why the differencewith the body visible the compiler used the truth, not the claim§3
which build models DPIthe separate translation unit§3
is a typical reference model pureno — state is the normal case§4
what does context enableexported calls, DPI utilities, scope awareness§5
which error is safean unnecessary context; a false pure is not§5, §5 callout
can a checker validate a qualifierno — it would require analysing the body§5 callout
does a test catch a false pureno, if it only checks return values§5 callout

8. What to Actually Do

  • Default to plain. It makes no claim, so no claim can be false.
  • Declare context whenever the function calls back or uses a DPI utility function — the unnecessary case costs only a little performance.
  • Apply the test before writing pure: a million calls or none, with an identical simulation. Any qualification means no.
  • Never declare pure on anything with state, a handle, an output argument, a log or a counter.
  • Expect most reference models not to be pure. State is the normal case.
  • Comment the C definition of anything claimed pure, because the claim lives in the other language and the next editor will not see it.
  • Treat adding pure as a behavioural change, not a tuning step.
  • Do not rely on a test to catch a false claim — the return values stay correct and only the lost side effect reveals it.
  • Put the two sides in different translation units when measuring anything about a cross-boundary claim.

9. Mistakes Worth Making Once

Declaring pure on a function that counts something. 1000 calls became 0, and the answers stayed right.

Concluding from correct answers that a pure claim is fine. Both arms returned 84.

Declaring pure on a model with a handle. The handle's target is state.

Giving a pure import an output argument. A side effect, and not allowed.

Omitting context on an import whose C function calls back. Chapter 28.4 §4's silent wrong scope.

Measuring a cross-TU claim with both sides in one file. The compiler uses the body and the claim does nothing.

Adding pure as a performance tweak to an existing import. It changes behaviour.

Letting a body gain a log line while an old pure claim stands. Nothing anywhere objects.

10. Interview Relevance

The frozen curriculum marks this High. The question is usually what do pure and context mean on a DPI import?

pure claims the function's result depends only on its arguments and that it has no side effects — so the simulator may cache, reorder, or not make the calls. It therefore cannot have output arguments, must return a value, and must not call back. context claims the opposite: the function may reach back into SystemVerilog, so the caller's scope is established on every call and some optimisations are disabled. Plain makes no claim and is the right default.

The point that makes the answer land is that these are promises about behaviour, not statements about type — which makes the qualifier the one part of the DPI contract that no checker could ever validate, since verifying "has no side effects" means analysing the body transitively.

Then the measurement. Using the C++ attribute that carries the same promise, a false purity claim turned two calls into zero and a thousand loop iterations into zero — while the returned answers stayed correct at 84. So a test that checks return values passes a false claim perfectly, and what actually broke was the model's own state, which was never updated.

And the detail worth volunteering is that this only showed up with the definition in a separate translation unit. With the body visible in the same file the compiler inlined the real code and made all thousand calls — so an experiment about a cross-boundary claim has to separate the sides, or it measures the wrong thing. Finishing with the asymmetry — an unnecessary context costs a little performance, a false pure can delete every call — is the judgement an interviewer is looking for.

11. What You Should Be Able to Say Now

  • The three kinds — plain, pure, context — and that the qualifier is a behavioural promise, not a type claim.
  • That pure requires the result to depend on arguments alone, forbids side effects and callbacks, permits no output arguments, and must return a value.
  • That it licenses the simulator to cache, reorder or elide calls.
  • That a false claim turned two calls into zero and 1000 loop iterations into zero, while both arms returned the correct 84.
  • That therefore a test checking return values passes a false pure claim, and the damage is to the model's own state.
  • That the effect only appeared with the definition in a separate translation unit — with the body visible the compiler made all 1000 calls — so a cross-boundary experiment must separate the sides.
  • That most reference models are not pure, because state is the normal case, and anything with a handle, a counter or a log is excluded.
  • That the test to apply is a million calls or none, identically.
  • That context enables exported calls, the DPI utility functions and scope awareness, at the cost of scope setup and some lost optimisation.
  • That the two errors are asymmetric: an unnecessary context is cheap, a false pure can remove every call — so the default belongs on the cheap side.
  • That no checker can validate a qualifier, which makes it the one place where a careful one-time human decision is the only safeguard.

12. Exercises

Exercise 1 — the three declarations. Write a plain, a pure and a context import and say what each claims.

Exercise 2 — the false claim, same file. Put a counting function and its caller in one translation unit, declare it pure, and record the call counts for all three patterns.

Exercise 3 — the false claim, two files. Move the definition to a separate translation unit and repeat. Explain the difference.

Exercise 4 — the answers stay right. Confirm that both arms return the same value, and write the test that would have caught the false claim.

Exercise 5 — the purity audit. Take four functions from a model you know and apply §4's test to each. Count how many pass.

Exercise 6 — the drift. Add a log line to a function whose import is declared pure and note that nothing anywhere objects.

Exercise 7 — the asymmetry. Write down the cost of an unnecessary context and of a false pure, and decide which way your defaults should lean.

13. Summary

  • Three kinds of import: plain makes no claim, pure promises the result depends only on the arguments with no side effects, and context promises the function may call back so the caller's scope must be established.
  • The qualifier is a promise about behaviour, not a statement about type, which makes it the one part of the DPI contract that no checker could validate — verifying "no side effects" means analysing the body transitively.
  • Measured, with the C++ attribute carrying the same promise: a false purity claim turned two calls into zero, and a thousand loop iterations into zero.
  • The returned answers stayed correct — 84 in both arms — so what broke was the model's own state, and a test that checks return values passes the false claim perfectly.
  • That effect required the definition to be in a separate translation unit. With the body in the same file the compiler inlined the real code and made all thousand calls, which refuted my prediction and is published alongside, because the explanation is the lesson: a cross-boundary claim must be measured across a boundary.
  • pure excludes output arguments, requires a return value, and forbids callbacks — and most reference models are therefore not pure, since state is the normal case.
  • The test is: a million calls or none, with an identical simulation. Any qualification means plain.
  • context enables exported calls, the DPI utility functions and scope awareness, costing scope setup and some optimisation.
  • The two errors are wildly asymmetric — an unnecessary context costs a little performance, a false pure can delete every call — so the default belongs on the cheap side, which is the same reasoning that makes seq_cst the right default for memory ordering.

Chapter 28.12 collects every failure in this module that produces a plausible wrong answer, and works each one from symptom to cause.

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.