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:
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:
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); }use_plain : 2 call(s) emitted for TWO source calls
use_pure : 1 call(s) emitted for TWO source callsOne 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:
static uint32_t counter = 0;
uint32_t c_next(void) { return ++counter; }three calls from 'instance A': 1 2 3
one call from 'instance B' : 4 <- SAME counter, not a fresh oneThat 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,svSetScopeand 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
| Declaration | The C may… | The simulator may… | Use for |
|---|---|---|---|
pure | depend only on arguments; nothing else | skip, cache or reorder calls | a stateless computation — a checksum, an encode, a decode |
| (neither) | keep state, do I/O | must call it every time; no callback | the default and the right answer most of the time |
context | call back into SystemVerilog, know its scope | must call it every time, plus scope bookkeeping | a 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:
/* 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
puremakes 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
contextwhen the function must call back or is a task — that is a requirement, not a choice. Usepurewhen 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
- Two qualifiers, opposite in spirit.
pureis a promise that lets the simulator do less;contextis a request that makes it do more. puremeans the result depends only on the arguments and there are no side effects — no state, no I/O, no callback.- 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. - Same mechanism as Chapter 17.1 §3, supplied deliberately rather than withdrawn by
volatile. - Breaking the promise is undefined behaviour with 13.2's signature — it will work, until a different simulator or version exploits the licence.
- Three disqualifiers: any
staticor global state, any I/O, any possibly-impure call. Measured: a static counter gave1 2 3then4from a second caller. - A debugging
printfmakes 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. contextis required for any import that calls an exported function, for the scope interfaces, and for every imported task.- Nothing is both, because calling back is a side effect.
contextis something you need;pureis something you give away. Default to neither, and write thepureobligation beside the C.
9. Exercises
- Reproduce §2's measurement with
__attribute__((const))and confirm 2 calls against 1. Then add aprintfto the pure function's definition and say what the attribute now asserts falsely. - Take Chapter 19.4's CRC import. Is it eligible for
pure? List what would have to stay true. - Write the comment from §6 for a function of your own, naming the SystemVerilog file that holds the promise.
- For each of the three disqualifiers in §3, write a plausible one-line edit to a model that introduces it accidentally.
- §4 says nothing is both
pureandcontext. Construct the argument from the definitions, without appealing to a rule. - A model must report through the testbench's messaging. Which qualifier does it need, and what does Chapter 19.2 §2 say about why?
- A colleague proposes declaring every stateless import
pureas a project standard. Write the three-sentence reply using §6 and §7.
10. Summary
pureis a promise that lets the simulator do less;contextis 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 3then4), I/O, impure calls. - A debugging
printfbreaks purity and can misreport the arguments — 17.6 §4's shape. contextis required for callbacks, the scope interfaces, and every imported task.- Nothing is both — calling back is a side effect.
- Default to neither.
contextis needed;pureis 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.
