Types, outputs and a status code
Match every argument to the width C expects, receive a result through an output argument, and let a status code say when that result is invalid.
- Lesson
- D1.2
- Time
- 20 min
- Runs on
- Verilator with C/C++
- Check
- 5 questions
A packet counter kept its running byte total in a small C helper. One run the total reached 2147483647, the next packet pushed it over, and the helper returned -2147483648. The checker compared two numbers and accepted the negative total, because C leaves signed overflow undefined and this build simply wrapped. This lesson gives the hand-off a second channel: a status that says when the answer is invalid.
| Call | True sum | Plain int addition returned | With a status |
|---|---|---|---|
100 + 23 | 123 | 123 | status 0, result 123 |
2147483647 + 1 | 2147483648 | -2147483648 | status 1, result 0 |
-2147483648 + -1 | -2147483649 | 2147483647 | status 1, result 0 |
The same three additions through a plain C int and through checked_sum. Only the status version can say that the second and third answers are invalid.
Widths belong to the type
A SystemVerilog int is 32 bits, on every simulator. The C side has to use a type of the same width, and the generated header tells you which one. Do not swap int for long because it looks roomier. A C long is 32 bits on Windows and 64 bits on Linux, so the same bridge would read different bytes on each. Fixed-width types such as int64_t are the safe choice for anything wider than the argument itself.
| SystemVerilog | C, from the generated header | Width |
|---|---|---|
int | int | 32 bits, signed |
longint | long long | 64 bits, signed |
shortint | short | 16 bits, signed |
byte | char | 8 bits, signed: 8'hc8 arrives as -56 |
byte unsigned | unsigned char | 8 bits: 8'hc8 arrives as 200 |
real | double | 64-bit floating point |
chandle | void* | a pointer C owns (D1.6) |
string | const char* | a borrowed C string (D1.5) |
bit [63:0] | const svBitVecVal* | 32-bit words, lowest first (D1.3) |
The C type is fixed by the standard for each SystemVerilog type. The header Verilator writes spells it out, so you never have to guess.
Signedness crosses with the type. byte is signed, so the byte c8 reaches C as -56; byte unsigned sends 200. Pick the SystemVerilog type that says what the value means, and the C type follows from the header.
An output argument is an address
In status = checked_sum(100, 23, result), the two inputs are copied into C. The output is different: C receives the address of result and writes the sum through it. The integer that comes back as the return value is the status, 0 for a valid sum and 1 for a sum out of range. The status is never the sum.
- Copy the inputs
The two
input intarguments are copied; C can change its copies without touching the testbench.Follow the calls, values and waits
- data a=100, b=23: main.sv → a, b
- Take the address
For the
output intargument the simulator passes the address ofresult. The header shows it asint* result.Follow the calls, values and waits
- call &result: int result → &result
- Enter C
C receives 100, 23 and the pointer. It widens the sum to 64 bits and compares it with the
intrange.Follow the calls, values and waits
- call a=100, b=23: a, b → checked_sum
- call &result: &result → checked_sum
- Write the output
The sum fits, so C stores 123 through the pointer. On a failure it stores 0 instead, so the variable is never left stale.
Follow the calls, values and waits
- data 123: checked_sum → int result
- Return the status
The return value is the status: 0 here. The testbench reads the status first and trusts
resultonly when it is 0.Follow the calls, values and waits
- return status 0: checked_sum → main.sv
Read the diagram as text
- main.sv: status = checked_sum(100, 23, result);
- int result: the variable C writes into
- a, b: copies: 100 and 23
- &result: the address of the variable, as int*
- checked_sum: widens, checks the range, writes *result, returns a status
- main.sv → a, b: copies
- int result → &result: address of
- a, b → checked_sum: arguments
- &result → checked_sum: pointer
- checked_sum → int result: writes through the pointer
- checked_sum → main.sv: return value
An output crosses as a pointer. The return value is free to carry a verdict about the output.
Set the output on every path, failure included. A caller that forgets to check the status then sees 0 rather than whatever the variable held before the call. A stale value looks like an answer; a zero after a status of 1 is at least consistent.
Widen before you add
The range check has to happen in a wider type. Casting after a + b is too late: the 32-bit addition may already have overflowed, and C makes no promise about what it produced. Convert a to int64_t first, add b there, compare against INT_MIN and INT_MAX, and narrow only when the value fits.
Cast after the addition
const int64_t wide = a + b; // the int addition already happened
if (wide > INT_MAX) return 1;
The overflow is in the int addition. wide holds the wrapped value, so the check never fires.
Widen, then add
const int64_t wide = static_cast<int64_t>(a) + b;
if (wide > INT_MAX || wide < INT_MIN) return 1;
The addition happens in 64 bits, where two int values can never overflow. The check sees the true sum.
A sum that fits
C writes the narrowed value through the pointer and returns 0.
100 + 23 123status 0A sum out of range
The 64-bit sum is 2147483648, above
INT_MAX. C leavesresultat 0 and returns 1.2147483647 + 1 2147483648 in int64_tstatus / result 1 / 0
A status is the agreed answer for failure
Give every C routine one documented way to say this call failed. Here it is the return value: 0 means the output is valid and 1 means out of range. In SystemVerilog, check the status before you read the output; a zero in result is a placeholder, never a sum.
A C++ library that throws needs a wrapper. An exception that leaves the DPI routine has nowhere to go: the simulator is C code that never expected one, and the run ends. Catch the documented exceptions inside the wrapper and turn each into a status and a message. The same goes for a C library that reports errors through a global: read it in the wrapper and hand the testbench a status.
Try it
Run it, then add an underflow check with 32'sh80000000 and -1, expecting status 1 and result 0, and a boundary check with 32'sh7fffffff and 0, expecting status 0 and result 2147483647. The two reach different paths in the C routine.
Then change the output declaration to output longint result and run again. The build stops in the bridge: the header now says long long* while the definition says int*.
status=0 result=123 overflow status=1 result=0Read included file: bridge.cpp
#include "Vtb__Dpi.h"
#include "svdpi.h"
#include <cstdint>
#include <climits>
#include <cstring>
extern "C" int checked_sum(int a, int b, int* result) {
// Widen first: the sum of two ints always fits in 64 bits.
const int64_t wide = static_cast<int64_t>(a) + b;
*result = 0; // the output is set on every path, failure included
if (wide > INT_MAX || wide < INT_MIN) return 1;
*result = static_cast<int>(wide); // narrow only after the range check
return 0;
}
Check yourself
Remember
- The C type for each argument is fixed by the standard and spelled out in the generated header:
intforint,long longforlongint,charforbyte. Never substitutelong. - An
outputargument crosses as a pointer to the SystemVerilog variable; C writes the value through it, on every path, failure included. - The return value is a separate channel. Use it as a status and check it before trusting the output.
- Widen to
int64_tbefore adding; a cast after a 32-bit addition checks a value that has already wrapped. - A C++ exception must never leave the DPI routine: catch it in the wrapper and return a status.