Skip to content
VLSI Mentor

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

C Linkage vs C++ Linkage Across the Boundary

This is the one chapter in the module that needs no simulator at all, because its entire subject is what a compiler writes into an object file. Module 24 established the mechanism: C++ encodes a function's parameter types into its symbol name so that overloading can work, and C does not. At a DPI boundary that becomes the difference between a binding that resolves and one that does not, and the failure has a distinctive shape worth recognising on sight — a linker error naming the plain C name you expected to see, from an object file that contains a long encoded one instead. The chapter measures both sides of it on one file with two identical function bodies, shows the exact error text, and then goes after the less obvious half: what C linkage costs you. It discards the type information C++ would have carried, which is precisely why nothing in the flow can check that your two declarations agree — the subject of Chapter 28.12, and the reason this module keeps insisting you supply that evidence yourself.

Expert20 min readC++Part IIVerificationDPI

Part II · Module 28 · Chapter 28.5 · C Linkage vs C++ Linkage Across the Boundary

1. What Linkage Decides

When a compiler finishes with a function it writes a name into the object file, and the linker matches that name against what other objects asked for. Linkage is the rule that decides what the name is.

C linkage uses the identifier. A function called model_step becomes the symbol model_step — with a platform-specific decoration this chapter measures — and nothing else about it is encoded.

C++ linkage encodes the parameter types into the name. It has to: step(int) and step(double) are different functions with the same identifier, so the symbol must distinguish them. Chapter 24.5 measured the scheme in detail.

At a DPI boundary the consequence is immediate. The simulator looks for a C name, because the import "DPI-C" declaration gives it an identifier and nothing else — Chapter 28.2 §2's observation that only the name is machine-checked. If your C++ was compiled with C++ linkage, the name it emitted is not the one being looked for.

2. The Two Symbols

One file, two functions, identical bodies — one with extern "C" and one without:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WITHOUT extern "C": the symbol is mangled.
int model_step_cpp(const unsigned char* in, unsigned len) { /* ... */ }

// WITH extern "C": the symbol is exactly what a DPI import would look for.
extern "C" int model_step(const unsigned char* in, unsigned len) { /* ... */ }

What the object file contains:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  == 28.5 the two linkages, as the linker sees them ==
0000000000000000 T __Z14model_step_cppPKhj
0000000000000210 T _model_step
  demangled:
model_step_cpp(unsigned char const*, unsigned int)
_model_step
declared assymbol emitteddemangles to
plain C++__Z14model_step_cppPKhjmodel_step_cpp(unsigned char const*, unsigned int)
extern "C"_model_stepunchanged — there is nothing to demangle

Three things to take from that.

The mangled name carries the signature. PKhj encodes "pointer to const unsigned char, unsigned int", and 14 is the length of the identifier. Running it through c++filt recovers the full declaration, which is why Chapter 24.5 §3 makes demangling the first step in reading a link error.

The extern "C" name carries nothing but the identifier, and c++filt leaves it alone because there is no encoding to undo.

And the leading underscore is a platform convention, not part of the name. On Mach-O — macOS — the C identifier model_step becomes the symbol _model_step. On ELF, which is what Linux uses, there is no underscore and the symbol is model_step. So do not read _model_step as an identifier with an underscore in your source; the source says model_step.

3. The Failure, Exactly as It Appears

Now the version a real flow hits. The C++ is compiled without extern "C", and a pure C caller — which is what the simulator's generated marshalling amounts to — asks for the C name:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
extern int model_step(const unsigned char* in, unsigned len);   /* C linkage */
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
0000000000000000 T __Z10model_stepPKhj
  link exit=1
Undefined symbols for architecture arm64:
  "_model_step", referenced from:
      _main in caller.o
ld: symbol(s) not found for architecture arm64
clang++: error: linker command failed with exit code 1 (use -v to see invocation)

Read that error carefully, because its shape is the diagnosis. It names _model_step — the name that was wanted — and says where it was referenced from. It does not mention __Z10model_stepPKhj, the name that actually exists. So the message tells you what is missing and says nothing about the near-miss sitting in the same object file, which is why this error is so often misread as "the object was not linked in".

The signature of this particular failure:

The name in the error is the plain C identifier you expected. The object file does contain a function with that identifier — nm shows a mangled symbol whose demangled form has the right name. And the link exits 1 with nothing built.

If all three hold, the cause is linkage and the fix is one keyword. With it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  with extern "C": link exit=0
  model_step returned 100

Exit 0 and the hand-checked answer. Same body, same caller, one keyword.

4. How to Diagnose It in Ten Seconds

A fixed procedure, because this failure is common enough to deserve one.

Ask the object file what it exports:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
nm -gU model.o | c++filt

Three outcomes and three conclusions. The name appears plainly — linkage is right, so the problem is elsewhere (the object was not given to the simulator, or the path is wrong). The name appears only in a demangled form with parameter types after it — the function is there and has C++ linkage, so add extern "C". The name does not appear at all — you are looking at the wrong object file, or the function was never compiled, or it is static.

