Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  CRC of "ABC": global 0x52, id 0x52, handle 0x52 -- all equal: yes

Identical. 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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:  yes

Three 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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 0xcc

Byte-identical to the C binary, which is Chapter 22.4 §3's structure holding up once more.

Three ways to keep per-stream state in C across calls, measured against the same two streams. With one stream at a time all three give 0x52, so the first test anybody writes cannot tell them apart. With two streams interleaved the difference appears: a single file-scope accumulator saw all seven bytes and reported 0xDA and 0x37, neither of which is either stream's CRC, while both the caller-chosen identifier and the C-issued handle gave 0x52 and 0xCC, agreeing with each other and with the one-stream answer. The identity is chosen by nobody in the first design, by the caller in the second, and by the C side in the third. A caller that forgets to reset continues the previous stream's CRC, measured at 0x1C where the stream alone is 0xCC. Two callers that both pick identifier zero share one accumulator silently, measured at 0xDA and 0x89. And a stale identity looks exactly like a live one unless the C side issues it: measured, feeding a closed handle returned 0x00, the reissued handle differed from the stale one, and the stale one stayed rejected afterwards.The situationthe same two streams inevery rowOne sharedaccumulatora file-scope variableAn identifier thecaller picksan index into a tableA handle the C sideissueschapter 20.10's designOne stream at atimethe first test anybodywrites0x52correct0x52correct0x52correctTwo streamsinterleavedthe whole question0xDA and 0x37one accumulator saw all 7bytes — neither is astream's CRC0x52 and 0xCCcorrect0x52 and 0xCCcorrect, and equal to theone-stream answerWho chooses theidentityand can it be refusednobody — there isonly onethere is only ever oneaccumulatorthe callerfrom a range it has toknowthe C sideand it can refuseTwo callers pickthe same onethe collision casealwaysthere is nothing else topicksilentlymeasured: both chose 0,giving 0xDA and 0x89cannot happenevery open returns adistinct handleA stale identity isreusedafter the stream endedcontinues theprevious streammeasured: 0x1C where thestream alone is 0xCClooks exactly likea live onean index is just a numberrejectedmeasured: 0x00, and stillrejected after the slotis reissuedAll three are correct for one stream; only the third can refuse a wrong identitymeasured — the same results in a plain C binary and through the import12
Figure 1 — three ways of holding per-stream state, scored on the same two streams. The second row is the one that decides the design and the first row is the reason the decision so often gets made by accident: with a single stream running, all three return 0x52 and no test can tell them apart. Interleave two streams and the shared accumulator returns 0xDA and 0x37, neither of which is either stream's answer, because one accumulator folded all seven bytes in arrival order. The lower three rows are about misuse rather than arithmetic, and they are where the second and third designs separate: an identifier the caller picks can be picked twice, and a number that is merely an index cannot be told from a live one, whereas a handle the C side issued can be refused — measured, a closed handle returned 0x00 and stayed rejected even after its slot had been reissued to somebody else.

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  measured: both report from one accumulator, A=0xDA B=0x89

Note 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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: yes

That 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

  1. Holding state is four lines; deciding whose state it is, is the design.
  2. Three designs: a shared accumulator · an identifier the caller picks · a handle the C side issues (20.10's).
  3. One stream cannot tell them apart — measured, 0x52 from all three, which is why the choice is usually made by accident.
  4. Two interleaved streams separate them at once — measured: global 0xDA and 0x37, neither a stream's answer; ids and handles both 0x52 and 0xCC.
  5. The per-stream designs agree with the single-stream answer, which is the external reference 22.5 §4 asked for.
  6. Measured through the import: 0xda/0x37 global, 0x52/0xcc handles — identical to the C binary.
  7. Missing a reset continues the previous stream — measured, 0x1C where the stream alone is 0xCC.
  8. Two callers can pick the same identifier — measured, 0xDA and 0x89, silently, with every index in range.
  9. Only a C-issued handle can refuse — measured, a closed handle returns 0x00 and stays rejected after its slot is reissued.
  10. State costs the testing property this module relied on — give every test its own handle and it comes back.
  11. "Only one at a time" is the caller's assumption and the C cannot check it — measured, no complaint whatsoever.

8. Exercises

  1. Build all three designs over the same accumulator function. Run one stream through each. Can any test distinguish them?
  2. Interleave two streams. Record all six results. Which two designs agree, and what makes their agreement meaningful rather than circular?
  3. Explain in one sentence why 0xDA is not stream A's CRC, in terms of what the accumulator saw.
  4. 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?
  5. Have two callers pick the same identifier. What does the function do, and which part of it could have objected?
  6. 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?
  7. 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?
  8. 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 handles 0x52 / 0xCC, matching the single-stream reference.
  • Identical through the import — 0xda/0x37 and 0x52/0xcc.
  • Missing reset → 0x1C where the stream alone is 0xCC.
  • Two callers, one id → 0xDA and 0x89, 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.