Skip to content
VLSI Mentor

C++ Part II · Module 28 · C/C++ and SystemVerilog DPI

Scalar Type Mapping

The scalar mapping is a short table that looks like it needs no explanation, and it is where a large share of real DPI bugs begin — because every row carries an assumption, and getting one wrong produces a number rather than an error. This chapter publishes the table and then measures what each choice commits you to. A SystemVerilog shortint carrying minus one arrives as 65535 if the C side declares the argument unsigned, which is a plausible value that no tool in the flow will question. A thirty-two-bit value received as sixteen bits loses its top half silently. A real and a shortreal holding what looks like the same number are not equal. And plain char — the obvious choice for a byte — has implementation-defined signedness, which this platform resolves one way and others resolve the other, so the apparently safest row in the table is the one that moves when you change machines.

Advanced20 min readC++Part IIVerificationDPI

Part II · Module 28 · Chapter 28.7 · Scalar Type Mapping

1. Why This Is Not Obvious

A SystemVerilog int is 32 bits and signed. A C int is 32 bits and signed on every platform anyone runs a simulator on. So the mapping looks like a formality.

It is not, for three reasons that the rest of the chapter measures.

Some SystemVerilog types have no exact C equivalent. A bit [11:0] is twelve bits; C has 8, 16, 32 and 64. A four-state logic has no C scalar at all.

Some C types have no fixed width. long is 64 bits on this platform and 32 bits on Windows, and char's signedness is implementation-defined.

And a mismatch is not an error. Chapter 28.5 §5 established why: C linkage discards the type information, so the wrong width links and runs. What you get is a number.

2. The Mapping

From IEEE 1800. This is a specification table, quoted rather than measured, because no simulator here can exercise it:

SystemVerilogC typebits
bytechar8, signed
byte unsignedunsigned char8
shortintshort int16, signed
shortint unsignedunsigned short int16
intint32, signed
int unsignedunsigned int32
longintlong long int64, signed
longint unsignedunsigned long long int64
realdouble64
shortrealfloat32
stringconst char*— Chapter 28.10
chandlevoid*— Chapter 28.13 §2
bitsvBit / unsigned char1, two-state
logic, regsvLogic1, four-state — §5

And here is what those C types actually are on this machine, measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A. the C types the DPI mapping names, on this ABI
   SystemVerilog                C type                  bytes  signed?
   byte                         char                  1         yes
   byte unsigned                unsigned char         1         no
   shortint                     short int             2         yes
   int                          int                   4         yes
   longint                      long long             8         yes
   real                         double                8         yes
   shortreal                    float                 4         yes
   NOTE: `long` here is 8 bytes, so it is NOT the type for `int`

Two rows in that transcript deserve attention before anything else.

long is eight bytes here. So writing long where the table says int is a 32-against-64-bit mismatch on this platform — and writing long where the table says long long happens to work here and breaks on Windows, where long is 32 bits. The table says int and long long, and those are the words to write. Never long.

And char is signed here, which the next section is about and which is the subtlest row in the table.

3. The char Problem

byte maps to char, and plain char's signedness is implementation-defined. On this platform it measured as signed; on some ABIs — notably arm on Linux — it is unsigned.

That makes the apparently safest row the one that moves between machines. A model that receives a byte as plain char and then widens it to int gets -1 on one platform and 255 on another, from the same simulator and the same testbench.

The fix is to be explicit and not to rely on the default:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The table says `char` for `byte`. Write what you mean instead:
extern "C" int take_byte(signed char b);            // SV `byte`
extern "C" int take_ubyte(unsigned char b);         // SV `byte unsigned`

signed char and unsigned char are distinct types from plain char and both have fixed signedness everywhere. Using them costs a word and removes a platform dependency, which is the same reasoning Chapter 9.2 gave for std::uint8_t over char in ordinary code — and the DPI boundary is precisely where a platform-dependent default is most expensive, because the two sides may be built by different toolchains.

4. Signedness and Width, Measured

Now the two failures that produce numbers rather than errors.