That third case is worth its own note: a static function has internal linkage and is not visible outside its translation unit at all, regardless of extern "C". It produces exactly the same undefined-symbol error, and nm -gU will not list it. A DPI entry point must not be static.

5. What C Linkage Costs

The chapter so far has treated C linkage as the thing you want. It is, and it is worth being precise about what you give up, because that cost is the origin of most of this module.

You lose overloading. Two extern "C" functions cannot share a name — there is nothing left to distinguish them. So a DPI boundary cannot offer model_step(int) and model_step(double); it offers model_step_i and model_step_d, named by hand.

You lose type checking across the boundary. This is the significant one. C++ mangling means a caller expecting step(int) cannot accidentally resolve to step(double) — the symbols differ, so the mistake is a link error. With C linkage the symbol is just the name, so a disagreement about types resolves cleanly and silently.

Chapter 28.12 §2 measures that directly: a caller declaring int scale(int, int) against a definition of int scale(double, int) links with exit 0 and returns a plausible wrong number. And Chapter 24.5 measured the same shape for a drifted extern "C" signature between two C++ translation units.

And you lose exceptions, in the sense that they must not cross. An extern "C" function may be implemented in C++ and may use exceptions internally, but one escaping into C-compiled frames is undefined behaviour — Chapter 28.2 §5. Hence the try/catch(...) wrapper at every entry point in Chapter 28.13 §2.

What you keep is everything inside. extern "C" changes the linkage of a name, not the language of the body: classes, templates, the standard library, exceptions used internally, all fine. Chapter 28.6 is that distinction in full, and it is the reason a C++ reference model can be exposed to a C interface at all.

6. Mixed Builds

Two practical points about compiling the boundary, both of which produce confusing errors when wrong.

A header shared by C and C++ needs the guard. If the same declarations are included from C++ and from C, the extern "C" must be conditional:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#ifdef __cplusplus
extern "C" {
#endif

int model_step(const unsigned char* in, unsigned len);

#ifdef __cplusplus
}
#endif

C does not know the keyword, so an unguarded extern "C" is a syntax error when the header is read by a C compiler. Chapter 24.3's include discipline applies to the rest of the header's structure.

And the final link must use the C++ driver if any part of the build is C++. Linking with clang rather than clang++ omits the C++ runtime, producing undefined symbols for library internals — ___cxa_..., std::... — rather than for anything you wrote. That error looks like a different problem entirely, and the fix is the driver rather than anything in the source. Chapter 28.3 §6 noted it from the build side; it belongs here too because the symptom is also an undefined-symbol error, and distinguishing the two is the skill.

The distinction in one line: undefined symbols naming your function mean linkage; undefined symbols naming library internals mean the wrong driver.

7. The Table

the questionthe answermeasured
what does linkage decidethe symbol name the compiler emits§1
what does C++ linkage encodethe parameter types, so overloads differ§1
what does the simulator look forthe plain C name — only the name is checked§1
the two symbols, same body__Z14model_step_cppPKhj and _model_step§2
is the leading underscore part of the nameno — Mach-O convention; ELF has none§2
what does the link failure sayUndefined symbols ... "_model_step", referenced from: _main§3
does it mention the mangled nameno — only what was wanted§3
what does the fix costone keyword; then exit 0 and the right answer§3
how do you diagnose itnm -gU obj.o | c++filt, three outcomes§4
what about a static entry pointinternal linkage — same error, not listed by nm -gU§4
what does C linkage costoverloading, and type checking across the boundary§5
why is the DPI contract uncheckedbecause discarding the types is extern "C"'s purpose§5 callout
undefined symbols naming library internalsthe wrong linker driver, not linkage§6

8. What to Actually Do

  • Put extern "C" on every DPI entry point, and nothing else.
  • Run nm -gU obj.o | c++filt first when a binding does not resolve. It answers the question in one command.
  • Read the undefined-symbol name as what was wanted, and check the object for the same identifier with types attached.
  • Never make a DPI entry point static — internal linkage produces the same error with no symbol to find.
  • Remember the leading underscore is the platform's, not your identifier's.
  • Guard extern "C" with #ifdef __cplusplus in any header a C compiler may read.
  • Link the final artefact with the C++ driver whenever any part of the build is C++.
  • Distinguish the two undefined-symbol failures: your function means linkage, library internals mean the driver.
  • Name overloaded entry points by hand — model_step_i, model_step_d — because C linkage has no room for overloads.
  • Expect no type checking across the boundary, and put the evidence in a generated source, a cross-checker, or a known-value call.

9. Mistakes Worth Making Once

Forgetting extern "C" on the entry point. The symbol is mangled and the link fails naming the plain name.

Reading the undefined-symbol error as "the object was not linked". The object is there; the name is different.

Not demangling before interpreting a linker error. __Z14model_step_cppPKhj is unreadable and c++filt makes it a declaration.

Making the entry point static. Same error, and nm -gU shows nothing.

Writing _model_step in the source because that is what nm printed.

An unguarded extern "C" in a header a C compiler reads. A syntax error in C.

Linking with clang when the model is C++. Undefined symbols for library internals, which looks like a different bug.

