Skip to content
VLSI Mentor

C Part II · Module 22 · Calling C from SystemVerilog

Output and Inout Arguments

One result per call stops being enough, and the C side of the answer is something you already have: a function that writes through a pointer. What is new is whose storage the pointer names, and what the failure looks like. A return value that never gets written delivers 0 — the same every time, and someone notices. An output argument that never gets written leaves the caller's variable exactly as it was, so after one successful call every later one reports the last good answer. Measured: a loop that looked like a working model producing a steady result, and was a model producing nothing at all.

Advanced24 min readCPart IIVerificationDPI-C

Part II · Module 22 · Chapter 22.3 · Output and Inout Arguments

1. A Value and a Place

Chapter 22.2 ended on a distinction worth repeating because this entire chapter sits on it:

A return value is a value; an output argument is a place.

A return value is copied back, and the questions it raises are about type and width — which is why 22.2 could be about conversions with no mention of storage anywhere. An output argument is written into, and the storage belongs to the caller.

A C function has exactly one return value. The moment an import must report two things — a quotient and a remainder, a result and a status, a decoded value and the number of bytes consumed — that constraint binds, and from the LRM the answer is an argument declared output or inout.

2. The C Side Is Module 7, Unchanged

This is the good news, and the chapter's purpose points straight at it: an output argument is a C pointer parameter. Nothing new to learn.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
static void divmod_i32(int a, int b, int *q, int *r) {
    if (b == 0) { *q = 0; *r = 0; return; }       /* guarded — see below */
    *q = a / b;
    *r = a % b;
}

Two inputs by value, two outputs by pointer — the shape Module 7 established, with no simulator type anywhere in it. From the LRM, the matching declaration is:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import "DPI-C" function void divmod_i32(input int a, input int b,
                                        output int q, output int r);

Measured, called from a running simulation:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  17 / 5 -> q = 3, r = 2
  -17 / 5 -> q = -3, r = -2
  17 / 0 -> q = 0, r = 0   (guarded in C, not UB)

Two things to notice in those three lines.

C division truncates toward zero. -17 / 5 is -3 with a remainder of -2, not -4 remainder 3. That is from the C standard since C99, and it matters for a reference model: if the design's divider rounds the other way, the model and the design will disagree by one on every negative input, and the disagreement will be correct arithmetic on both sides.

The zero case is a guard, not a behaviour. a / b with b == 0 is undefined behaviour — Chapter 13.2's category, not a value. The measured q = 0, r = 0 is what the if statement in the function produces, and without that if the measurement would be meaningless. Decide what a model does with a degenerate input and write it down; do not let the language decide, because it has declined to.

3. Inout: Read as Well as Written

An inout argument carries a value in and a value out through the same place:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
static void bump(int *v) { *v += 1; }

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  41 bumped -> 42
  -1 bumped -> 0

*v += 1 reads *v and writes it, which is the whole point. And it creates an obligation the output case does not have: the incoming value has to actually arrive.

Measured, the same function behind glue that writes the result back but never read the argument first:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  41 bumped by a glue that did not read -> 1

1. The function computed 0 + 1, because the local it was given started at zero. The call succeeded, the write-back worked, the C function was correct — and the answer is wrong because one of the two halves of "inout" was missing.

Under DPI the compiler generates that half for you from the declaration, which is a real argument for the declaration-driven mechanism. The reason it is worth seeing explicitly is that the failure has no signature: 1 is a perfectly ordinary number, and nothing anywhere reports that an input was not collected.

4. Keep the Pointers Out of the Logic

A discipline that costs nothing and is worth stating because it is what makes the rest testable.

The function that computes should take pointers and nothing else simulator-shaped. divmod_i32 above takes four plain C parameters. It can be called from a main, compiled with every warning, run under a sanitizer, and reviewed by someone who has never seen a testbench — Chapter 21.3 §5's argument, now with a concrete shape.

What that rules out is the tempting alternative: a function that takes the simulator's handles and does the arithmetic inside the glue. It works, and it is untestable without a simulator, which this module has spent two chapters showing is the expensive kind of untestable.

