C Part II · Module 20 · DPI-C Type Mapping
Structures Across the Boundary
Every other chapter in this module has a specified answer. This one does not, and saying so is the chapter's job. A packed SystemVerilog struct is a bit vector and crosses through the canonical representation; an unpacked one has no layout the LRM fixes, so whether it crosses correctly is a property of your simulator and your compiler agreeing. Measured here: the same three members laid out three ways gave sizeof 12, 8 and 6, with the first half padding — and two structs whose members are all equal compared unequal byte for byte. The recommendation that follows is to stop crossing structs and serialise instead.
Expert24 min readCPart IIVerificationDPI-C
Part II · Module 20 · Chapter 20.9 · Structures Across the Boundary
1. The One Question This Module Cannot Answer for You
Chapters 20.3 to 20.8 each had a specified answer. int is 32 bits. A packed vector is 32-bit chunks. A string is a const char *. You could disagree with the reasoning, but not with the mapping.
A struct is different, and the chapter title's question — can a struct cross at all? — has a genuine answer of "sometimes, and the condition is not written in any standard."
That is not a hedge. It is the finding, and this chapter's purpose names it: "recognize where the answer depends on the tool and its ABI rather than on the standard." The useful thing to take away is not a recipe but the ability to tell which situation you are in.
2. Why a C Struct Has No Single Layout
Chapter 10.5 established that a C struct's members are laid out with padding so each one lands on a suitable boundary, and that the compiler chooses. Measured here with the same three members — a one-byte flags, a four-byte addr, a one-byte kind — declared three ways:
as declared sizeof 12 offsets 0/4/8 alignof 4
reordered sizeof 8 offsets 0/4/5 alignof 4
packed sizeof 6 offsets 0/1/5 alignof 1
payload actually carried: 6 bytes in every case
padding: as declared 6 bytes, reordered 2 bytes, packed 0 bytesThree objects, identical members, three different sizes. The first is half padding — six bytes of data in twelve bytes of storage — and nothing changed but the order the members were written in.
Two further facts make the layout unrepeatable across tools rather than merely surprising.
Padding bytes are not part of the value. Measured, two structs whose every member is equal:
all three members equal: yes
memcmp over 12 bytes says equal: NO <- the padding differs
byte 1 of each: 0x00 vs 0xAA (padding after member flags)The structs are equal as values and unequal as bytes. So memcmp is not a struct comparison — it compares six bytes you care about and six you have no control over. From the C standard, the padding bytes of a struct take unspecified values; nothing says what is in them, and copying a struct does not promise to copy them.
Bit-field order is implementation-defined. Chapter 10.10 established this; measured here:
sizeof(struct BF) = 4
lo=0x1 hi=0xF -> first byte 0xF1
so THIS compiler put the first-declared field in the LOW nibble: yes0xF1 — the first-declared field landed in the low nibble. From the C standard, the order of allocation of bit-fields within a unit is implementation-defined, so an equally conforming compiler could produce 0x1F. A struct of bit-fields is therefore not a portable description of a bit layout, which is exactly why Chapter 15.2 taught masks and shifts instead.
3. The Split: Packed and Unpacked
From the LRM, two cases that behave completely differently.
A packed SystemVerilog struct is a bit vector. Packing is defined to lay the members out contiguously with no gaps, so the struct is a vector of bits with named slices. It therefore crosses using Chapter 20.6's canonical packed representation — svBitVecVal or svLogicVecVal, 32-bit chunks, the layout that is specified and that 20.6 round-tripped exactly.
This is the case where the answer is good news: a packed struct crosses on specified terms, and your C side reads it with §20.6's chunk arithmetic and masks rather than by declaring a matching C struct.
An unpacked SystemVerilog struct is not. It is a collection of members whose storage the simulator arranges as it sees fit, and the LRM does not fix a C counterpart for it. What a tool does instead is define its mapping in terms of the C ABI it was built against — which means the layout you get is a property of your simulator and your compiler as a pair, settled by their agreement rather than by a document you can read and rely on.
That is the answer to the chapter's question, and it is worth stating in the strongest honest form: an unpacked struct crossing correctly is a property of a toolchain, not of your code. The same C file, the same SystemVerilog, a different simulator — or the same simulator built against a different ABI — and the agreement is no longer guaranteed by anything you wrote.
4. What That Actually Means in Practice
Not that it never works. It usually works, which is the problem.
A C struct and a simulator's unpacked struct will, on a mainstream platform, usually agree, because both follow the same platform ABI. So the first thing an engineer does — declare a matching C struct, run the test, see the right numbers — succeeds, and the success is taken as confirmation that the approach is sound.
What the success actually confirms is that two independent layout decisions happened to coincide on this machine, with this compiler, at these member types. Any of the three can change:
- a different simulator, or the same one on a different host
- a different compiler, or the same one with different flags
- a member added, removed or reordered — measured in §2 to change the size by 50% with no other edit
And when it does change, the failure mode is the worst available: the members are read at the wrong offsets, so the values are plausible garbage. Nothing warns, nothing faults, and the model and the design disagree in a way that looks like a design bug.
This is the shape Chapter 20.2 measured for the hand-written header typedef, Chapter 20.4 measured for signedness and Chapter 20.5 measured for X: the broken version compiles, runs, and produces numbers.
5. The Decision
So, the procedure the purpose asks for. Three questions.
Is the SystemVerilog struct packed? If yes, you are not in tool-dependent territory. Treat it as a packed vector and use Chapter 20.6 — specified layout, chunk arithmetic, masks. Do not declare a matching C struct; a packed struct and a C struct are different things that happen to have the same members.
If it is unpacked, can you avoid crossing it? Almost always yes, and §6 is how.
If you must cross an unpacked struct, is the dependency written down? Then it is a deliberate engineering decision rather than an accident — which is the difference this module keeps drawing. Record which simulator, which compiler, which flags; add a start-up check that compares sizeof and the offsets against what the C side expects; and treat a toolchain change as something that re-opens the question.
A _Static_assert is the cheapest version of that check:
_Static_assert(sizeof(struct hdr) == 8, "layout changed -- re-check the DPI contract");
_Static_assert(offsetof(struct hdr, addr) == 0, "member moved");It will not tell you the simulator agrees. It will tell you your side changed, which is the half you control and the half that moves when somebody adds a member.
6. The Recommendation: Do Not Cross a Struct
The option with no tool dependency at all is to stop sending a layout and send a value sequence instead. Two forms.
Pass the fields as separate arguments. Three int arguments cross on terms Chapter 20.3 specified exactly. It is more typing and nothing about it is tool-dependent.
Or serialise to a byte layout you define. Measured:
hand-serialised to 6 bytes: 01 02 00 00 00 03
round trip of all three members: exact (no padding crossed anything)Six bytes — the payload, with nothing else in it. The code is Chapter 14.5's:
uint8_t wire[6];
wire[0] = s.flags;
wire[1] = (uint8_t)( s.addr & 0xFFu); /* little-endian, chosen */
wire[2] = (uint8_t)((s.addr >> 8) & 0xFFu);
wire[3] = (uint8_t)((s.addr >> 16) & 0xFFu);
wire[4] = (uint8_t)((s.addr >> 24) & 0xFFu);
wire[5] = s.kind;Every byte's position is in the source. The byte order was chosen rather than inherited — Chapter 14.3's rule. There is no padding to disagree about, no alignment requirement, and no dependence on what two tools happen to do.
The cost is explicit code, and it is the same trade Chapter 14.5 made for hardware data formats: a layout the standard does not define is not a layout, it is a coincidence you are relying on. A DPI boundary is one more place where that is true.
7. What About packed, Then?
__attribute__((packed)) and #pragma pack remove the padding — measured, sizeof 6 with offsets 0/1/5 — and the first instinct is to apply them on the C side and call the layouts matched.
Four reasons that does not settle anything, three of them measured here.
It is not ISO C, and the compiler will not tell you. Measured:
__attribute__((packed)) at -Wall -Wextra -Wpedantic: 0 warnings-Wpedantic is the flag whose job is to report constructs outside the standard, and it is silent about this one. So the usual signal that you have left portable C — a diagnostic — does not arrive. The construct is a compiler extension, in the sense Chapter 13.3 defined: available, useful, and outside what any other implementation is obliged to provide.
It reintroduces unaligned access. Measured, alignof drops from 4 to 1 and addr now starts at byte 1. Chapter 14.4 was about exactly this: on a platform where it is permitted the compiler emits different code for it, and on one where it is not, the access faults. Packing does not make the alignment problem go away; it moves it from the layout to every access.
Bit-field order is still implementation-defined. Packing controls padding, not the allocation order within a unit — measured, 0xF1, and nothing in the standard requires that end.
And SystemVerilog has no such attribute to apply. The C side has been changed to match a layout the other side was never told about. If the two agree afterwards it is because the simulator's own unpacked layout happened to be contiguous too, which is a second coincidence stacked on the first.
Packing is a reasonable tool for describing a format you already have — a packet header, a register image — which is the use Chapter 14.5 made of it. It is not a way to make two independent layout decisions agree.
8. The Misconception: Packed on Both Sides Makes Them Match
"Add
packedto the C struct and declare the SystemVerilog one packed too. Now both are contiguous with no padding, so the layouts match and the struct can cross safely."
The premise is half true and the conclusion still does not hold.
A packed SystemVerilog struct does not want a C struct at all. §3: it is a bit vector, and it crosses through Chapter 20.6's canonical chunk representation. Declaring a matching packed C struct and pointing at it is a different thing from reading the chunks, and the two agree only if the chunking, the bit order within chunks, and the member boundaries all line up — three assumptions where 20.6 gave you a specified answer for free.
On the C side, packed is an extension that -Wpedantic does not flag (measured), so the step that takes you outside the standard is the one that produces no evidence of having done so.
It costs alignment (measured, alignof 1) — 14.4's problem, now on every member access rather than once in the layout.
And bit-fields are unaffected (measured, 0xF1), so any struct using them is still implementation-defined in the one respect that was supposed to be fixed.
The corrected position:
Packing changes one of the several things that have to agree, does not change the others, and leaves ISO C without saying so. For a packed SystemVerilog struct the specified route already exists — Chapter 20.6. For an unpacked one, the way to stop depending on an agreement is to stop needing it.
9. What You Should Be Able to Say Now
- This is the one mapping in the module that is not specified end to end, and recognising that is the deliverable.
- Measured: the same three members gave
sizeof12 / 8 / 6, offsets0/4/8,0/4/5,0/1/5— the payload is 6 bytes in every case, so the first is half padding. - Declaration order alone changed the object's size by 50%.
- Padding is not part of the value — measured, all members equal and
memcmpunequal. Never compare structs withmemcmp. - Bit-field order is implementation-defined — measured
0xF1, first-declared field in the low nibble, and the standard does not require that end (10.10). - A packed SystemVerilog struct is a bit vector and crosses through Chapter 20.6's specified representation — do not declare a matching C struct for it.
- An unpacked struct has no LRM-fixed C layout; the mapping follows the tool's ABI, so correctness is a property of your simulator and compiler as a pair.
- It usually works, and that is the hazard — the failure, when it comes, is members read at wrong offsets producing plausible garbage.
- Prefer not to cross a struct: pass the fields, or serialise — measured, 6 bytes, exact round trip, no padding crossed anything.
packedis a compiler extension that-Wpedanticdoes not flag (measured, 0 warnings), dropsalignofto 1 (14.4), and does not fix bit-field order.
10. Exercises
- Declare the three-member struct three ways and print
sizeof, every offset and_Alignof. Reproduce 12 / 8 / 6. - Which single edit to the first declaration saves the most bytes, and why does it not change the payload?
- Fill two structs so every member is equal but the padding differs, and show
memcmpreporting them unequal. What does that tell you about usingmemcmpas a scoreboard comparison? - Lay out two 4-bit bit-fields and print the first byte. Which end did your compiler use? What would the other answer look like?
- Serialise the struct to six bytes and back. Confirm the round trip and state which byte order you chose and where that choice appears in the source.
- Compile a packed struct at
-Wall -Wextra -Wpedantic. How many warnings? What does that tell you about using diagnostics to detect non-standard constructs? - Write the
_Static_assertpair from §5 for a struct in your own code. Add a member and watch which assertion fires — and note which side of the boundary it did not check. - §3 says an unpacked struct's crossing is a property of a toolchain. Write the paragraph you would put in a project's README recording that dependency.
11. Summary
- The one mapping in this module that is not specified end to end — and saying so is the point.
- Measured: same members,
sizeof12 / 8 / 6; payload 6 bytes in all three; the first is half padding; declaration order alone moved it by 50%. - Padding is not part of the value — members equal,
memcmpunequal, measured. - Bit-field order is implementation-defined — measured
0xF1(10.10). - Packed SystemVerilog struct = bit vector → Chapter 20.6's specified representation, not a matching C struct.
- Unpacked struct → the tool's ABI; correctness is a property of a simulator and a compiler as a pair.
- It usually works, and the failure is plausible garbage at wrong offsets.
- Don't cross a struct: pass the fields, or serialise — measured 6 bytes, exact round trip.
packed: a compiler extension, 0 warnings at-Wpedantic,alignof→ 1 (14.4), bit-fields still implementation-defined.- If you must, write the dependency down and
_Static_assertyour own side.
One case is left, and it is the inverse of this one. Everything in the module so far has been about describing a layout precisely enough for both sides to agree on it. The last type is the one where you deliberately refuse to describe the layout at all — and hand the other side something it is not allowed to look inside.
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.
