Skip to content

Commit f994468

Browse files
committed
fix: discover LLVM bitcode DLL exports without overriding annotations
Export intent is checked before candidate enumeration. The selected LLVM compiler and llvm-nm handle LTO objects, while ordinary COFF retains its native reader. Per-target auto_export disables discovery for root and dependency DLLs.
1 parent 6e3cb97 commit f994468

19 files changed

Lines changed: 747 additions & 59 deletions

‎docs/04-mcpp-toml.md‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,10 @@ soname = "libmydriver.so.1"
263263
exports = "abi/mydriver.exports" # or inline: exports = ["vk_icd*"]
264264
```
265265

266-
**Omitting the key publishes everything, which is what both platforms already
267-
do** — ELF gives symbols default visibility, and PE gets an auto-generated
268-
`.def` listing every symbol. `exports` narrows that.
266+
Omitting `exports` leaves ELF and Mach-O's native visibility rules in effect.
267+
On the MSVC ABI, mcpp discovers exportable external definitions unless an input
268+
already declares exports or `auto_export = false` disables discovery. `exports`
269+
narrows the linker's published set.
269270

270271
Two projects need the narrowing. A **runtime with a stable ABI** publishes a
271272
reviewed set and nothing else, so that what is not in the set stays free to
@@ -300,6 +301,31 @@ A `soname` is meaningful on `kind = "lib"` too — see
300301
[`dependency_linkage`](#dependency_linkage--static-or-shared-is-the-consumers-decision)
301302
below, where the form a library takes becomes the consumer's decision.
302303

304+
#### `auto_export` — native export control on the MSVC ABI (unreleased)
305+
306+
The key and LLVM bitcode discovery require an unreleased source build; they are
307+
not available in mcpp 2026.10.3.1. A target that supplies its own export control
308+
can omit the automatic export-discovery step:
309+
310+
```toml
311+
[targets.plugin]
312+
kind = "shared"
313+
auto_export = false
314+
```
315+
316+
The boolean defaults to `true` and applies only to PE shared libraries on the
317+
MSVC ABI, including clang and clang-cl. It applies when a library target is
318+
built as a dependency too. It has no effect on static libraries, executables,
319+
ELF, Mach-O or MinGW. Native `__declspec(dllexport)`, linker flags and explicit
320+
`exports` lists remain effective when discovery is disabled.
321+
322+
With discovery enabled, any input's explicit export intent suppresses automatic
323+
exports for the whole DLL. COFF directives are checked first. LLVM bitcode is
324+
inspected with the selected LLVM compiler, including `dllexport` declarations
325+
and linker-option metadata. Only an unannotated DLL needs candidate enumeration;
326+
bitcode candidates come from `llvm-nm` beside that compiler. Both FullLTO and
327+
ThinLTO inputs can be mixed with ordinary COFF objects.
328+
303329
#### `windows_subsystem` and `windows_entry` — a Windows GUI executable (mcpp 2026.9.12.2+)
304330

305331
```toml

‎docs/12-binary-distribution.md‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -372,9 +372,9 @@ exports at 65535.
372372
mcpp generates the `.def` from the objects, which is what CMake's
373373
`WINDOWS_EXPORT_ALL_SYMBOLS` has done since 3.4. It is a build-graph node whose
374374
inputs are the same objects the link consumes, so the exported surface cannot
375-
drift from what was compiled, and it reads COFF directly rather than shelling out
376-
to `dumpbin` — that tool lives in a Visual Studio developer environment, and
377-
mcpp's default Windows toolchain is clang.
375+
drift from what was compiled. Ordinary COFF objects are read directly. LLVM
376+
bitcode inspection (unreleased source builds) uses the selected LLVM compiler
377+
and the `llvm-nm` beside it; both FullLTO and ThinLTO are accepted.
378378

379379
**Two limits survive that no tool can remove**, and they are the same two CMake
380380
documents for the same mechanism:
@@ -386,10 +386,12 @@ documents for the same mechanism:
386386

