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.dll
libhello.lib
main
main.exe
libhello.exp

View File

@@ -6,8 +6,8 @@
`print_obj()`. `main.cpp` defines a `namespace nt` with a free function
`print()`, and calls both `nt::print()` and `print_obj()`.
`hello.cpp` is compiled into a shared library (`libhello.so`), and `main.cpp` is
linked against it.
`hello.cpp` is compiled into a shared library (`libhello.so` on Linux,
`libhello.dll` on Windows), and `main.cpp` is linked against it.
## The Problem
@@ -39,7 +39,7 @@ With `-O3` (optimizations on):
- The compiler inlines the class `nt::print()` directly into `print_obj()`
- `_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
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
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.
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).
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.
## 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`:
@@ -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
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
- Class and namespace names mangle identically in the Itanium C++ ABI
- Weak symbols in shared libraries can be silently hijacked by strong symbols in
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
- 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
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
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>
#ifdef _WIN32
#define EXPORT __declspec(dllexport)
#else
#define EXPORT
#endif
using namespace std;
class nt {
@@ -9,7 +15,7 @@ public:
}
};
void print_obj() {
EXPORT void print_obj() {
cout << "Hello from function\n";
nt obj;
obj.print();