Trying to overload two extern "C" functions. There is nothing left to distinguish them.

Expecting a width mismatch to be caught. C linkage discarded exactly that information.

10. Interview Relevance

The frozen curriculum marks this Critical, and it is a favourite because the answer is short, checkable, and separates people who have integrated a model from people who have read about it.

The question is usually a symptom: your DPI import does not link — what is wrong? The expected answer is the missing extern "C", and the way to say it well is to describe the mechanism and the evidence together. C++ encodes parameter types into the symbol name so that overloading can work; the simulator looks for the plain C identifier, because an import declaration gives it a name and nothing else; so without extern "C" the compiler emitted something like __Z14model_step_cppPKhj while the linker wanted _model_step. Then the diagnostic: nm -gU on the object piped through c++filt, which tells you in one command whether the name is plain, mangled, or absent.

Two details are worth volunteering. The leading underscore is a Mach-O convention and not part of the identifier, which stops a confusing detour. And a static entry point produces the identical error with nothing for nm -gU to show, which is the case people chase for an hour.

The observation that marks a senior answer is what C linkage costs. extern "C" works by discarding the type information that mangling would have carried — and that is precisely the information which would have caught a signature mismatch. So a wrong width or a swapped argument links cleanly and returns a plausible number, and the unchecked DPI contract is not a gap in the standard but the price of being able to call plain C functions at all. Which is why you add the check back yourself: one generated source, a build-time cross-checker, or a known-value assertion through the boundary.

11. What You Should Be Able to Say Now

  • That linkage decides the symbol name, that C++ encodes parameter types into it so overloads differ, and that C does not.
  • That a DPI import gives the simulator only a name, so the C++ must have C linkage to be found.
  • The measured pair from one file with identical bodies: __Z14model_step_cppPKhj and _model_step, the first demangling to model_step_cpp(unsigned char const*, unsigned int).
  • That the leading underscore is Mach-O's, and ELF has none.
  • The exact failure: Undefined symbols for architecture arm64: "_model_step", referenced from: _main in caller.o, link exit 1 — and that it names only what was wanted, never the mangled near-miss in the same object.
  • That with extern "C" the identical caller links with exit 0 and returns the right answer.
  • The ten-second diagnostic — nm -gU obj.o | c++filt — and its three outcomes: plain, mangled, absent.
  • That a static entry point gives the same error and is not listed by nm -gU.
  • That C linkage costs overloading and type checking across the boundary, and that exceptions must not escape.
  • That extern "C" works by discarding exactly the information that would catch a signature mismatch, so the unchecked contract is the price of the reuse, not a defect.
  • That undefined symbols naming your function mean linkage, while ones naming library internals mean the wrong linker driver.

12. Exercises

Exercise 1 — the two symbols. Compile one file with two identical bodies, one extern "C" and one not. Print nm -gU and pipe it through c++filt.

Exercise 2 — the failure. Write a C caller for the non-extern "C" version and link it. Read the error and identify which name it reports.

Exercise 3 — the fix. Add the keyword and confirm exit 0 and an answer you computed by hand.

Exercise 4 — the static trap. Make the entry point static and compare the error with Exercise 2's. Then check what nm -gU shows.

Exercise 5 — the wrong driver. Link a C++-using object with clang instead of clang++ and read the undefined symbols. Note that none of them is your function.

Exercise 6 — the guard. Write a header included from both C and C++ and confirm it fails without #ifdef __cplusplus.

Exercise 7 — the cost. Declare int f(int) in a C caller against a definition of int f(double) and confirm it links. Predict the output before running it.

13. Summary

  • Linkage decides the symbol name. C++ encodes the parameter types so overloads can be distinguished; C uses the identifier alone.
  • A DPI import supplies only a name, so a C++-linkage definition is invisible to it.
  • Measured, from one file with two identical bodies: __Z14model_step_cppPKhj without extern "C" and _model_step with it, the first demangling to model_step_cpp(unsigned char const*, unsigned int). The leading underscore is a Mach-O convention, absent on ELF.
  • The failure is distinctive: Undefined symbols ... "_model_step", referenced from: _main in caller.o, exit 1 — naming only the name that was wanted, never the mangled one sitting in the same object file. With extern "C", the identical caller links with exit 0 and returns 100.
  • The ten-second diagnostic is nm -gU obj.o | c++filt: plain means linkage is fine, mangled means add the keyword, absent means wrong object — or a static entry point, which has internal linkage and produces the same error with nothing to show.
  • C linkage costs overloading and, more importantly, type checking across the boundary — and exceptions must not escape it.
  • extern "C" works by discarding the very information that would catch a signature mismatch, which is why the DPI contract is unchecked: that looseness is the price of calling plain C functions and of the model being a plain library the firmware team can also link.
  • So add the check back yourself — one generated source, a build-time cross-checker, or a known-value call through the boundary on every run.
  • And distinguish the two undefined-symbol failures: your function's name means linkage; library internals mean you linked with the C driver instead of the C++ one.

Chapter 28.6 takes the keyword seriously as a design tool: where exactly to put the boundary so that classes, templates and the standard library stay on the right side of it.

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.