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.

CallTrue sumPlain int addition returnedWith a status
100 + 23123123status 0, result 123
2147483647 + 12147483648-2147483648status 1, result 0
-2147483648 + -1-21474836492147483647status 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.

SystemVerilogC, from the generated headerWidth
intint32 bits, signed
longintlong long64 bits, signed
shortintshort16 bits, signed
bytechar8 bits, signed: 8'hc8 arrives as -56
byte unsignedunsigned char8 bits: 8'hc8 arrives as 200
realdouble64-bit floating point
chandlevoid*a pointer C owns (D1.6)
stringconst 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.

Two inputs by value, one output by addressArrows show what crosses: the inputs are copied, the output crosses as the address of the SystemVerilog variable, and the status comes back as the return value.
Two inputs by value, one output by addressArrows show what crosses: the inputs are copied, the output crosses as the address of the SystemVerilog variable, and the status comes back as the return value. SystemVerilog main.sv status =checked_sum(100,23, result); variable int result the variable C writesinto by value a, b copies: 100 and 23 by address &result the address of thevariable, as int* C checked_sum widens, checks therange, writes *result,returns a status copiesaddress ofargumentspointerwrites through thepointerreturn value
  1. Copy the inputs

    The two input int arguments are copied; C can change its copies without touching the testbench.

    Follow the calls, values and waits
    1. data a=100, b=23: main.sv → a, b
  2. Take the address

    For the output int argument the simulator passes the address of result. The header shows it as int* result.

    Follow the calls, values and waits
    1. call &result: int result → &result
  3. Enter C

    C receives 100, 23 and the pointer. It widens the sum to 64 bits and compares it with the int range.

    Follow the calls, values and waits
    1. call a=100, b=23: a, b → checked_sum
    2. call &result: &result → checked_sum
  4. 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
    1. data 123: checked_sum → int result
  5. Return the status

    The return value is the status: 0 here. The testbench reads the status first and trusts result only when it is 0.

    Follow the calls, values and waits
    1. 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.

  1. A sum that fits

    C writes the narrowed value through the pointer and returns 0.

    100 + 23123
    status0
  2. A sum out of range

    The 64-bit sum is 2147483648, above INT_MAX. C leaves result at 0 and returns 1.

    2147483647 + 12147483648 in int64_t
    status / result1 / 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*.

RUNVerilator + C/C++ over DPI · simulator service
Edit the code, then run it
Expected: status=0 result=123 overflow status=1 result=0
Read 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

Predict the outputThe bridge is the lesson's checked_sum. What line does this program print?
`timescale 1ns/1ps
module tb;
  import "DPI-C" function int checked_sum(input int a, b, output int result);
  int result, status;
  initial begin
    status = checked_sum(2147483647, 0, result);
    $display("%0d %0d", status, result);
    $finish;
  end
endmodule
Read 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;
}
Spot the bugThe bridge offers checked_sum and a plain plain_sum that just adds two int values. Each option is a wrapper add_or_zero(a, b) that must return the sum, or 0 when the sum does not fit an int. Which wrapper fails?
Read included file: bridge.cpp
#include "Vtb__Dpi.h"
#include "svdpi.h"
#include <cstdint>
#include <climits>
#include <cstring>
extern "C" int plain_sum(int a, int b) { return a + b; }
extern "C" int checked_sum(int a, int b, int* result) {
  const int64_t wide = static_cast<int64_t>(a) + b;
  *result = 0;
  if (wide > INT_MAX || wide < INT_MIN) return 1;
  *result = static_cast<int>(wide);
  return 0;
}
ConceptA colleague rewrites the bridge with long instead of int, "so the sum has more room". Why is that wrong?
ConceptIn status = checked_sum(a, b, result), where does the sum end up?
ConceptThe wrapper calls a C++ library that throws std::out_of_range on a bad index. What should the wrapper do with the exception?

Remember

  • The C type for each argument is fixed by the standard and spelled out in the generated header: int for int, long long for longint, char for byte. Never substitute long.
  • An output argument 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_t before 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.