Add Windows build with clang

This commit is contained in:
2026-02-23 16:29:49 +01:00
parent f1f11ecf9b
commit 0fda0d75fb
4 changed files with 67 additions and 9 deletions

View File

@@ -1,2 +1,6 @@
libhello.so libhello.so
libhello.dll
libhello.lib
main main
main.exe
libhello.exp

View File

@@ -6,8 +6,8 @@
`print_obj()`. `main.cpp` defines a `namespace nt` with a free function `print_obj()`. `main.cpp` defines a `namespace nt` with a free function
`print()`, and calls both `nt::print()` and `print_obj()`. `print()`, and calls both `nt::print()` and `print_obj()`.
`hello.cpp` is compiled into a shared library (`libhello.so`), and `main.cpp` is `hello.cpp` is compiled into a shared library (`libhello.so` on Linux,
linked against it. `libhello.dll` on Windows), and `main.cpp` is linked against it.
## The Problem ## The Problem
@@ -39,7 +39,7 @@ With `-O3` (optimizations on):
- The compiler inlines the class `nt::print()` directly into `print_obj()` - The compiler inlines the class `nt::print()` directly into `print_obj()`
- `_ZN2nt5printEv` is never emitted as a standalone symbol in `libhello.so` - `_ZN2nt5printEv` is never emitted as a standalone symbol in `libhello.so`
- No collision occurs each call resolves correctly - No collision occurs - each call resolves correctly
- Output: correct - Output: correct
This is why the bug was **optimization-dependent** and hard to spot. This is why the bug was **optimization-dependent** and hard to spot.
@@ -58,7 +58,7 @@ This is why the bug was **optimization-dependent** and hard to spot.
The C++ standard (basic.def.odr) requires that any entity with more than one The C++ standard (basic.def.odr) requires that any entity with more than one
definition across translation units must have **identical** definitions. definition across translation units must have **identical** definitions.
Violating this is **ill-formed, no diagnostic required** the compiler and Violating this is **ill-formed, no diagnostic required** - the compiler and
linker are not required to warn you. linker are not required to warn you.
The consequences are undefined behavior: the program may crash, produce wrong The consequences are undefined behavior: the program may crash, produce wrong
@@ -77,10 +77,37 @@ Windows (where DLL symbols are explicitly imported/exported via import tables
and don't collide this way). and don't collide this way).
This flat namespace behavior means a weak symbol in a `.so` can be silently This flat namespace behavior means a weak symbol in a `.so` can be silently
overridden by any strong symbol of the same name anywhere in the process even overridden by any strong symbol of the same name anywhere in the process - even
from the main executable. from the main executable.
## The Fix ## Why Windows Is Not Affected
On Windows, this collision does not occur. The DLL symbol model is fundamentally
different:
- Only symbols explicitly marked `__declspec(dllexport)` are visible outside the
DLL - everything else is private by default
- The linker generates an import library (`.lib`) with stubs that reference the
DLL by name, so the loader knows exactly which DLL each symbol comes from
- There is no flat global symbol namespace - each DLL is its own isolated scope
This means the internal `class nt` in `hello.cpp` was never at risk on Windows
regardless of optimization level. The bug is Linux (and ELF) specific.
To keep `hello.cpp` portable, the `__declspec(dllexport)` on `print_obj` is
wrapped in a macro:
```cpp
#ifdef _WIN32
#define EXPORT __declspec(dllexport)
#else
#define EXPORT
#endif
```
On Linux `EXPORT` expands to nothing. On Windows it marks the symbol for export.
## The Linux Fix
Wrap internal-use classes in an **anonymous namespace** in `hello.cpp`: Wrap internal-use classes in an **anonymous namespace** in `hello.cpp`:
@@ -103,14 +130,20 @@ The `_GLOBAL__N_1` prefix is the ABI encoding for the anonymous namespace,
making it unique per translation unit and invisible to the dynamic linker. No making it unique per translation unit and invisible to the dynamic linker. No
collision is possible. collision is possible.
The Linux and Windows fixes solve the same problem from opposite defaults:
- Linux: **opt-in to hiding** symbols (anonymous namespace, `-fvisibility=hidden`)
- Windows: **opt-in to exporting** symbols (`__declspec(dllexport)`)
## Key Takeaways ## Key Takeaways
- Class and namespace names mangle identically in the Itanium C++ ABI - Class and namespace names mangle identically in the Itanium C++ ABI
- Weak symbols in shared libraries can be silently hijacked by strong symbols in - Weak symbols in shared libraries can be silently hijacked by strong symbols in
the executable the executable
- ODR violations are UB with no required diagnostic bugs may only appear at - ODR violations are UB with no required diagnostic - bugs may only appear at
certain optimization levels certain optimization levels
- This is a Linux/ELF-specific problem - Windows DLLs use explicit export tables
and are not affected
- Internal implementation details in shared libraries should use anonymous - Internal implementation details in shared libraries should use anonymous
namespaces to prevent symbol leakage namespaces to prevent symbol leakage on Linux
- Use `objdump -t` or `nm` to inspect symbol visibility and catch these issues - Use `objdump -t` or `nm` to inspect symbol visibility and catch these issues
early early

15
namecollision/build.ps1 Normal file
View File

@@ -0,0 +1,15 @@
param(
[string]$Opt = "-O0"
)
$ErrorActionPreference = "Stop"
# Compile hello.cpp as a shared library (also generates libhello.lib import library)
clang++ -std=c++17 $Opt -shared -o libhello.dll hello.cpp "-Wl,/IMPLIB:libhello.lib"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
# Compile and link main.cpp with the shared library
clang++ -std=c++17 $Opt -o main.exe main.cpp libhello.lib
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Host "Build successful"

View File

@@ -1,5 +1,11 @@
#include <iostream> #include <iostream>
#ifdef _WIN32
#define EXPORT __declspec(dllexport)
#else
#define EXPORT
#endif
using namespace std; using namespace std;
class nt { class nt {
@@ -9,7 +15,7 @@ public:
} }
}; };
void print_obj() { EXPORT void print_obj() {
cout << "Hello from function\n"; cout << "Hello from function\n";
nt obj; nt obj;
obj.print(); obj.print();