Skip to content
VLSI Mentor

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

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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 arm64

Two 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  c++ compile exit: 0
  link exit: 1
  Undefined symbols for architecture arm64:
    "_crc8_step", referenced from:
        _main in user.o

Word 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  from the C build  : 0000000000000000 T _crc8_step
  from the C++ build: 0000000000000000 T __Z9crc8_stephh
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
nm -g lib.o | grep crc8

The 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
Reading a DPI build failure as a statement about symbols. The first row is the baseline: a translation unit that compiles cleanly, exiting 0, tells you nothing about whether anything will link. The second row is a definition that was never supplied, which compiles with exit 0 and fails at the link step with exit 1, reporting undefined symbols and naming both the symbol and the object file that referenced it. The third row is the same source compiled as C plus plus, which produces the identical linker message from a completely different cause. The fourth row is the discriminator, and it is one command: the name tool shows the C build exporting the symbol as underscore crc8 step and the C plus plus build exporting a mangled name beginning underscore underscore Z 9. The fifth row is the fix, an extern C declaration, after which the C plus plus build exports the plain name, the link succeeds and the program prints its answer.The situationa DPI build that will not linkWhat the toolchain saysmeasuredWhat it meansThe compile succeededthe baseline everyone starts fromexit 0measurednothing about linkinga translation unit is checkedaloneThe definition was neversupplieda file left off the command linelink exit 1, undefinedsymbolsmeasured: names the symbol and theobject that referenced ita missing translation unitadd the file, or the libraryThe same source compiled asC++a .cpp extension, or a C++ driverthe identical messagemeasured: same wording, same exit1a different cause entirelythe definition exists underanother nameThe discriminatorone command, no rebuildingThe two spellings_crc8_step against __Z9crc8_stephh— measured: what each object fileactually exportsthe C++ name carries theargument typeswhich is why the linker cannotmatch itThe fix is a declaration, not a rebuild: with extern C the C++ build exports the plain nameand the link succeedsmeasured — the same object file then exports _crc8_step, links with exit 0, and the program prints 9712
Figure 1 — a link failure read as a statement about symbols. The first row is the baseline that misleads people: a clean compile says nothing at all about whether anything will link, because a compiler checks one translation unit at a time. The second and third rows are two quite different causes producing the identical message — a definition nobody supplied, and a definition that exists under a name no C caller will ever ask for. The fourth row is the discriminator and it costs one command: the C object exports the plain name and the C++ object exports a mangled one carrying the argument types. The last row is the fix, which is a declaration rather than a rebuild.

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#ifdef __cplusplus
extern "C" {
#endif
uint8_t crc8_step(uint8_t c, uint8_t x);
#ifdef __cplusplus
}
#endif

Measured, the same source compiled as C++ with that header:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  with extern "C": 0000000000000000 T _crc8_step
  link exit: 0
  97

The 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 nm reads in one command, without rebuilding anything.

6. What You Should Be Able to Say Now

  1. A link error is a statement about symbols: somebody asked for a name and nobody provided it.
  2. 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.
  3. Measured: a missing definition gives compile 0, link 1, "Undefined symbols … _crc8_step, referenced from _main in user.o".
  4. The same source compiled as C++ gives the identical message — measured, word for word.
  5. So split it with nm — one command, no rebuild.
  6. Measured: C exports _crc8_step; C++ exports __Z9crc8_stephh, encoding the argument types.
  7. Absent symbol → a missing translation unit. Present but mangled → compiled as C++.
  8. The fix is a declaration, not a rebuild — extern "C" behind a __cplusplus guard, in the header.
  9. Measured after the fix: _crc8_step exported, link exit 0, the program prints 97.
  10. Four ordinary ways a C file gets compiled as C++, none of them visible in the source.
  11. A clean compile says nothing about linking — measured 0 in both failing cases.
  12. Grep your built library for mangled names as a build step; it costs nothing.

7. Exercises

  1. Leave a source file off a link command. Record both exit codes and the exact message.
  2. Compile the same file as C++ and link it the same way. Is the message distinguishable?
  3. Run nm on both object files. What does each export, and what does the C++ name encode?
  4. Add extern "C" to the header behind a __cplusplus guard. What does nm show now?
  5. Why must the guard be there? What happens if you put extern "C" in a header included from C?
  6. Find out whether your project's build drives .c files through a C or a C++ compiler. How did you check?
  7. Write the one-line build check from §4's callout for your library. What does it report today?
  8. §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.
  • nm splits them in one command: _crc8_step against __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, prints 97.
  • 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.