C Part II · Module 22 · Calling C from SystemVerilog
Returning Computed Values to the Testbench
A result that is not simply the sum of its arguments has to be declared, and the obvious belief is that declaring a width on both sides makes the value that width. Measured against a C function returning 300, three places could have narrowed it: the declared width of the import did nothing at all, the testbench's destination variable did it at 8 bits, and an explicit cast in the C did it too. One of those three is in source you control. Signedness turns out not to travel either — the same all-ones value reads as 255 or as minus 1 depending entirely on where it lands.
Advanced22 min readCPart IIVerificationDPI-C
Part II · Module 22 · Chapter 22.2 · Returning Computed Values to the Testbench
1. When the Result Stops Being Obvious
Chapter 22.1's function returned the sum of two ints, and the return type asked no questions: both sides are 32-bit, both signed, nothing to decide.
A useful import is different. It returns a status code, a field width, an encoded value, a count — things whose natural size is 8 bits or 16, or whose values are known to be non-negative, or which are meant to be read as a bit pattern rather than a number. Each of those invites a declaration, and the declaration invites a belief:
"I will declare it the right width on both sides, and the value will be that width."
This chapter measures what the declaration actually does.
2. The Setup
One C function, returning the same value every time:
/* every measurement below sends this value back */
return 300; /* 0x0000012C — needs 9 bits */300 is chosen because it is just past a byte: it survives 16 bits and 32 bits intact, and an 8-bit narrowing turns it into 44, which is a value you would notice.
Three places could in principle do that narrowing, and the chapter is about finding out which of them does.
- The declared width of the imported function — the type in the import declaration.
- The destination variable the testbench assigns into.
- An explicit cast in the C.
From Chapter 22.1's labelling rule: the import "DPI-C" declaration and its correspondence rules are from the LRM; everything measured below is a C function called from a running simulation through VPI, which is the binding available here.
3. What the Declaration Did
Measured, the same function declared at three widths and assigned into a 32-bit integer:
declared 32 bits -> 300
declared 16 bits -> 300
declared 8 bits -> 300
-> the declared width did NOT truncate itNothing. Declaring the function eight bits wide delivered 300 intact.
That is worth stating carefully, because it is easy to over-read. It is a measurement of this binding on this simulator, and it is not a claim that every DPI implementation ignores a return-type mismatch. From the LRM, the declared return type and the C return type are required to correspond; a mismatch between them is a defect in the interface, not a documented narrowing mechanism, and what any particular tool does with one is exactly the sort of thing Chapter 20.9 was about.
But notice that the lesson does not depend on which way the measurement went:
Whatever the declaration does or does not do with a mismatch, it is not the thing to rely on to produce a value. Here it did nothing; elsewhere it might do something; in neither case is it an operation you wrote.
4. What Actually Narrowed It
Measured, the same returned 300 assigned into three different destinations:
into integer (32-bit) : 300 (0x0000012c)
into reg [15:0] : 300 (0x012c)
into reg [7:0] : 44 (0x2c)There it is. The 8-bit destination produced 44 — the low byte of 300, which is Chapter 15.5's truncation happening on the SystemVerilog side.
And the explicit cast in C:
static int8_t narrow_in_c(int x) { return (int8_t)x; } (int8_t)300 in C : 44The same 44, by a different route. Two of the three candidates narrow, and they produce the same answer — but they are not interchangeable, because of where they are written.
The destination variable is the caller's. It lives in the testbench, it can be changed by someone who has never read your C, and from inside the C function it is completely invisible. The cast is yours: it is in the source you maintain, it is covered by your C-side tests, and a reviewer reading the function can see what the function promises.
5. Signedness Does Not Travel Either
Chapter 20.4 established that the same bits are two different numbers depending on how they are read. On the return path that is not an abstract point — it is where the reading happens.
Measured, a C function returning -1:
-1 into integer : -1 (0xffffffff)
-1 into reg [7:0] : 255 (0xff)
-1 into reg signed [7:0] : -1 (0xff)Look at the last two lines. The bits are identical — 0xff in both. The values are 255 and -1.
Nothing was converted, nothing was lost, and nothing went wrong. The destination's declared signedness decided how to read a byte that is the same byte in both cases, and the C function has no influence over that and no way to see it.
So a function returning a value that could be negative has a real obligation, and it is not a code change — it is a sentence:
/* DPI import: returns a signed difference in the range -128..127.
The SystemVerilog destination MUST be declared signed, or every negative
result reads as a large positive one. The bits are identical either way. */
int8_t sample_delta(int a, int b);That is the same shape as Chapter 16.6's access-type column and Chapter 21.2 §6's array-kind comment: the information exists, nothing enforces it, and a comment is the enforcement.
6. Writing a Return Value You Can Defend
Four rules, each from a measurement above.
Do the narrowing in C, explicitly. If the function's result is a byte, return a byte type and cast to it in the function. Measured, that produced 44 — the same answer the destination would have produced, from source you control.
Declare the correspondence, and do not ask it to do work. From the LRM, the declared return type and the C return type must correspond, so get them right; they are a type agreement, not an operation.
Say which range and which signedness the result has, next to the declaration. §5's comment. A reviewer on the SystemVerilog side has no other way to learn it.
And confirm with a value that cannot be produced by accident — Chapter 22.1 §6's rule, which applies with more force here. A status code of 0 confirms nothing at all; 300 was chosen in this chapter precisely because 300 and 44 are both unmistakable.
7. The Misconception: Declared Eight Bits on Both Sides Means an Eight-Bit Result
"I declared the function
bytein SystemVerilog and returned acharfrom C. The value is eight bits now."
Measured false in the direct form: declared eight bits, the value 300 arrived as 300.
But the measurement is the weaker half of the answer, because the result could have gone the other way on another tool. The stronger half is what the belief gets wrong about declarations in general.
A declaration describes a type; it does not perform an operation. Saying both sides are eight bits wide is a statement that they agree — it is the thing the LRM requires you to get right so that the two sides are talking about the same object. It is not an instruction to compute anything, and there is nowhere in a declaration for an arithmetic step to live.
The value gets narrowed by something that runs. A cast in your C is code. An assignment into an 8-bit variable is code. Measured, both produced 44. The declaration is neither.
And the two pieces of code are owned by different people. The assignment is in the testbench; the cast is in your function. Only one of them is covered by your tests, visible in your review, and present when somebody calls your function from a different testbench tomorrow.
The corrected position:
Make the declarations correspond because the standard requires it, and narrow the value in C because that is the only narrowing you own. If the result is meant to be a byte, return a byte-sized type from a function that casts to it — then every caller gets a byte whatever they assign it into, and the comment above the declaration tells them how to read it.
8. What You Should Be Able to Say Now
- A useful import returns something computed, and the moment it is not an obvious
int, the width and signedness have to be decided rather than inherited. - Three places could narrow a result: the declaration, the destination variable, an explicit cast in C.
- The declaration did nothing — measured, declared 8 and declared 16 both delivered 300. Stated as a measurement of this binding, not as a rule about every tool.
- From the LRM, the declared and C return types must correspond — that is a type agreement, not an operation.
- The destination narrowed it — measured,
reg [7:0]took 44. But it is the caller's variable and invisible from your C. - An explicit cast narrowed it too — measured, 44 — and it is the only one of the three in source you control.
- Signedness is decided on landing — measured,
-1reads as 255 inreg [7:0]and -1 inreg signed [7:0], from identical bits (0xff). - So a function returning negatives owes the caller a sentence, because nothing enforces how the destination is declared.
- Prefer a return value while one is enough — it is a value, not a place, so it has no lifetime question at all.
- Confirm with a value that cannot arise by accident (22.1 §6) —
300and44are both unmistakable;0and1are not.
9. Exercises
- Return 300 from a C function and assign it into 32-bit, 16-bit and 8-bit destinations. Which one changed the value, and to what?
- Declare the same import at two different widths without changing the C. Did the value change? What does your answer let you conclude, and what does it not?
- Add an explicit cast to a signed 8-bit type in the C. Compare the result with the 8-bit destination's. Why are two identical answers still not interchangeable?
- Return
-1and read it into a signed and an unsigned 8-bit variable. Print both the value and the bits. Explain why nothing has gone wrong. - Write the comment that should sit above a function returning a signed difference. What would a caller do wrong without it?
- §7 says a declaration performs no operation. Name two other places in C where a declaration is sometimes mistaken for an operation.
- For an import you have seen that returns a status code, list every value it can return. Could a test that only checks the success value distinguish it from a disconnected function?
- Design an import that must report both a result and an error flag. Write the version that uses only a return value, and say what it costs.
10. Summary
- Three candidates for narrowing a result: the declaration · the destination variable · a cast in your C.
- The declaration did nothing — measured, declared 8 and declared 16 both delivered 300. From the LRM the types must correspond; correspondence is an agreement, not an operation.
- The destination narrowed — measured,
reg [7:0]gave 44 — and it belongs to the caller. - The cast narrowed — measured, 44 — and it is the only one in your source.
- Signedness is decided on landing — measured,
0xffreads 255 unsigned and -1 signed. - A function returning negatives owes the caller a comment; nothing enforces the destination's declaration.
- The narrowing you can rely on is the one you wrote.
- Prefer a return value while one is enough — a value, not a place, so no lifetime question.
- Confirm with an unmistakable value —
300and44, never0or1.
One result per call is enough until it is not. A function that has to report two things, or hand back more data than fits in a word, stops returning a value and starts being given somewhere to write — at which point the question changes from what type the result is to whose storage it lands in.
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.
