C Part II · Module 22 · Calling C from SystemVerilog
Keeping State in C Across Calls
Holding a value between calls is four lines of C. Deciding whose value it is, is the chapter. Three designs measured head to head on the same two streams: with one stream running they all return 0x52, so the first test anybody writes cannot tell them apart. Interleave two and a single shared accumulator reports 0xDA and 0x37 — neither of which is either stream's answer — while both the other designs return the right pair. Only one of the three can refuse an identity it never issued, and all three cost about the same four lines.
Expert24 min readCPart IIVerificationDPI-C
Part II · Module 22 · Chapter 22.7 · Keeping State in C Across Calls
1. The Easy Half and the Hard Half
From the LRM, nothing special is needed to keep state between calls: your C runs in the simulator's process, an object with static storage duration persists, and the next call sees what the last one left. Chapter 19.7 established the execution model this rests on.
So the easy half is four lines:
static uint8_t G;
void g_reset(void) { G = 0; }
uint8_t g_feed(uint8_t b) { G = step(G, b); return G; }The purpose's second clause is the chapter: "understand what that means when several instances of the caller exist." The hard half is not how to keep state but whose state it is — and that question is decided by a design choice the C makes, which the testbench then has to live with.
Every import in this module so far has been a function of its arguments. This one gives that up deliberately, and §2 measures what it costs.
2. Three Designs
Each keeps a running CRC. Chapter 22.4's step is reused unchanged, so the arithmetic is already checked against a published value and nothing in this chapter depends on getting it right again.
One — a shared accumulator. The four lines above. One static, no identity at all.
Two — an identifier the caller picks.
static uint8_t I[NSLOT];
void i_reset(int id) { if (id >= 0 && id < NSLOT) I[id] = 0; }
uint8_t i_feed(int id, uint8_t b) { if (id < 0 || id >= NSLOT) return 0;
I[id] = step(I[id], b); return I[id]; }Three — a handle the C side issues. Chapter 20.10's design, index plus generation, cited rather than re-derived:
int h_open(void); /* returns a handle, or -1 */
uint8_t h_feed(int h, uint8_t b); /* 0 if the handle is not one we issued */
int h_close(int h);3. One Stream Cannot Tell Them Apart
Measured, the CRC of a three-byte stream through all three:
CRC of "ABC": global 0x52, id 0x52, handle 0x52 -- all equal: yesIdentical. Every design is correct, every test passes, and nothing distinguishes them.
That is this module's recurring pattern — Chapter 21.2 §5's byte-identical array models, Chapter 21.5 §6's zero-based range, Chapter 21.6 §6's symmetric matrix — and it is why the design choice here is so often made by accident: the first test anybody writes is a single stream, and it confirms whichever design you happened to pick.
4. Two Streams Separate Them Immediately
Measured, the same two streams interleaved byte by byte:
global : stream A reported 0xDA, stream B reported 0x37
(one accumulator saw all 7 bytes, so neither is a stream's CRC)
ids : stream A 0x52, stream B 0xCC
handles: stream A 0x52, stream B 0xCC
ids and handles agree with each other: yes
and with the ONE-STREAM answer for A: yesThree things in that block are worth separating.
The global design returns two numbers and neither is an answer. 0xDA is not stream A's CRC and 0x37 is not stream B's — one accumulator folded all seven bytes of both streams in arrival order, so each "result" is a CRC of an interleaving that exists nowhere in the design.
The other two agree with each other, which is a check, and weak on its own — Chapter 22.5 §5 established exactly why agreement between two implementations proves consistency rather than correctness.
And they agree with §3's single-stream answer, which is the check that matters. 0x52 was measured when stream A ran alone with nothing to interfere with it, and the per-stream designs reproduce it under interleaving. That is an external reference in the sense Chapter 22.5 §4 required: it was established under conditions the bug cannot affect.
And measured through the import, two interleaved testbench threads over the same stateful.c:
two testbench threads, interleaved, through ONE global accumulator
A reported 0xda, B reported 0x37
the same interleaving through per-stream handles
A reported 0x52, B reported 0xccByte-identical to the C binary, which is Chapter 22.4 §3's structure holding up once more.
5. The Finding: Only One of Them Can Say No
§4 separated the designs on arithmetic. The rest of the comparison is about misuse, and it is where the second and third designs part company.
The shared accumulator has no identity to get wrong, so every misuse is the same misuse. Measured, a second stream run without a reset:
measured: B after A without a reset = 0x1C (B alone is 0xCC)The caller-chosen identifier can be chosen twice. Measured, two callers that both pick 0:
measured: both report from one accumulator, A=0xDA B=0x89Note the shape: the function did exactly what it was asked, the index was in range, nothing was out of bounds, and two independent streams quietly merged. An identifier the caller picks is only as good as the caller's bookkeeping, and the C has no way to audit it — an index is just a number, so a stale one and a live one are the same value.
The C-issued handle can refuse. Measured:
measured: feeding a closed handle returns 0x00, live handles now 1
measured: the reissued handle differs from the stale one: yes
measured: the stale handle still rejected after reissue: yesThat third line is the one that matters, and it is Chapter 20.10 §8's generation counter earning its keep: the slot was reused by a new stream, and the old handle is still rejected rather than silently attaching to somebody else's data.
So the three designs are not three styles. Each one fixes the previous one's measured failure, and the cost is roughly the same four lines in every case:
All three are correct for one stream. Only the third can refuse an identity it never issued.
6. The Misconception: We Only Run One Stream at a Time
"Our testbench has one monitor on one interface. There is only ever one stream, so a file-scope accumulator is simpler and perfectly correct."
For one stream it is correct — §3 measured all three designs agreeing at 0x52. The problem is not the arithmetic; it is where the assumption lives.
"Only one at a time" is a property of the caller, and the C cannot check it. Measured, the global design given two interleaved streams reported 0xDA and 0x37 and complained about nothing. There is no error path, because from the function's point of view nothing unusual happened: it was fed seven bytes and it folded seven bytes.
A second instance is a normal thing for somebody else to add. A second channel, a second monitor on the other direction, the same component instantiated twice in a different test. Nobody doing that has any reason to read your C, and nothing in the import's declaration mentions that it holds one accumulator for the whole simulation.
And concurrency is not required for the failure. Chapter 19.7 established that a DPI call is an ordinary call in the simulator's process; §4's interleaving was two sequential threads taking turns, which is the ordinary way two testbench components behave. Nothing exotic is needed.
The accurate position:
A static is correct for exactly as long as the assumption holds, and the assumption is held by somebody who cannot see it. The handle design costs the same four lines, is correct for one stream and for a hundred, and refuses an identity it never issued — which is the only one of the three properties that gets you a diagnostic instead of a wrong number.
And the honest limit: if the state genuinely belongs to the program rather than to a stream — a log file, a configuration read once at start-up, a counter of everything the model has ever seen — a file-scope variable is right and a handle would be ceremony. The test is Chapter 20.10 §9's: could two of them ever be alive at once?
7. What You Should Be Able to Say Now
- Holding state is four lines; deciding whose state it is, is the design.
- Three designs: a shared accumulator · an identifier the caller picks · a handle the C side issues (20.10's).
- One stream cannot tell them apart — measured, 0x52 from all three, which is why the choice is usually made by accident.
- Two interleaved streams separate them at once — measured: global
0xDAand0x37, neither a stream's answer; ids and handles both0x52and0xCC. - The per-stream designs agree with the single-stream answer, which is the external reference 22.5 §4 asked for.
- Measured through the import:
0xda/0x37global,0x52/0xcchandles — identical to the C binary. - Missing a reset continues the previous stream — measured,
0x1Cwhere the stream alone is0xCC. - Two callers can pick the same identifier — measured,
0xDAand0x89, silently, with every index in range. - Only a C-issued handle can refuse — measured, a closed handle returns
0x00and stays rejected after its slot is reissued. - State costs the testing property this module relied on — give every test its own handle and it comes back.
- "Only one at a time" is the caller's assumption and the C cannot check it — measured, no complaint whatsoever.
8. Exercises
- Build all three designs over the same accumulator function. Run one stream through each. Can any test distinguish them?
- Interleave two streams. Record all six results. Which two designs agree, and what makes their agreement meaningful rather than circular?
- Explain in one sentence why
0xDAis not stream A's CRC, in terms of what the accumulator saw. - Run a second stream through the global design without resetting. How far is the answer from the correct one, and could you tell by looking at it?
- Have two callers pick the same identifier. What does the function do, and which part of it could have objected?
- Close a handle, feed it, then open a new one and feed the stale handle again. What does each call return, and which mechanism makes the last one work?
- Write a test for a stateful import that passes in one order and fails in another. What does that tell you about where the state should live?
- For a stateful model you know, apply Chapter 20.10 §9's test: could two of them ever be alive at once? What follows from your answer?
9. Summary
- Four lines to hold state; the design is deciding whose it is.
- Three designs: shared accumulator · caller-chosen id · C-issued handle (20.10).
- One stream: all three give
0x52— the first test confirms whichever you picked. - Two interleaved: global
0xDA/0x37, neither a stream's answer; ids and handles0x52/0xCC, matching the single-stream reference. - Identical through the import —
0xda/0x37and0x52/0xcc. - Missing reset →
0x1Cwhere the stream alone is0xCC. - Two callers, one id →
0xDAand0x89, silently, all indices in range. - Only the C-issued handle can refuse —
0x00, and still rejected after the slot is reissued. - State breaks per-call testability — restore it by giving every test its own handle.
- "Only one at a time" is an assumption the C cannot check, held by someone who cannot see it.
- The test: could two of them ever be alive at once?
A stateful model still answers questions the testbench asks. The last import in this module does something different — it is asked to produce something, where there is no input to be a function of, and the only thing standing between a useful test and an unreproducible one is a decision about where the numbers come from.
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.