A negative value received as unsigned. SystemVerilog sends a shortint of −1; the C side declares the argument short in one case and unsigned short in the other:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
B. a negative shortint, received as the right type and the wrong one
   SV shortint -1 -> C short      -> int : -1   (correct)
   SV shortint -1 -> C unsigned short -> int : 65535   (plausible, wrong)

65535 is a perfectly plausible number. It is not garbage, it is not a crash, and it is what two's complement means when sixteen bits of all-ones are widened without sign extension. If the model is computing an address, a length or a count, 65535 may well pass every downstream sanity check — and a scoreboard comparing it against an expected −1 reports a data mismatch pointing at the arithmetic rather than at the declaration.

A wide value received narrow. SystemVerilog sends a 32-bit int; the C side declares short:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
C. a 32-bit value received as 16 bits
   SV int 0x00012345 -> C short : 0x2345  (truncated, no diagnostic)

The top half is simply gone. 0x00012345 becomes 0x2345, and — this is what makes it dangerous — for every input whose top half happens to be zero, the truncation is invisible. A test using small values passes; the bug appears when a value exceeds 65535, which may be months later and will look like a new failure rather than an old one.

The two together give the rule: write the exact type from §2's table, not an approximately right integer. And since nothing checks it, Chapter 28.12 §4's cross-checker exists to compare the two declarations mechanically.

5. Real, shortreal, and Four-State

real is double and shortreal is float, and they are not interchangeable:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
D. real and shortreal
   real      0.1 as double : 0.10000000000000001
   shortreal 0.1 as float  : 0.10000000149011612   (different value)
   equal = no

The same literal, two types, two different values — and a comparison between them is false. So a model declaring float where the testbench sends real loses precision on every value, which shows up as a scoreboard that reports tiny mismatches everywhere and looks like a tolerance problem rather than a type error. Chapter 27.3 §4's warning about floating point in an oracle applies on top of this.

And four-state logic is not a C scalar at all. Chapter 28.2 §3 introduced this; the measurement again, because it belongs to the mapping:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
E. 4-state logic is NOT a C scalar
   32 bits of 4-state needs 8 bytes (two words), not 4
   aval=0x0000000F bval=0x0000000A
   bits where bval is 1 (not 0 or 1) : 2 of 32
   reading aval alone yields 15 and DISCARDS every bval bit

Two words per thirty-two bits, one of values and one recording which bits are not 0 or 1. The IEEE 1800 encoding is aval/bval = 00 → 0, 10 → 1, 01 → Z, 11 → X, which I am quoting from the standard rather than measuring. What is measured is the consequence: two of the thirty-two bits had bval set, and reading the value word alone yields a definite 15 with no indication that two of its bits were unknown.

The rule that follows is the simplest in the module: declare two-state types at the boundary. int, byte, bit. Let SystemVerilog resolve any X before the call, and keep X-propagation checks in SystemVerilog where Chapter 27.1 §6 put them.

6. Bit Vectors Wider Than a Scalar

A SystemVerilog packed vector wider than 64 bits has no C scalar, and the mapping sends it as an array of 32-bit chunks — svBitVecVal for two-state, svLogicVecVal for four-state. Chapter 28.8 §4 measures the chunk assembly and the ordering trap.

Three facts worth knowing here, because they decide the declaration.

Up to 64 bits, use the scalar. A bit [63:0] can cross as longint unsigned → unsigned long long, which is far easier than chunk arithmetic.

Above 64 bits it is chunks, and the chunk count is (width + 31) / 32. A bit [95:0] is three chunks.

And a width that is not a multiple of 32 has undefined bits in the top chunk. For a bit [11:0], one chunk carries twelve meaningful bits and twenty whose value you must not rely on. Masking on the C side — value & 0xFFF — is the defensive habit, because a model that sums or compares the whole chunk includes bits the testbench never set.

7. Output and Inout Scalars

Chapter 28.3 §4 established that output and inout arguments are pointers. Two scalar-specific points.

An output scalar is a pointer the C side must write. void get_status(int* out) — and if a path does not write it, SystemVerilog sees whatever was in that storage. There is no "unwritten" state.

