Skip to content
VLSI Mentor

C Part II · Module 19 · DPI-C Foundations

Pure and Context Imports

Two qualifiers can appear on an import declaration and they pull in opposite directions. One is a promise you make about your C that lets the simulator do less work — including skipping calls entirely, which is measured here collapsing two calls into one. The other is a request that makes the simulator do more, and it is what a function needs before it may call back into SystemVerilog at all. Nothing is both, breaking the first is undefined behaviour with no diagnostic, and the default is neither.

Advanced21 min readCPart IIVerificationDPI-C

Part II · Module 19 · Chapter 19.5 · Pure and Context Imports

1. Two Qualifiers, Opposite in Spirit

Chapter 19.4's declaration carried neither qualifier:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import "DPI-C" function byte unsigned c_crc8(input byte unsigned data[], input int len);

Two can appear, and they are not two settings of the same dial.

pure is a promise you make about your C, and it lets the simulator do less: cache results, reorder calls, or skip them entirely.

context is a request you make of the simulator, and it makes the simulator do more: track which SystemVerilog scope called your function, so that your function can get back there.

Nothing is both — §4 explains why that is structural rather than a restriction — and the default is neither.

2. What pure Promises

From the LRM: a pure imported function's result must depend only on its arguments, and it must have no side effects whatsoever. It may not call an exported function, access any simulation state, perform I/O, or read or write anything that persists between calls.

In exchange, the simulator may treat two calls with the same arguments as interchangeable — cache one result, reorder them, or not call at all.

That licence is worth seeing in action, and C has an exact analogue. Measured, at -O2:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
extern int plain_f(int);
extern int pure_f(int) __attribute__((const));   /* no side effects, args only */

int use_plain(int x) { return plain_f(x) + plain_f(x); }
int use_pure (int x) { return pure_f(x)  + pure_f(x);  }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
use_plain  : 2 call(s) emitted for TWO source calls
use_pure   : 1 call(s) emitted for TWO source calls

One call for two. The compiler evaluated pure_f(x) once and used the result twice, because the attribute promised that a second call with the same argument would return the same thing and do nothing else.

This is the same mechanism Chapter 17.1 §3 measured from the other direction — there, two reads of an ordinary object became one load because nothing could have changed it between them, and volatile withdrew the premise. Here the premise is supplied deliberately.

This is an analogue, not DPI itself — DPI cannot be executed on this machine, as Chapter 19.1 §4 verified. What it demonstrates is the shape of the licence: a promise about purity lets the thing above you skip work, and the skipping is invisible from the source.

3. What Breaking the Promise Costs

Declaring pure on a function that is not pure is a defect with a familiar character.

