Skip to content
VLSI Mentor

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:

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  declared 32 bits -> 300
  declared 16 bits -> 300
  declared  8 bits -> 300
  -> the declared width did NOT truncate it

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

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
static int8_t narrow_in_c(int x) { return (int8_t)x; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  (int8_t)300 in C : 44

The 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.

A C function returns the value 300 in every measurement here, and three different places could in principle narrow it. The declared width of the imported function did nothing at all: declaring it 8 bits wide and then 16 bits wide both delivered 300 intact, so the declaration is not where narrowing happens and must not be relied on to do it. The destination variable in the testbench did narrow: assigning the same 300 into a 32-bit integer kept 300, into a 16-bit register kept 300, and into an 8-bit register produced 44. An explicit cast written in the C function also produced 44, and that is the one of the three that lives in source you control. Signedness is decided on landing rather than in transit: the value minus 1 arrives as all ones, and reading those bits into an unsigned 8-bit register gives 255 while reading them into a signed 8-bit register gives minus 1.Where the value couldnarrowthe C function returns 300 inevery rowWhat it actually didmeasuredCan you rely on it?the column that decides your codeThe declared width of thefunctionthe number in the importdeclarationnothing at alldeclared 8 and declared 16 bothdelivered 300noand this is the one people assumeis doing the workThe destination variablewhat the testbench assigns intoit narrowed32-bit kept 300, 16-bit kept 300,8-bit gave 44it will, but it is thecaller's choicenot yours, and not visible fromthe CAn explicit cast in the Cwritten where you can read itit narrowedthe cast to a signed 8-bit typegave 44yesthe only one of the three that isin your sourceAnd signedness is decidedon landingthe same bits, two readingsminus 1 arrives as all onesunsigned 8-bit reads 255, signed8-bit reads minus 1the destination's declaredsignedness decidesnothing about the value itselfsays whichThe narrowing you can rely on is the one you wrotemeasured — of the three candidates, one did nothing, one belongs to the caller, and one is in your source12
Figure 1 — three places a returned value could narrow, measured against a C function that returns 300 every time. The first row is the surprise and the reason the chapter exists: declaring the imported function eight bits wide, and then sixteen, delivered 300 intact both times, so the declaration is not performing an operation on the value. The second row is where the narrowing really happened — an 8-bit destination variable in the testbench took the low byte and produced 44 — but that variable belongs to the caller and is invisible from the C. The third row is the same result reached by a cast written in the C function, which is the only one of the three in source you control. The fourth row adds signedness, which is not decided in transit at all: minus 1 arrives as all ones, and whether that reads as 255 or as minus 1 depends entirely on how the destination is declared.

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:

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
/* 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 byte in SystemVerilog and returned a char from 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

  1. 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.
  2. Three places could narrow a result: the declaration, the destination variable, an explicit cast in C.
  3. 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.
  4. From the LRM, the declared and C return types must correspond — that is a type agreement, not an operation.
  5. The destination narrowed it — measured, reg [7:0] took 44. But it is the caller's variable and invisible from your C.
  6. An explicit cast narrowed it too — measured, 44 — and it is the only one of the three in source you control.
  7. Signedness is decided on landing — measured, -1 reads as 255 in reg [7:0] and -1 in reg signed [7:0], from identical bits (0xff).
  8. So a function returning negatives owes the caller a sentence, because nothing enforces how the destination is declared.
  9. Prefer a return value while one is enough — it is a value, not a place, so it has no lifetime question at all.
  10. Confirm with a value that cannot arise by accident (22.1 §6) — 300 and 44 are both unmistakable; 0 and 1 are not.

9. Exercises

  1. 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?
  2. 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?
  3. 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?
  4. Return -1 and read it into a signed and an unsigned 8-bit variable. Print both the value and the bits. Explain why nothing has gone wrong.
  5. Write the comment that should sit above a function returning a signed difference. What would a caller do wrong without it?
  6. §7 says a declaration performs no operation. Name two other places in C where a declaration is sometimes mistaken for an operation.
  7. 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?
  8. 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, 0xff reads 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 — 300 and 44, never 0 or 1.

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.