And the pointer's type must be exact too, with the same consequences. short* where the table says int* does not merely truncate on the way out; it writes two bytes where the simulator expects four, leaving the other two as they were. That is a half-updated value, which is less obviously wrong than a truncated one and harder to recognise.

The defensive shape for any output scalar:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
extern "C" void rm_get(void* h, int* out)
{
    *out = 0;                                   // define it on every path
    if (!h) return;
    *out = static_cast<rm::Model*>(h)->value();
}

Writing a defined value first costs one store and converts "whatever was there" into a known 0, which is diagnosable.

8. The Table

the questionthe answermeasured
int in Cint — 4 bytes, signed§2
longint in Clong long, never long§2
is long ever rightno — 8 bytes here, 4 on Windows§2
byte in Cchar per the table — write signed char§3
is plain char signedimplementation-defined; signed here§3
shortint −1 as unsigned short65535 — plausible and wrong§4
int 0x00012345 as short0x2345 — truncated, no diagnostic§4
when is truncation invisiblefor every value whose top half is zero§4
real against shortrealdouble against float — not equal for 0.1§5
four-state in Ctwo words; reading the value word discards the unknowns§5
what should the boundary declaretwo-state types only§5
why are these failures silentevery one returns a representable number§5 callout
where should the test gothe type's boundaries, not the middle§5 callout
above 64 bitschunks of 32; (width + 31) / 32 of them§6
an output scalara pointer the C side must write on every path§7

9. What to Actually Do

  • Write the exact C type from §2's table. Not an approximately right integer.
  • Never write long. It is int or long long, depending on the row.
  • Write signed char or unsigned char, never plain char, because its signedness is implementation-defined.
  • Match signedness exactly. A negative value received as unsigned becomes a large positive one that passes sanity checks.
  • Declare two-state types at the boundary and resolve X in SystemVerilog.
  • Keep real as double; use shortreal/float only when the specification says so.
  • Use a 64-bit scalar for anything up to 64 bits rather than chunk arithmetic.
  • Mask a vector whose width is not a multiple of 32, because the top chunk's spare bits are undefined.
  • Write every output pointer on every path, starting with a defined default.
  • Test at the type's boundaries: most negative, most positive, full width, and a non-representable real.
  • Run a static cross-check of the two declarations — Chapter 28.12 §4.

10. Mistakes Worth Making Once

Declaring long for a SystemVerilog int. A 32-against-64-bit mismatch here.

Declaring long for longint. Works here, breaks on Windows.

Using plain char for byte. Signed on this platform, unsigned on others, from identical source.

Receiving a signed value as unsigned. −1 arrives as 65535 and passes every range check.

Receiving a 32-bit value as short. Invisible until a value exceeds 65535.

Declaring float where the testbench sends real. Tiny mismatches everywhere, looking like a tolerance problem.

Declaring a logic argument. The X becomes a definite number.

Summing a whole chunk for a bit [11:0]. Twenty of those bits are not yours.

Leaving an output pointer unwritten on one path. SystemVerilog reads whatever was there.

Using short* for an output int. Two bytes written, two left stale — a half-updated value.

Testing only with small positive round numbers. They pass every bug in this chapter.

11. Interview Relevance

The frozen curriculum marks this Critical. The question is usually the table itself — what does a SystemVerilog int map to in C? — and the table is worth knowing cold: byte/char, shortint/short, int/int, longint/long long, real/double, shortreal/float.

What makes the answer more than recall is naming the traps inside it. Three are worth having ready.

long is never the right answer, because it is 64 bits on Linux and macOS and 32 on Windows — so int and long long are the words to write.

Plain char has implementation-defined signedness, so a byte received as char and widened gives −1 on one platform and 255 on another from identical source; write signed char or unsigned char.

And a mismatch is never an error. This is the point that matters most: a shortint of −1 received as unsigned short becomes 65535, and an int of 0x00012345 received as short becomes 0x2345 — both measured, both plausible values that pass downstream sanity checks, and neither producing a diagnostic anywhere, because C linkage discarded the type information that would have caught it.