And the lifetime rule applies here too. The pointer your function receives names storage the simulator owns, valid for the duration of the call — Chapter 21.7's rule, for an argument instead of a payload. Write through it during the call; do not keep it.

A return value and an output argument compared, with the failure modes measured. A return value is a value that is copied back, and in C it is the return type; there is exactly one per call. An output argument is a place that is written into, and in C it is a pointer parameter; you may have as many as you need. The difference that matters is what happens when the glue never writes the result. For a return value, measured, the testbench receives 0, because the simulator prepared a fresh place for it. For an output argument, measured, the caller's variable keeps whatever it already held: a marker of 0xDEAD survived the call unchanged. After a working call that set the variable to 3, a silent call left it reading 3 — the previous answer rather than a zero — so in a loop every iteration reports the last good result and the model looks like it is working but stuck. And an inout argument carries an extra obligation: the glue must read the incoming value before calling, because a glue that skipped the read turned 41 plus 1 into 1 rather than 42.The questiontwo ways to get a result outA return valuechapter 22.2's subjectAn output argumentthis chapter'sWhat it isthe distinction everything followsfroma value, copied backno storage question at alla place, written intosomebody else's storageWhat it is in Cthe half you already knowthe return typeone per functiona pointer parametermodule 7, unchangedHow many per callthe reason to reach for oneexactly oneand that is the constraintas many as you needmeasured: a quotient and aremainder togetherIf the glue never writes itthe failure both sharethe testbench receives 0measured — a fresh place, neverfilledthe caller's variable isuntouchedmeasured — a marker survived thecall intactSo after a good callthe shape it takes in real codestill 0the same every time, andnoticeablethe previous answer,repeatedmeasured: a good call left 3, thesilent one still reads 3And an inout owes one morethingit is read as well as writtennot applicablenothing is readthe glue must read theincoming value firstmeasured: skipping the read turned41 plus 1 into 1A value has no storage question; a place is the caller's and keeps what it hadmeasured — an unwritten return value is 0, an unwritten output argument is the last answer that worked12
Figure 1 — a return value and an output argument, compared on what each one is and on how each one fails. The top half is the straightforward part: a value is copied back and there is only ever one of it, while a place is written into, belongs to the caller, and there can be as many as the function needs. The bottom half is why this chapter is the more dangerous of the two. When the glue never writes the result, a return value arrives as 0 — the simulator prepared a fresh place and nothing filled it — whereas an output argument leaves the caller's variable holding exactly what it held before. So after one call that worked, every silent call afterwards reports the last good answer, which is what a model that is working but stuck looks like from the outside. The last row is the extra obligation an inout carries: its incoming value has to be collected before the function runs, and measured, a glue that skipped that step turned 41 plus 1 into 1.

5. The Finding: What an Unwritten Output Looks Like

Chapter 22.1 §6 measured what a return value does when the glue never hands it back: 0, every time, because the simulator prepared a fresh place and nothing filled it.

An output argument does something different, and worse. Measured, the caller's variable set to a recognisable marker before the call:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  v after the call -> 57005 (0x0000dead)

The marker survived. Nothing was written, so the caller's variable holds exactly what it held before — which is the correct behaviour for an untouched place, and is why it is dangerous.

Watch it in the shape it actually takes. Measured, a working loop:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  10 / 3 -> q = 3, r = 1
  20 / 3 -> q = 6, r = 2
  30 / 3 -> q = 10, r = 0
  40 / 3 -> q = 13, r = 1

and the same loop with a glue that never writes back:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  call 1 -> q = 0
  call 2 -> q = 0
  call 3 -> q = 0
  call 4 -> q = 0
  every iteration reported the value from before the loop

and then the realistic one — a good call followed by a silent one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  good call  -> q = 3, r = 2
  silent call-> q = 3   <- the PREVIOUS answer, not a 0

That is the finding. A broken output argument does not report a constant; it reports the last answer that worked. In a scoreboard it looks like a model that is running and producing a stable result. In a loop it looks like a design whose output has stopped changing — which is a real bug shape, so the investigation starts in the design.

And it is strictly harder to spot than 22.1's case. Zero repeated across every call is conspicuous; a plausible previous answer repeated is what success looks like until somebody checks whether the number changes when the input does.