387387
Both are answered by annotation, and **annotation wins**: an object that already
388388
carries `/EXPORT:` directives — which is what `__declspec(dllexport)` emits —
389-
makes mcpp stand down and generate nothing. Adding a list on top would export the
389+
makes mcpp write an empty `EXPORTS` section. Adding a list on top would export the
390390
same names twice (`LNK4197`) and export everything else besides, replacing a
391-
chosen public surface with all of it. Nothing is configured for this; the objects
392-
say it.
391+
chosen public surface with all of it. Bitcode's `dllexport` storage class and
392+
linker-option metadata express the same intent. Export intent is checked before
393+
candidate enumeration. The per-target [`auto_export`](04-mcpp-toml.md#auto_export--native-export-control-on-the-msvc-abi-unreleased)
394+
key disables discovery entirely (unreleased source builds).
393395

394396
Past 65535 exportable symbols mcpp refuses rather than truncating. A truncated
395397
export table links cleanly and then fails at whichever consumer needed the symbol

‎docs/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ token in front of a reader to the chapter that owns it.
118118
| | chapter | | chapter |
119119
|---|---|---|---|
120120
| `[package]`, `[targets.<n>]`, `[build]`, `[lib]` | [04](04-mcpp-toml.md) | `[profile.<n>]`, `[resources]`, `[runtime]` | [04](04-mcpp-toml.md) |
121+
| `[targets.<n>] auto_export` | [04](04-mcpp-toml.md) | `[targets.<n>] exports` | [04](04-mcpp-toml.md) |
121122
| `[dependencies]`, `[dev-dependencies]`, `[build-dependencies]` | [05](05-dependencies.md) | `scan_overrides`, `module_extensions` | [04](04-mcpp-toml.md) |
122123
| `[features]`, `[feature-deps.<f>]`, `provides` / `requires` | [06](06-features-and-capabilities.md) | `[workspace]` | [07](07-workspace.md) |
123124
| `[toolchain]`, `cxx_runtime` | [20](20-toolchains.md) | `[target.<sel>]`, `cfg(…)` | [22](22-target-side.md) |

‎docs/zh/04-mcpp-toml.md‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,9 +265,9 @@ soname = "libmydriver.so.1"
265265
exports = "abi/mydriver.exports" # or inline: exports = ["vk_icd*"]
266266
```
267267

268-
**省略这个键会发布一切,而这恰好是两个平台本来就在做的事**——ELF 给
269-
符号默认可见性,PE 自动生成一份列出每个符号的 `.def`。`exports` 收窄
270-
这个范围。
268+
省略 `exports` 会保留 ELF 和 Mach-O 的原生可见性规则。在 MSVC ABI 上,
269+
mcpp 会发现可导出的外部定义;任一输入已经声明导出,或设置了
270+
`auto_export = false` 时不进行这种发现。`exports` 收窄链接器发布的集合。
271271

272272
两类工程需要这种收窄。**带稳定 ABI 的运行时**只发布一份经过审查的
273273
集合,其余一概不发布,让不在集合里的东西保留自由变化的空间。**与同类
@@ -300,6 +300,28 @@ script 并通过 `[build] ldflags` 传入,或者自行计算并发出
300300
[`dependency_linkage`](#dependency_linkage--静态还是动态由消费者决定),
301301
在那里,一个库采取的形式变成消费者的决定。
302302

303+
#### `auto_export` —— MSVC ABI 上的原生导出控制(尚未发布)
304+
305+
这个键和 LLVM bitcode 导出发现需要尚未发布的源码构建,mcpp 2026.10.3.1
306+
不提供这些能力。自行控制导出的 target 可以省略自动导出发现步骤:
307+
308+
```toml
309+
[targets.plugin]
310+
kind = "shared"
311+
auto_export = false
312+
```
313+
314+
这个布尔值默认为 `true`,只作用于 MSVC ABI 上的 PE 共享库,包括 clang 和
315+
clang-cl;库作为依赖构建时也生效。它不影响静态库、可执行文件、ELF、Mach-O
316+
或 MinGW。关闭发现后,原生 `__declspec(dllexport)`、链接旗标和显式 `exports`
317+
列表仍然生效。
318+
319+
开启发现时,任一输入的显式导出意图都会禁止整个 DLL 的自动导出。首先检查
320+
COFF 指令,再使用所选 LLVM 编译器检查 bitcode 中的 `dllexport` 声明和
321+
linker-option 元数据。只有没有标注的 DLL 才需要枚举候选符号,bitcode 的
322+
候选符号由该编译器旁的 `llvm-nm` 提供。FullLTO 和 ThinLTO 输入都可以与
323+
普通 COFF 对象混用。
324+
303325
#### `windows_subsystem` 与 `windows_entry` —— Windows GUI 可执行文件(mcpp 2026.9.12.2+)
304326

305327
```toml

‎docs/zh/12-binary-distribution.md‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -343,9 +343,9 @@ error while loading shared libraries: libstdc++.so.6: cannot open shared object
343343

344344
mcpp 从对象文件生成 `.def`——这正是 CMake 的 `WINDOWS_EXPORT_ALL_SYMBOLS`
345345
自 3.4 起在做的事。它是构建图里的一个节点,输入就是链接所消费的那批对象,
346-
因此导出面不会与「实际编译了什么」发生漂移;而且它直接读 COFF,不会 shell 出去
347-
调用 `dumpbin`——那个工具存在于 Visual Studio 的开发者环境里,而 mcpp 在
348-
Windows 上的默认工具链是 clang。
346+
因此导出面不会与「实际编译了什么」发生漂移。普通 COFF 对象直接读取;LLVM
347+
bitcode 检查(尚未发布的源码构建)使用所选 LLVM 编译器和它旁边的 `llvm-nm`,
348+
支持 FullLTO 和 ThinLTO。
349349

350350
**有两条限制是任何工具都消不掉的**,与 CMake 为同一机制记录的正是同样两条:
351351

@@ -355,10 +355,12 @@ Windows 上的默认工具链是 clang。
355355
| **vtable** 被引用的类 | 整个类都要被标注,例如一个带虚函数的类的委托构造函数 |
356356

357357
两者都靠标注来回答,而且**标注优先**:一个已经带 `/EXPORT:` 指令的对象——那
358-
正是 `__declspec(dllexport)` 产生的——会让 mcpp 让开,什么都不生成。在其上再
358+
正是 `__declspec(dllexport)` 产生的——会让 mcpp 写入空的 `EXPORTS` 节。在其上再
359359
叠加一份列表,会把同一批符号导出两次(`LNK4197`),还会把其余所有符号也一并
360-
导出,用「全部」取代作者选定的那个公开面。这件事没有任何东西需要配置:对象
361-
文件自己说了算。
360+
导出,用「全部」取代作者选定的那个公开面。bitcode 的 `dllexport` 存储类别和
361+
linker-option 元数据表达同样的意图。导出意图在枚举候选符号前检查。每个 target
362+
可以通过 [`auto_export`](04-mcpp-toml.md#auto_export--msvc-abi-上的原生导出控制尚未发布)
363+
完全关闭导出发现(尚未发布的源码构建)。
362364

363365
超过 65535 个可导出符号时,mcpp 拒绝而不是截断。一个被截断的导出表能干净地
364366
链接完成,随后在恰好需要那个掉出去的符号的消费方那里失败。

‎docs/zh/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
| | 章节 | | 章节 |
115115
|---|---|---|---|
116116
| `[package]`、`[targets.<n>]`、`[build]`、`[lib]` | [04](04-mcpp-toml.md) | `[profile.<n>]`、`[resources]`、`[runtime]` | [04](04-mcpp-toml.md) |
117+
| `[targets.<n>] auto_export` | [04](04-mcpp-toml.md) | `[targets.<n>] exports` | [04](04-mcpp-toml.md) |
117118
| `[dependencies]`、`[dev-dependencies]`、`[build-dependencies]` | [05](05-dependencies.md) | `scan_overrides`、`module_extensions` | [04](04-mcpp-toml.md) |
118119
| `[features]`、`[feature-deps.<f>]`、`provides` / `requires` | [06](06-features-and-capabilities.md) | `[workspace]` | [07](07-workspace.md) |
119120
| `[toolchain]`、`cxx_runtime` | [20](20-toolchains.md) | `[target.<sel>]`、`cfg(…)` | [22](22-target-side.md) |

‎modules/manifest/src/toml.cppm‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1849,6 +1849,12 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
18491849
if (auto msg = validate_target_soname(t, std::format("targets.{}.", tname))) {
18501850
return std::unexpected(error(origin, *msg));
18511851
}
1852+
if (auto it = tt.find("auto_export"); it != tt.end()) {
1853+
if (!it->second.is_bool())
1854+
return std::unexpected(error(origin, std::format(
1855+
"targets.{}.auto_export must be a boolean", tname)));
1856+
t.autoExport = it->second.as_bool();
1857+
}
18521858
// `exports` -- a file of symbol patterns, or the patterns inline.
18531859
//
18541860
// BOTH FORMS, because the two are used at different scales and the ABI
@@ -1983,7 +1989,7 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
19831989
// must reach SHARED code is intentionally not a target key; point users
19841990
// at the right axis (workspace / features / profile).
19851991
static constexpr std::string_view kKnownTargetKeys[] = {
1986-
"kind", "linkage", "main", "soname", "exports",
1992+
"kind", "linkage", "main", "soname", "exports", "auto_export",
19871993
"cflags", "cxxflags", "defines", "required_features",
19881994
"windows_entry", "windows_subsystem", "windows_code_page",
19891995
};

‎modules/manifest/src/types.cppm‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,9 @@ struct Target {
178178
// stand in for it, adds `Application` guarded by the row's actual form.
179179
bool is_program() const { return kind == Binary || kind == Application; }
180180
std::string soname; // ABI name for shared libraries, e.g. libfoo.so.1
181+
// PE / MSVC ABI only: discover exports when the objects do not declare
182+
// any. False leaves export control entirely to the native linker inputs.
183+
bool autoExport = true;
181184
// WHICH SYMBOLS THIS ARTIFACT PUBLISHES. Empty = every symbol, which is
182185
// what both platforms do today (ELF default visibility; PE gets an
183186
// auto-generated .def listing everything, mcpp.build.coff_exports).

‎modules/manifest/src/xpkg.cppm‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1626,6 +1626,13 @@ synthesize_from_xpkg_lua(std::string_view luaContent,
16261626
t.main = cur.read_string();
16271627
} else if (sub == "soname") {
16281628
t.soname = cur.read_string();
1629+
} else if (sub == "auto_export") {
1630+
auto raw = cur.read_bareword();
1631+
if (raw != "true" && raw != "false")
1632+
return std::unexpected(ManifestError{
1633+
std::format("targets.{}.auto_export must be a boolean", tname),
1634+
m.sourcePath, 0, 0});
1635+
t.autoExport = raw == "true";
16291636
} else if (sub == "required_features") {
16301637
// #355: without this, a Form B descriptor could not
16311638
// express the cost gate that makes an optional host

‎src/build/coff_exports.cppm‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ bool is_supported_machine(std::uint16_t machine);
7474
// place to say something the objects already say, and the two could disagree.
7575
bool declares_exports(std::span<const std::byte> bytes);
7676

77+
// Shared by the COFF and LLVM symbol readers so LTO does not change the
78+
// filtering policy or i386 calling-convention spelling.
79+
std::optional<std::string> export_name(std::string_view name, bool i386);
80+
7781
// Read one object file's exportable symbols. `bytes` is a whole `.obj`.
7882
//
7983
// Returns an error string for input this reader cannot honestly interpret —
@@ -151,6 +155,15 @@ bool is_skipped_name(std::string_view n) {
151155

152156
} // namespace
153157

158+
std::optional<std::string> export_name(std::string_view name, bool i386) {
159+
if (name.empty() || is_skipped_name(name)) return std::nullopt;
160+
std::string out(name);
161+
if (i386 && out.starts_with('_') && out.find('@') == std::string::npos)
162+
out.erase(0, 1);
163+
if (out.empty()) return std::nullopt;
164+
return out;
165+
}
166+
154167
bool declares_exports(std::span<const std::byte> bytes) {
155168
if (bytes.size() < kFileHeaderSize) return false;
156169
const auto numSections = rd16(bytes, 2);
@@ -293,13 +306,8 @@ read_exports(std::span<const std::byte> bytes)
293306

294307
auto nm = name_at(rec);
295308
if (!nm) return std::unexpected(nm.error());
296-
if (nm->empty() || is_skipped_name(*nm)) continue;
297-
298-
// i386 (and `__cdecl` on it) carries a leading underscore that is part
299-
// of the calling convention rather than of the name.
300-
std::string name = *nm;
301-
if (machine == kMachineI386 && name.starts_with('_') && name.find('@') == std::string::npos)
302-
name.erase(0, 1);
309+
auto name = export_name(*nm, machine == kMachineI386);
310+
if (!name) continue;
303311

304312
const auto flags = std::size_t(sectionNum) < sectionFlags.size()
305313
? sectionFlags[sectionNum] : 0u;
@@ -308,7 +316,7 @@ read_exports(std::span<const std::byte> bytes)
308316
// section (a `const`) is still data.
309317
const bool isData = type != kSymTypeFunction
310318
&& (flags & kScnMemExecute) == 0;
311-
out.push_back(Export{ std::move(name), isData });
319+
out.push_back(Export{ std::move(*name), isData });
312320
}
313321

314322
return out;

0 commit comments

Comments
 (0)