Then the conclusion a senior candidate draws: because every failure in this class returns a representable number, and because truncation is invisible for small values and sign errors invisible for positive ones, a test suite using small positive numbers passes all of them. So you test at the type's boundaries — most negative, most positive, a value needing the full width — and you add a static cross-check of the two declarations. Mentioning that four-state logic needs two words and that reading the value word silently discards every X is the detail that closes the answer.

12. What You Should Be Able to Say Now

  • The mapping cold: byte/char, byte unsigned/unsigned char, shortint/short, int/int, longint/long long, real/double, shortreal/float, string/const char*, chandle/void*.
  • That long is never correct — measured at 8 bytes here, 4 on Windows.
  • That plain char's signedness is implementation-defined (signed on this platform), so write signed char or unsigned char.
  • That a shortint of −1 received as unsigned short becomes 65535, a plausible value with no diagnostic.
  • That an int of 0x00012345 received as short becomes 0x2345, and that the truncation is invisible for every value whose top half is zero.
  • That real and shortreal are double and float, that 0.1 differs between them (0.10000000000000001 against 0.10000000149011612), and that a comparison is false.
  • That four-state logic needs two words per 32 bits, that the IEEE encoding is 00 → 0, 10 → 1, 01 → Z, 11 → X, and that reading the value word alone yields a definite number while discarding every unknown bit.
  • That the boundary should declare two-state types only.
  • That above 64 bits a vector crosses as 32-bit chunks, (width + 31) / 32 of them, with the top chunk's spare bits undefined.
  • That an output scalar is a pointer that must be written on every path, and that a narrow pointer leaves a half-updated value.
  • That every failure in this class returns a representable number, so the tests must be at the type's boundaries and backed by a static cross-check.

13. Exercises

Exercise 1 — the ABI. Print sizeof and signedness for every C type in §2's table on your platform. Note which rows surprised you.

Exercise 2 — long. Print sizeof(long) and decide which rows of the table it is wrong for.

Exercise 3 — the char default. Print whether plain char is signed on your platform, then on another if you have one.

Exercise 4 — the signedness error. Take a signed 16-bit −1 through an unsigned short parameter and into an int. Confirm 65535.

Exercise 5 — the truncation. Pass 0x00012345 through a short parameter. Then pass 0x00001234 and note that nothing appears wrong.

Exercise 6 — real against shortreal. Compare 0.1 as double and as float and print both to 17 digits.

Exercise 7 — the four-state loss. Build the two-word representation, set some unknown bits, and confirm that reading the value word alone loses them. Count them with a popcount.

Exercise 8 — the boundary test. Write the four known-value calls per scalar argument that would catch every failure in this chapter, and assert them at the start of a run.

14. Summary

  • The scalar mapping is exact types, not approximate ones: byte/char, shortint/short, int/int, longint/long long, real/double, shortreal/float.
  • long is never right — measured at 8 bytes here and 4 on Windows — and plain char's signedness is implementation-defined, so write signed char or unsigned char.
  • Measured: a shortint of −1 received as unsigned short becomes 65535, and an int of 0x00012345 received as short becomes 0x2345. Neither produces a diagnostic, because C linkage discarded the type information that would have caught it.
  • And the truncation is invisible for every value whose top half is zero, while the signedness error is invisible for every non-negative value — so a suite of small positive numbers passes both.
  • real and shortreal are not interchangeable: 0.1 is 0.10000000000000001 as a double and 0.10000000149011612 as a float, and the comparison is false.
  • Four-state logic is not a C scalar: 32 bits needs two words, and reading the value word alone returns a definite number while discarding every unknown bit — two of thirty-two in the measurement. Declare two-state types at the boundary.
  • Above 64 bits a vector crosses as 32-bit chunks, and a width that is not a multiple of 32 leaves undefined bits in the top chunk that must be masked.
  • An output scalar is a pointer that must be written on every path, and a pointer of the wrong width leaves a half-updated value rather than an obviously wrong one.
  • Because every failure here returns a representable number, test at the type's boundaries — most negative, most positive, full width, a non-representable real — and add a static cross-check of the two declarations.

Chapter 28.8 takes the same question to arrays, starting with the one that decides everything else: packed or unpacked.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

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.