C Part II · Module 25 · Debugging DPI-C and Simulator Integration
Compile and Link Errors in a DPI Build
A link error is a statement about symbols, and it names both the symbol the linker wanted and the object file that wanted it. Measured here, two completely different causes produce that statement word for word: a definition nobody supplied, and the same definition compiled as C++. One command separates them without rebuilding anything — the C object exports _crc8_step and the C++ object exports __Z9crc8_stephh, a name carrying the argument types, which is exactly why no C caller can match it.
Advanced21 min readCPart IIVerificationDPI-C
Part II · Module 25 · Chapter 25.6 · Compile and Link Errors in a DPI Build
1. A Link Error Is About Symbols
A build failure in a DPI library is almost always at the link step, and reading it correctly starts with knowing what the two steps do.
The compiler checks one translation unit against the declarations it can see. It does not know, and does not care, whether a definition exists anywhere.
The linker collects the object files it was given and resolves every symbol one of them references. A failure here says: somebody asked for this name and nobody provided it.
Measured, a definition that was never supplied:
compile exit: 0
link exit: 1
Undefined symbols for architecture arm64:
"_crc8_step", referenced from:
_main in user.o
ld: symbol(s) not found for architecture arm64Two useful facts in that message, and the second is routinely ignored:
- the symbol the linker wanted —
_crc8_step - the object file that wanted it —
_main in user.o
The second narrows which translation unit has the unsatisfied reference, which matters when a build has dozens.
2. The Same Message, a Different Cause
Now compile the same source as C++ and link it against the same caller. Measured:
c++ compile exit: 0
link exit: 1
Undefined symbols for architecture arm64:
"_crc8_step", referenced from:
_main in user.oWord for word identical. Same symbol, same referencing object, same exit code — and the cause is completely different. In the first case the definition did not exist. In this one it exists, it is in the object file that was linked, and it is exported — under a different name.
This is Chapter 25.2 §3's pattern again: one message, several causes, and the next step is a cheaper measurement rather than a guess.
3. The Discriminator
One command, no rebuilding. Measured, the two object files side by side:
from the C build : 0000000000000000 T _crc8_step
from the C++ build: 0000000000000000 T __Z9crc8_stephhnm -g lib.o | grep crc8The C build exports _crc8_step. The C++ build exports __Z9crc8_stephh — the same function, under a mangled name that encodes the argument types (hh for two unsigned char parameters). C++ needs that because it allows overloading: two functions may share a source name and must not share a symbol.
A C caller asks the linker for _crc8_step. That name is not in the file, so the link fails — while a human reading the source sees the function sitting right there.
So:
- the symbol is absent entirely → a missing translation unit; add the file or the library
- the symbol is present but mangled → it was compiled as C++; §4
4. The Fix Is a Declaration
The mangled-name case does not need the file renamed or the build system changed. It needs the declaration to say that this function uses C linkage:
#ifdef __cplusplus
extern "C" {
#endif
uint8_t crc8_step(uint8_t c, uint8_t x);
#ifdef __cplusplus
}
#endifMeasured, the same source compiled as C++ with that header:
with extern "C": 0000000000000000 T _crc8_step
link exit: 0
97The plain name is back, the link succeeds, and the program runs.
Three things worth knowing about this pattern.
The guard matters. extern "C" is not valid C, so it has to be hidden behind __cplusplus — which is why the idiom looks the way it does and why it belongs in the header, where both languages read it.
It is about linkage, not about the language of the definition. The function can be compiled by a C++ compiler and still export a C symbol; what changes is the name it is given.
And it is the right default for a DPI library. The simulator will look up these names from C, so every function that crosses the boundary should be declared this way whether or not anyone currently builds with C++ — because §5 is how a project ends up building with C++ without deciding to.
5. The Misconception: It Compiled, So the Function Is There
"The file compiled without a single warning, so the function exists. The problem must be in the simulator's build."
Measured: the compile exited 0 in both failing cases — the one where the definition was never supplied, and the one where it existed under another name.
A compiler checks one translation unit against the declarations visible in it. Given a declaration, a call to an undefined function is perfectly well-formed C: the definition is expected to be somewhere else, and the compiler's job ends there.
So a clean compile establishes that your source is valid C and nothing about whether the program can be assembled — which is the same shape as Chapter 25.2's finding that "it builds" says almost nothing about an import.
The order that follows, and it is three commands:
- Did the compile succeed? If not, it is a language problem and the message says so.
- Did the link succeed? If not, read the symbol and the referencing object file.
- Is the symbol in the library at all?
nm. Absent means a missing file; present-but-mangled means §4.
The accurate position:
A compiler checks source; a linker checks a program. A DPI library's job is to export names a C caller can ask for, and the only artefact that can confirm it did is the built library — which
nmreads in one command, without rebuilding anything.
6. What You Should Be Able to Say Now
- A link error is a statement about symbols: somebody asked for a name and nobody provided it.
- The message names two things — the symbol and the object file that referenced it; the second is usually ignored and narrows which translation unit is at fault.
- Measured: a missing definition gives compile 0, link 1, "Undefined symbols …
_crc8_step, referenced from_main in user.o". - The same source compiled as C++ gives the identical message — measured, word for word.
- So split it with
nm— one command, no rebuild. - Measured: C exports
_crc8_step; C++ exports__Z9crc8_stephh, encoding the argument types. - Absent symbol → a missing translation unit. Present but mangled → compiled as C++.
- The fix is a declaration, not a rebuild —
extern "C"behind a__cplusplusguard, in the header. - Measured after the fix:
_crc8_stepexported, link exit 0, the program prints97. - Four ordinary ways a C file gets compiled as C++, none of them visible in the source.
- A clean compile says nothing about linking — measured 0 in both failing cases.
- Grep your built library for mangled names as a build step; it costs nothing.
7. Exercises
- Leave a source file off a link command. Record both exit codes and the exact message.
- Compile the same file as C++ and link it the same way. Is the message distinguishable?
- Run
nmon both object files. What does each export, and what does the C++ name encode? - Add
extern "C"to the header behind a__cplusplusguard. What doesnmshow now? - Why must the guard be there? What happens if you put
extern "C"in a header included from C? - Find out whether your project's build drives
.cfiles through a C or a C++ compiler. How did you check? - Write the one-line build check from §4's callout for your library. What does it report today?
- §5 says a clean compile says nothing about linking. Give the one-sentence reason, in terms of translation units.
8. Summary
- A link error is about symbols, and names both the symbol and the referencing object file.
- Measured: missing definition → compile 0, link 1, "Undefined symbols …
_crc8_step…_main in user.o". - The same source as C++ gives the identical message from a different cause.
nmsplits them in one command:_crc8_stepagainst__Z9crc8_stephh.- Absent → missing translation unit. Mangled → compiled as C++.
- The fix is
extern "C"in the header, behind__cplusplus— measured: plain name, link 0, prints97. - Four ordinary routes to a C file being compiled as C++, none visible in the source.
- A clean compile says nothing about linking — measured 0 in both failing cases.
- Grep the built library for mangled names in CI.
A library that links is not yet a library the simulator can use. The next set of failures happen after the build is finished and before any simulated time exists — when something goes looking for a file, and for a name inside it.
Where this fits
Part of the C Programming curriculum.