Consider a function with a counter in it — Chapter 19.1 §5's measurement, which exists for Chapter 19.7 and matters here too:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
static uint32_t counter = 0;
uint32_t c_next(void) { return ++counter; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
three calls from 'instance A': 1 2 3
one call from 'instance B'   : 4   <- SAME counter, not a fresh one

That function's result does not depend only on its arguments — it has none — and it has a side effect. Declaring it pure would be a lie, and the consequence is that the simulator may call it a different number of times than the source suggests, which changes the counter's value.

From the LRM: the behaviour of a pure function that is not pure is undefined.

That is Chapter 13.2's category, and it has 13.2's signature: it will work. A simulator that happens not to exploit the licence produces the expected answers, and the code ships. A different simulator, a different optimisation setting, or a later version exploits it, and the counter is wrong by an amount nobody can explain from reading the source.

Three things that disqualify a function from being pure, and all three are easy to introduce without noticing:

Any static or global state. Including a lazily-initialised table, a cache, or a seed.

Any I/O. A printf for debugging makes a pure function impure, and adding one temporarily during an investigation changes the number of times it is called.

Any call that might not be pure — including a call back into SystemVerilog, which §4 shows is excluded anyway.

4. What context Grants

From the LRM: a context import is one that needs to know the SystemVerilog scope from which it was called. The simulator maintains that association across the call, and the C may then use it — most importantly to call back into SystemVerilog.

It is required for:

  • any import that calls an exported task or function — Chapter 19.2 §2's direction
  • any import using the scope-handling interfaces (svGetScope, svSetScope and relatives)
  • every imported task, which Chapter 19.3 §4 flagged and this chapter owns: a task may consume time, and consuming time means reaching back into SystemVerilog

The cost is bookkeeping. The simulator must establish and maintain the scope association for each call, which is work a plain import does not require.

Why nothing is both pure and context: calling back into SystemVerilog is a side effect, and a pure function may not have one. The two qualifiers are mutually exclusive by construction rather than by a rule someone added.

5. The Three Choices

DeclarationThe C may…The simulator may…Use for
puredepend only on arguments; nothing elseskip, cache or reorder callsa stateless computation — a checksum, an encode, a decode
(neither)keep state, do I/Omust call it every time; no callbackthe default and the right answer most of the time
contextcall back into SystemVerilog, know its scopemust call it every time, plus scope bookkeepinga model that reports, samples or drives; every imported task

The middle row deserves emphasis because it is both the default and under-used as a deliberate choice. Chapter 19.4's CRC import is stateless and could be declared pure — and leaving it plain costs nothing measurable and removes a promise that a future edit could break.

The decision procedure is two questions:

Does it need to call back into SystemVerilog, or is it a task? → context.

Otherwise, is it genuinely stateless, I/O-free, and will it stay that way? → pure is available. If you are not confident about "will it stay that way", take the default.

6. Why pure Is Worth Less Than It Looks

The reason to be cautious is that the benefit is usually small and the promise is permanent.

The benefit is eliminating duplicate calls with identical arguments. §2 measured exactly that — two calls became one. How often does a testbench call a model twice with the same arguments in a context where the simulator can see both? Occasionally, and not usually on the path that dominates runtime.

The cost is a constraint on every future edit. The function may never acquire a cache, a counter, a log line or a lookup table built on first use. None of those is an unreasonable thing to add, and each one silently invalidates the declaration — with no diagnostic, because nothing in the C says it was declared pure on the other side.

That asymmetry is worth stating plainly: the promise lives in the SystemVerilog file and the thing that breaks it lives in the C file. Chapter 16.8 §7's habit is the defence — write the constraint where the code is:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
/* Declared `pure` on the SystemVerilog side (tb/dpi_pkg.sv).
   MUST remain stateless and free of I/O: no statics, no printf, no caching.
   Adding any of those requires removing `pure` from the declaration. */
uint8_t c_crc8(const uint8_t *data, int len);

Four lines of comment, next to the code that could break it, naming the file that would have to change.

7. The Misconception: Declare It pure, It Is Faster

"If the function has no side effects, declaring it pure makes it faster. There is no reason not to."

Two errors, and the second is the serious one.

On speed: the benefit is the elimination of duplicate calls with identical arguments, which §2 measured and which is a narrow win. A model called once per transaction with different data every time gains nothing at all, because no two calls are identical.

On risk: pure is not an optimisation hint, it is an assertion about your code, and breaking it is undefined behaviour. §3's printf scenario is the common path, and Chapter 13.2's measured lesson applies — the program will work, for a while, on this simulator.

So the honest ordering is the reverse of the folklore:

Take the default unless you have a reason. Use context when the function must call back or is a task — that is a requirement, not a choice. Use pure when the function is genuinely stateless and you are prepared to keep it that way, and write that obligation down beside the C.

And one framing worth carrying: context is something you need; pure is something you give away. You discover a need for context by trying to write the model. You never discover a need for pure — you only decide to offer it.

8. What You Should Be Able to Say Now

  1. Two qualifiers, opposite in spirit. pure is a promise that lets the simulator do less; context is a request that makes it do more.
  2. pure means the result depends only on the arguments and there are no side effects — no state, no I/O, no callback.
  3. The licence is real. Measured in the C analogue: two source calls became one emitted call under __attribute__((const)), where the unqualified version emitted both.
  4. Same mechanism as Chapter 17.1 §3, supplied deliberately rather than withdrawn by volatile.
  5. Breaking the promise is undefined behaviour with 13.2's signature — it will work, until a different simulator or version exploits the licence.
  6. Three disqualifiers: any static or global state, any I/O, any possibly-impure call. Measured: a static counter gave 1 2 3 then 4 from a second caller.
  7. A debugging printf makes a pure function impure, and the simulator's licence means the printed arguments may not be the arguments it was asked about — the same shape as 17.6 §4's barrier effect.
  8. context is required for any import that calls an exported function, for the scope interfaces, and for every imported task.
  9. Nothing is both, because calling back is a side effect.
  10. context is something you need; pure is something you give away. Default to neither, and write the pure obligation beside the C.

9. Exercises

  1. Reproduce §2's measurement with __attribute__((const)) and confirm 2 calls against 1. Then add a printf to the pure function's definition and say what the attribute now asserts falsely.
  2. Take Chapter 19.4's CRC import. Is it eligible for pure? List what would have to stay true.
  3. Write the comment from §6 for a function of your own, naming the SystemVerilog file that holds the promise.
  4. For each of the three disqualifiers in §3, write a plausible one-line edit to a model that introduces it accidentally.
  5. §4 says nothing is both pure and context. Construct the argument from the definitions, without appealing to a rule.
  6. A model must report through the testbench's messaging. Which qualifier does it need, and what does Chapter 19.2 §2 say about why?
  7. A colleague proposes declaring every stateless import pure as a project standard. Write the three-sentence reply using §6 and §7.

10. Summary

  • pure is a promise that lets the simulator do less; context is a request that makes it do more. Opposite directions, not two settings.
  • pure: result from arguments alone, no side effects — no state, no I/O, no callback.
  • Measured licence: __attribute__((const)) collapsed two calls into one at -O2.
  • Breaking it is undefined behaviour with 13.2's signature: it works until it does not.
  • Disqualifiers: statics (measured — 1 2 3 then 4), I/O, impure calls.
  • A debugging printf breaks purity and can misreport the arguments — 17.6 §4's shape.
  • context is required for callbacks, the scope interfaces, and every imported task.
  • Nothing is both — calling back is a side effect.
  • Default to neither. context is needed; pure is given away — and the obligation belongs in a comment beside the C.

Both qualifiers describe what happens when the function is called. Getting it to be callable at all is a separate problem that has nothing to do with SystemVerilog semantics and everything to do with symbol names and shared objects — and one of its failure modes was measured two chapters ago.

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.