6. The Misconception: It Is Just Another Way to Return a Result

"Returning a value and writing through an output argument are two spellings of the same thing. Same risks, pick whichever reads better."

They are two ways to get a result out, and they do not carry the same risk, because what decides whether a bug costs an hour or a week is the failure signature.

An unwritten return value is a constant. Measured, 0 — the same on every call, immediately, including the first. A test that runs once sees it.

An unwritten output argument is the caller's last value. Measured — 0xDEAD survived, and after a good call the silent one still read 3. A test that runs once after anything has set the variable correctly sees a correct answer.

And there are two obligations a return value simply does not have:

  • An inout must be read before it is written. Measured: skipping that turned 41 + 1 into 1.
  • Every output must be written on every path, including early returns — §5's callout.

The accurate position:

Use an output argument when you genuinely need a second result or a place to write into, and prefer a return value while one is enough (22.2 §6). The choice is not stylistic: an output argument hands the caller's storage to your function, which buys you more than one result and costs you a failure mode that looks exactly like working code.

7. What You Should Be Able to Say Now

  1. A return value is a value; an output argument is a place. Everything in this chapter follows.
  2. In C an output argument is a pointer parameter — Module 7, unchanged, with no simulator types in the function.
  3. Measured: 17 / 5 → q = 3, r = 2; -17 / 5 → q = -3, r = -2 — C division truncates toward zero, which a reference model must match deliberately.
  4. 17 / 0 → 0, 0 is a guard, not a behaviour — a / b with b == 0 is undefined behaviour (13.2).
  5. An inout is read as well as written — measured, 41 → 42; and with the read skipped, 41 → 1.
  6. Keep the pointers out of the logic: a function taking plain pointers is testable from a C main without a simulator.
  7. The pointer names storage valid for the call — 21.7's rule, for an argument.
  8. The finding: an unwritten return value is 0; an unwritten output argument is whatever the caller's variable held.
  9. Measured, after a good call set q = 3, the silent call still read 3 — the last answer that worked, which looks like a working model.
  10. Two checks: write every output on every path including early returns; poison the destination in tests.
  11. The test that catches it: call twice with different inputs and assert the results differ.

8. Exercises

  1. Write a C function returning a quotient and a remainder through pointers. Call it from a plain main first. What have you confirmed before any simulator is involved?
  2. Measure -17 / 5 in C. Which way does it round, and what would you check in a design's divider before trusting a model that uses it?
  3. Remove the b == 0 guard. What does the C standard say the program now does, and why is the measured output of such a program not worth reporting?
  4. Write an inout whose glue does not read the incoming value. What answer comes out for 41, and explain it in terms of what the function was given.
  5. Set an output destination to a recognisable marker, call a function that never writes it, and print the variable. Then do the same for a return value. Which failure would you have noticed first?
  6. Build the loop from §5 both ways. Describe what the broken version looks like to someone reading only the printed results.
  7. Write a function with an output argument and an early return that skips the write. Under what input does it fail, and what does the caller see?
  8. For an import you know with an output argument, write the two-call test from §5's callout. Would the existing tests have caught a stale output?

9. Summary

  • A value versus a place — and in C a place is a pointer parameter (Module 7).
  • Measured: 17/5 → 3, 2; -17/5 → -3, -2 (truncation toward zero); 17/0 → 0, 0 because the function guards it — division by zero is UB.
  • Inout is read as well as written — measured 41 → 42, and 1 when the read is skipped.
  • Keep pointers out of the logic, so the function is testable from a C main.
  • The pointer is valid for the call (21.7).
  • The finding: unwritten return → 0; unwritten output → the caller's previous value.
  • Measured: after a good call left 3, the silent call still reads 3 — the last answer that worked.
  • Harder to spot than 22.1's case, because a plausible repeated answer is what success looks like.
  • Write every output on every path (including early returns) and poison the destination in tests.
  • Call twice with different inputs and assert the results differ.

Three chapters of wiring, and the function on the other end has been an adder and a divider — chosen because nothing in them could be wrong. An import that earns its place does something the testbench could not conveniently do itself, and the first one almost everybody writes is also the one with a published answer to check it against.

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.