C Part II · Module 19 · DPI-C Foundations
How a DPI Build Works
A DPI build has five stages and three of them can be checked without a simulator, because they produce artefacts you can inspect. This chapter measures all three — the shared object and the symbols it exports, the identical source compiled as C++ exporting names no simulator will ever find, and an exported function sitting as an undefined symbol until the simulator supplies it. It also gives the four-step triage for a failed DPI link, which starts with one nm command and almost never starts with reading the SystemVerilog.
Advanced21 min readCPart IIVerificationDPI-C
Part II · Module 19 · Chapter 19.6 · How a DPI Build Works
1. Five Stages, Three Checkable
2. The Artefact
A DPI build's C side produces an object the simulator loads — usually a shared object, sometimes a static archive linked into the simulation executable.
Measured:
$ clang -std=c17 -shared -fPIC dpi_c.c -o libdpi.dylib
$ file libdpi.dylib
libdpi.dylib: Mach-O 64-bit dynamically linked shared library arm64
$ nm -gU libdpi.dylib
00000000000002d0 T _c_add
00000000000002f0 T _c_crc8Two exported symbols, which are exactly the names a simulator will look for when it resolves import "DPI-C" function int c_add(...).
Two details worth getting right before §3.
The leading underscore is a Mach-O convention. On this machine symbols carry a _ prefix. On ELF — Linux, most embedded targets — the same function exports as c_add with no underscore. The convention differs; the principle does not.
-fPIC and -shared are the two flags that matter. -shared produces a shared object rather than an executable; -fPIC makes the code position-independent, which a shared object loaded at an arbitrary address requires.
3. The Hazard: Symbol Names
This is where a DPI build fails quietly, and it is fully measurable.
The identical source, compiled two ways:
compiled as C:
_c_add
_c_crc8
compiled as C++:
__Z5c_addii
__Z6c_crc8PKhiC++ encodes the argument types into the symbol name so that overloading can work — __Z5c_addii is c_add taking two ints. The simulator is looking for c_add, which is not present, and the failure is "symbol not found" rather than anything hinting at the cause.
The fix:
compiled as C++ with extern "C":
_c_add
_c_crc8Measured — the plain names are restored.
So the rule for any DPI C that might be compiled by a C++ compiler — which includes any file with a .cpp extension, any project whose build system defaults to C++, and any header shared with C++ code:
#ifdef __cplusplus
extern "C" {
#endif
int c_add(int a, int b);
uint8_t c_crc8(const uint8_t *data, int len);
#ifdef __cplusplus
}
#endifThe guard makes the header usable from both languages, which is Chapter 11.3's include-guard habit extended to a second dimension.
4. The Export Direction
Chapter 19.2 §3 established that an export is an ordinary prototype in the C, and the build consequence is measurable.
extern void sv_report(int severity, int code); /* SystemVerilog supplies this */
void c_model_step(int value) {
if (value > 1000) sv_report(2, value);
}Measured:
$ clang -std=c17 -c model.c -o model.o
$ nm -u model.o
_sv_report
$ clang model.o -o /dev/null
1 undefined-symbol errorsv_report is an undefined symbol in the object, and linking the object alone fails. In the simulation build, the simulator supplies it. In a standalone test build, your stub supplies it — Chapter 19.2 §5's practice.
That is the whole of it, and it is Chapter 11.9's linker material with nothing added: a name one translation unit uses and another provides, where in this case "another" is not a translation unit at all.
5. The Stage That Varies
Stage five has no portable form, and this chapter will not invent one.
Every simulator has its own way of being told where the object is — a flag naming a shared library, a flag naming C source files to compile itself, a configuration file entry — and the spellings have nothing in common. Chapter 19.1 §4 verified that no DPI-capable simulator is available on this machine, so any specific command line written here would be copied from documentation rather than run.
What can be said is the shape, which is consistent across tools:
- the simulator needs to know the object exists — a path, a library name, or source files it compiles for you
- it needs it at elaboration or run time, not at SystemVerilog compile time, because the declaration alone does not require the symbol
- the failure, when it is wrong, is a tool error naming the missing function — loud, not silent
That last point is the useful one and it feeds §6: stage five fails loudly, which is why it belongs last in the build order.
6. Triage for a Failed DPI Link
Four steps, and the first three need no simulator and take seconds.
One — is the symbol there at all?
nm -gU libdpi.dylib | grep c_crc8Nothing → it is static, or the file was not compiled into this object. A mangled __Z... name → it was compiled as C++ (§3).
Two — is it the name the declaration asks for? The import "DPI-C" declaration names the function; the symbol must match exactly, allowing for the platform's underscore convention.
Three — is the object the one the simulator loads? A stale build, a second copy on the path, an object built from older source. Compare timestamps, and check the symbol in the object the simulator was actually pointed at.
Four — only now, the declaration. A type mismatch between the SystemVerilog prototype and the C signature.
That order inverts the instinct, and §7 is why.
7. The Misconception: A DPI Build Failure Means the Declaration Is Wrong
"The DPI call fails, so the import declaration must not match the C function."
It is the natural first suspicion — the declaration is the DPI-specific part, so it looks like the DPI-specific problem — and it is usually wrong.
Most DPI build failures are symbol failures, and §6 lists four: mangled by C++, absent because static, missing because the file was not compiled in, or present in a different object than the one the simulator loaded. None of the four is visible in the SystemVerilog, and all four are diagnosed by one nm command.
A declaration mismatch, when it happens, usually fails differently. It either fails loudly at elaboration with a message naming the function, or it produces wrong data rather than a failed link — a value that is the wrong width or the wrong signedness, which looks like a model bug and is Chapter 19.4 §5's territory.
So the two failure modes have different signatures:
A link failure is almost always a symbol problem — check
nmfirst. A wrong answer is almost always a type or a data problem — check the standalone test first.
Both of those checks happen outside the simulator, which is the thread running through Chapters 19.2 §5, 19.4 §6 and this chapter: the parts of a DPI interface that fail quietly are the parts you can inspect without a simulation.
8. What You Should Be Able to Say Now
- Five stages, three checkable without a simulator — write the C, build the object, check the symbols.
- The artefact is a shared object. Measured:
Mach-O 64-bit dynamically linked shared library arm64, exporting_c_addand_c_crc8.-sharedand-fPICare the flags. - The leading underscore is a Mach-O convention; on ELF the same symbols are
c_addandc_crc8. - The mangling hazard. Measured: identical source gives
_c_addas C and__Z5c_addiias C++;extern "C"restores it. No simulator will find a mangled name. - Guard the header with
#ifdef __cplusplusso it is usable from both languages. staticmakes the function vanish entirely. Measured: absent fromnm -gU. Chapter 11.4's internal linkage, arriving as a DPI failure — one of the few places external linkage is the deliberate choice.- An export is an undefined symbol in your object. Measured:
nm -ushows_sv_report, and linking alone gives one undefined-symbol error. The simulator or your stub supplies it. - Stage five varies by tool and no portable command line exists — but it fails loudly, naming the function, which is why it goes last.
- Triage: is the symbol there · is it the right name · is it the object the simulator loaded · then the declaration.
- Link failure → symbol problem, check
nm. Wrong answer → data problem, check the standalone test. Both outside the simulator.
9. Exercises
- Build a shared object from a two-function C file and list its exported symbols. Then build the same source as C++ and compare. Which names would a simulator find?
- Add the
extern "C"guard from §3 and confirm the names are restored under C++. Check that the header still compiles as C. - Make one of the functions
staticand rerunnm -gU. Describe the difference between this failure and the mangled one, from the simulator's point of view. - Write the export example from §4, compile it alone, and show the undefined symbol. Then supply it two ways — a stub and a second object — and confirm both link.
- For your own simulator, find the flag that points it at a DPI object. Write down the command, and say which of §6's four triage steps it would not help with.
- A colleague reports that a DPI function "is not being called". Write the four commands you would ask them to run, in order.
- §6 puts the declaration last. Construct the case where checking it first would have been faster, and say how common you think that case is.
10. Summary
- Five stages; the first three are checkable with no simulator.
- Measured artefact: a shared object exporting
_c_addand_c_crc8;-shared -fPIC. - The underscore is a Mach-O convention — ELF has none.
- Mangling, measured:
_c_addas C,__Z5c_addiias C++, restored byextern "C". Guard the header with#ifdef __cplusplus. staticremoves the symbol entirely — measured absent fromnm -gU. DPI is one of the few places external linkage is deliberate (11.4).- An export is an undefined symbol — measured
_sv_report, one link error alone. Simulator or stub supplies it. - Stage five is tool-specific and fails loudly, which is why it goes last.
- Triage: symbol present? · right name? · right object? · then the declaration.
- Link failure = symbol; wrong answer = data. Both diagnosed outside the simulator.
The object is built, the symbols are right, and the simulator can find the function. What happens when it calls it is a separate question, because the C is now running inside somebody else's program — one with its own threading, its own idea of when things happen, and a lifetime that is not your program's.
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.
