diff --git a/README.md b/README.md index 952ca089..d94f1785 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ C library. ```toml [dependencies] -openkal-llvm-runtime = "0.14.0" +openkal-llvm-runtime = "0.15.0" ``` > **Engine floor (mcpp 2026.9.20.1):** this version of this package pins diff --git a/examples/cxx/src/main.cpp b/examples/cxx/src/main.cpp index f98b727c..8ebf623b 100644 --- a/examples/cxx/src/main.cpp +++ b/examples/cxx/src/main.cpp @@ -43,6 +43,11 @@ static int hidden = 7; static int weak = 11; struct weak_alias { int value; }; +// THE thread_local PROBE'S SUBJECT, at file scope because a local struct's +// destructor cannot reach a lambda capture. See the criterion in `main`. +static std::atomic tls_dtor_ran{0}; +struct TlsProbe { int v; ~TlsProbe() { tls_dtor_ran.store(v); } }; + static int depth_three(int n) { if (n > 2) throw std::runtime_error("thrown"); return n; } static int depth_two(int n) { return depth_three(n) + 1; } static int depth_one(int n) { return depth_two(n) + 1; } @@ -286,6 +291,36 @@ int main() { check(!(a == b && b == c), "three draws from the entropy source differ"); } + // --- thread_local destructors, where no other C runtime supplies them --- + + // TWO CHECKS, AND THE SECOND IS THE ONE THAT WAS SILENTLY FALSE. + // + // `__cxa_thread_atexit` is exported by upstream libc++abi on Linux and + // Fuchsia only, because everywhere else another runtime already defines it + // --- on an ordinary MinGW target, `libmingw32.a`. openkal replaces the C + // library and its runtime together, so both sides assumed the other would + // and the link failed by name on `x86_64-windows-gnu`. + // + // Exporting it was not enough. The fallback kept its list of pending + // destructors in a `__thread` variable, and this runtime is built with + // `-femulated-tls` for PE: emutls releases the thread's block behind a + // pthread key of its own, and the key destructor that walks the list runs + // after that, reading a FRESH ZEROED block at a different address. The + // link then succeeded, the program ran, and nothing was destroyed. + // + // So a check that only asserted construction --- or only asserted that the + // program linked --- would have passed through both defects. The list now + // lives in the key's own value, which no other key's teardown can reach. + { + std::atomic seen{0}; + std::thread t([&]{ thread_local TlsProbe p{7}; seen.store(p.v); }); + t.join(); + check(seen.load() == 7, + "a thread_local is constructed in a spawned thread"); + check(tls_dtor_ran.load() == 7, + "and its destructor runs when that thread ends"); + } + std::printf("-- failures: %d --\n", failures); return failures != 0; } diff --git a/llvm/PATCHES.md b/llvm/PATCHES.md index 045eef8c..5118be33 100644 --- a/llvm/PATCHES.md +++ b/llvm/PATCHES.md @@ -1,6 +1,6 @@ # 这棵 vendored 树被动过的地方 -⭐ **规矩三条,与 `openkal-musl` 的一致:** + **规矩三条,与 `openkal-musl` 的一致:** 1. **可以动源码。** 移植就是动源码;假装不动只会把差异藏进别处。 2. **动过的地方要标注清楚。** 每一处都夹在 @@ -21,7 +21,7 @@ grep -rn "openkal ─── BEGIN" llvm/ > upstream 在这里问「这是哪个 OS」,而这个问题真正的答案是 openkal 的 `<接口>`。 -⚠️ 填不进去的**不要动**。`sizeof(long)`、`int64_t` 的拼法、目标格式、调用约定 +注意:填不进去的**不要动**。`sizeof(long)`、`int64_t` 的拼法、目标格式、调用约定 不是「下面是谁」,是**目标自己的定义** —— 那些归 C 库(`musl-generated/` 按 (arch, os) 分档),不归这里。 @@ -29,7 +29,7 @@ grep -rn "openkal ─── BEGIN" llvm/ ## 已替换 -⭐ 五处,四个文件,一个判据 —— 每一处都能填进上面那句话。 + 五处,四个文件,一个判据 —— 每一处都能填进上面那句话。 ```sh grep -rn "openkal ─── BEGIN" llvm/ # 10 处标记(含配对的 #else/#endif) @@ -62,16 +62,16 @@ _WIN32 → WaitOnAddress / WakeByAddressSingle openkal 的答案是 `kal_task_wait` / `kal_task_wake` —— 规范管它叫**挂起原语**, 而它在每一个目标上是同样的两个调用,**包括底下没有操作系统的那个**。 -⚠️ **它本来就已经到 openkal 了,只是绕了一圈。** 在 Linux 上,上游那条分支发 +**它本来就已经到 openkal 了,只是绕了一圈。** 在 Linux 上,上游那条分支发 `SYS_futex`,openkal-musl 的 port 拦下这个系统调用,它的 `__okm_futex` 调 `kal_task_wait`。直接走去掉了那一圈,并且让另外两种目标格式也能用 —— 在那里 「拦系统调用」没有系统调用可拦。 -⚠️ 两处细节按上游的语义保留:两秒的默认超时(没有超时的等待注意不到与它竞争的 +注意:两处细节按上游的语义保留:两秒的默认超时(没有超时的等待注意不到与它竞争的 唤醒),以及 `-1` 表示「全部唤醒」(上游写作 `INT_MAX`,openkal 的计数是 `kal_uintptr`,所以按回绕写)。 -### ⭐⭐ `libunwind/src/AddressSpace.hpp` — 这个镜像的展开表在哪 +### `libunwind/src/AddressSpace.hpp` — 这个镜像的展开表在哪 上游按 OS 答:`EnumProcessModules` 枚举进程的全部模块,逐个解析 PE 头找 `.eh_frame`。**而这个问题这份文件已经答过三遍,没有一遍问操作系统**: @@ -81,14 +81,14 @@ openkal 的答案是 `kal_task_wait` / `kal_task_wake` —— 规范管它叫** | 裸机 | 链接器脚本定义的 `__eh_frame_start` / `__eh_frame_end` | | Darwin | `_dyld_find_unwind_sections`(`openkal-macos` 用链接器的 `section$start$` 实现) | | ELF | 自己的 program headers | -| **PE(上游)** | ⚠️ 让操作系统枚举模块 | +| **PE(上游)** | 注意:让操作系统枚举模块 | ⇒ openkal 的答案和前三条同一句话:**镜像自己知道**。静态链接的 openkal 程序 只有一个模块,基址是链接器定义的符号 `__ImageBase`(不是调用),段表在离它固定 的偏移上。读它是**目标格式**的知识 —— 展开器本来就是由这种知识构成的 —— 不是 系统调用,也不是操作系统。 -⚠️ **而台账原先写着这一支「已由 DWARF 路线绕开」,那是凭读守卫写的,实测否掉了。** +**而台账原先写着这一支「已由 DWARF 路线绕开」,那是凭读守卫写的,实测否掉了。** `_WIN32 && DWARF` 正是它的守卫,DWARF 路线就是它。2026-08-23 实测: ``` @@ -99,11 +99,11 @@ AddressSpace.hpp:114 → /usr/x86_64-w64-mingw32/include/windows.h:69 —— 一条 include 把**宿主的 mingw sysroot** 拉进了刚刚摆脱了厂商 SDK 的构建。 -⭐ 实测确认段表里找得到:链接后的镜像里是 `.eh_fram`(PE 的段名字段固定 8 字节, + 实测确认段表里找得到:链接后的镜像里是 `.eh_fram`(PE 的段名字段固定 8 字节, `.eh_frame` 是 9 个字符),大小 0x530c8 —— 这正是上游用 `IMAGE_SIZEOF_SHORT_NAME` 比 8 个字节而不是比全名的原因。 -⚠️ **走过一条错路,记下来免得再走**:先试过用 COFF 的分组段 +**走过一条错路,记下来免得再走**:先试过用 COFF 的分组段 (`.eh_frame$a` / `.eh_frame$z`)去夹住 `.eh_frame`,链接器的排序是 ``` @@ -117,7 +117,7 @@ AddressSpace.hpp:114 → /usr/x86_64-w64-mingw32/include/windows.h:69 ### `libunwind/src/RWMutex.hpp` — 读写锁 -⚠️ 两处,而不是一处:`` 的 include 在 `_LIBUNWIND_HAS_NO_THREADS` +注意:两处,而不是一处:`` 的 include 在 `_LIBUNWIND_HAS_NO_THREADS` **之前**就无条件发生,类的选择是第二处。只撤回第一处会留下引用 `SRWLOCK` 而没有 任何东西声明它。 @@ -129,7 +129,7 @@ AddressSpace.hpp:114 → /usr/x86_64-w64-mingw32/include/windows.h:69 ### `compiler-rt/lib/builtins/emutls.c` — 互斥与对齐分配 -⚠️ **这个文件被编译到 PE 上,恰恰是因为那个平台自己的 thread-local 机制用不了** +**这个文件被编译到 PE 上,恰恰是因为那个平台自己的 thread-local 机制用不了** (`_tls_index` 需要动态加载器),然后它转身去要了同一个平台的 C 运行时: ``` @@ -178,31 +178,119 @@ openkal-musl 的 `[c-abi] presents = "posix"` 把它和 `_WIN32`、`__MINGW32__` 因为它**是**已安装的。两者读不同的宏,回答同一个关于同一个目标的问题,这正是下面那一节 要求的性质。 -⚠️ **下一次改这棵树,`grep` 的清单要加上 `__CYGWIN__`**: +**下一次改这棵树,`grep` 的清单要加上 `__CYGWIN__`**: `grep -rn "_WIN32\|_WIN64\|__MINGW32__\|__MINGW64__\|__CYGWIN__"`。 -## ⭐ 一个名字,不是五个 +## 第七处:`libcxxabi/src/cxa_thread_atexit.cpp`(2026-09-21) + +**前六处都是「守卫问错了问题,于是落错分支」。第七处不一样:守卫确实要放开,而放开之后 +它的实现本身在这个目标上是坏的**——只修守卫,换来的是一个静默的运行期失败。 + +### 第一层:导出 + +```c +#if defined(__linux__) || defined(__Fuchsia__) +extern "C" { int __cxa_thread_atexit(Dtor, void*, void*) throw(); } +#endif +``` + +上游只在这两个系统上导出,因为别处已经有人导出了——普通 MinGW 目标由 `libmingw32.a` +提供(`llvm-nm --defined-only libmingw32.a | grep -c __cxa_thread_atexit` 为 1)。openkal +把 C 库连同它的运行时一起换掉,于是两边都以为对方会提供。实测:索引兼容性测量里 +`doctest` 与 `spdlog` 两个成员在 `x86_64-windows-gnu` 上停在 +`ld.lld: error: undefined symbol: __cxa_thread_atexit`。 + +这个文件**不是**已安装的头,所以按本文的规矩用私有 define `OPENKAL_TARGET_WINDOWS`。 + +### 第二层:`__thread` 在 emutls 下活不过 key 析构 + +放开守卫之后**链接通了,而析构不跑**。上游的 fallback 把待执行析构的链表存在 +`__thread DtorList* dtors` 里,只拿 TLS key 的值当一个触发用的哑元。在本包为 PE 采用的 +`-femulated-tls` 下,这个前提不成立。实测(wine,同一个线程): + +``` +registered dtor, dtors=0x7ffffe994680, &dtors=0x7ffffe9946a8 +run_dtors called, dtors=0, &dtors=0x7ffffe9946c8 +``` + +**`&dtors` 两次不同。** emutls 把每线程的块挂在它自己的一个 pthread key 后面,而那个 key +的析构已经先释放了本线程的块;之后再读就新分配一个**清零**的块,地址每次都不一样。于是 +`run_dtors` 走的是一个空链表,每一个 `thread_local` 的析构被静默跳过。 + +**为什么之前一次探针把 emutls 排除掉了。** musl 按 key 的创建顺序逐个调析构。那次探针 +自己的 key 建在第一次访问 `thread_local` **之前**,于是 emutls 的 key 在它之后才被析构, +读到的值是对的。真实情形的顺序正好相反——**一个探针报不出它被构造成不会发生的那个顺序**。 + +### 第三层:macOS 用另一个名字问同一件事,而弱引用探测在 Mach-O 上不成立 + +**这一层是被上一层的判据挖出来的**——`examples/cxx` 新增那条 `thread_local` 断言之后, +`aarch64-macos` 那条腿立刻红了,而在此之前该目标从未构造过带析构的 `thread_local`。 + +``` +ld64.lld: error: undefined symbol: _tlv_atexit +>>> referenced by main.cpp:316 +``` + +clang 在 Mach-O 上把带非平凡析构的 `thread_local` 降解为 `_tlv_atexit`,在别处才是 +`__cxa_thread_atexit`。两者只差一个 dso handle,而 fallback 本来就不读它,所以不需要第二份 +实现——一个转发就够。供给 `_tlv_atexit` 的本来是 libSystem,而 libSystem 正是本栈替换掉的。 + +放开守卫到 `__APPLE__` 之后**又红了一次**,这次是另一个符号: + +``` +ld64.lld: error: undefined symbol: __cxa_thread_atexit_impl +``` + +上游用的是**弱声明 + 判空**——`if (__cxa_thread_atexit_impl)`。那是 ELF 的习语:未解析的 +弱符号在那里**就是 0**。ld64 在静态链接里不这么做,于是**探测本身**成了未定义符号。 +在这个目标上这个探测也是多余的:能定义那个符号的是 libSystem。 + +所以 Apple 直接走 fallback,而 fallback 的函数体被提出来命名,两个调用点共用一份实现。 + +### 修法:链表存进 key 自己的值 + +key 的析构函数本来就被交给 key 的值。把链表存在那里,任何别的 key 的拆除都碰不到它, +`dtors_alive` 也随之不需要——值非空就是「链表在」。 + +**判据是两条,缺一不可**(`examples/cxx`): + +``` +ok: a thread_local is constructed in a spawned thread +ok: and its destructor runs when that thread ends +``` + +只断言「链接通过」或只断言「构造发生」的判据,会同时放过前两层。 + +**三个目标都构建**(`x86_64-linux-gnu`、`x86_64-windows-gnu`、`aarch64-macos`,均从 +Linux 宿主交叉),前两个**实跑** `failures: 0`(Windows 经 wine)。macOS 的运行由 CI 的 +`host-dimension` / `run-on-macos` 覆盖。 + +**这条判据是它自己挖出第三层的。** 加上它之前,三个目标里没有任何一个构造过带析构的 +`thread_local`,于是 macOS 那一侧的两个缺口都不在场——判据的价值不在于它今天绿,而在于 +它让一类从来没有对象的检查有了对象。 + +## 一个名字,不是五个 五处补丁全部守卫在 **`OPENKAL`** 上,`cflags` 和 `cxxflags` 各给一次 (`compiler-rt` 是 C)。读法是「在 Windows 上,除非底下是 openkal」。 -⚠️ 一度写成 `_LIBUNWIND_OPENKAL`,随后 `emutls.c` 需要同一个事实而它是 C 文件 —— +注意:一度写成 `_LIBUNWIND_OPENKAL`,随后 `emutls.c` 需要同一个事实而它是 C 文件 —— **同一个事实两个名字**正是这套代码里反复出问题的形状,所以收敛掉了。 --- ## 不需要动的 —— 而这一节比上一节重要 -⭐ 整棵树里 `#include ` 共 **19 处**。按守卫分类: + 整棵树里 `#include ` 共 **19 处**。按守卫分类: | 守卫 | 处数 | 怎么解决的 | |---|---|---| -| `_LIBCPP_WIN32API` | **12** | ✅ `port/include/__config` 撤回了它 → **自己落到 POSIX 分支** | -| `__SEH__` / `_LIBUNWIND_SUPPORT_DWARF_UNWIND` | 2 | ✅ `-fdwarf-exceptions`,异常机制跟随我们带的 unwinder | -| 裸 `_WIN32` | 3 | ✅ 三处全换(AddressSpace / RWMutex / UnwindCursor) | -| `emutls.c` 的 `_WIN32` | 2 | ✅ 走 POSIX 分支 | +| `_LIBCPP_WIN32API` | **12** | 已 `port/include/__config` 撤回了它 → **自己落到 POSIX 分支** | +| `__SEH__` / `_LIBUNWIND_SUPPORT_DWARF_UNWIND` | 2 | 已 `-fdwarf-exceptions`,异常机制跟随我们带的 unwinder | +| 裸 `_WIN32` | 3 | 已 三处全换(AddressSpace / RWMutex / UnwindCursor) | +| `emutls.c` 的 `_WIN32` | 2 | 已 走 POSIX 分支 | -⭐⭐ **12 处不用换,因为 libc++ 的 POSIX 分支本来就已经在 openkal 上了。** 它调 + **12 处不用换,因为 libc++ 的 POSIX 分支本来就已经在 openkal 上了。** 它调 `fopen` / `clock_gettime` / `pthread_*`,那些是 musl,而 musl 就在 openkal 上。 让谓词答对,它自己就走到那条路。 @@ -211,7 +299,7 @@ openkal-musl 的 `[c-abi] presents = "posix"` 把它和 `_WIN32`、`__MINGW32__` --- -## ⚠️ 不在这里的两类东西 +## 注意:不在这里的两类东西 一开始误以为要在这棵树里解决,实际不在: @@ -220,7 +308,7 @@ openkal-musl 的 `[c-abi] presents = "posix"` 把它和 `_WIN32`、`__MINGW32__` | 数据模型(`sizeof(long)`) | **C 库**(`musl-generated/[-]`) | 它重建的是 POSIX,而 POSIX 自己命名了 `long`。openkal 接口上不存在这个问题 —— 见 openkal 的 §1.3 | | `-fdwarf-exceptions` / `-femulated-tls` | **mcpp**(`graph_runtime_compile_flags`) | 它们决定 `throw` 和 `thread_local` **编译成什么**,所以是**整张图**的性质,不是某个包的。写在本包 `[build]` 里时,只覆盖了本包的对象,使用者的 `main.o` 拿不到 | -⚠️ 第二行是实测逼出来的:全部编过之后,链接报 +注意:第二行是实测逼出来的:全部编过之后,链接报 `undefined symbol: __gxx_personality_seh0`,引用它的是**使用者的 `main.o`** —— 一个写了 `try` 而没有任何理由知道这件事的翻译单元。 @@ -233,5 +321,5 @@ openkal-musl 的 `[c-abi] presents = "posix"` 把它和 `_WIN32`、`__MINGW32__` | **头文件**里的平台分派 | `port/include/` 覆盖 | 靠 include 顺序遮蔽,vendored 树逐字节不动,**不随上游漂移** | | **源码**里的平台分派 | 本文件记录的原地标记 | 消费者不 include `.cpp`,遮蔽不了 | -⚠️ 顺序是有偏好的:**能用覆盖就别用补丁**。覆盖的漂移面是零,补丁的漂移面是被 +注意:顺序是有偏好的:**能用覆盖就别用补丁**。覆盖的漂移面是零,补丁的漂移面是被 标记的那几十行。 diff --git a/llvm/libcxxabi/src/cxa_thread_atexit.cpp b/llvm/libcxxabi/src/cxa_thread_atexit.cpp index 402a52c7..72f07741 100644 --- a/llvm/libcxxabi/src/cxa_thread_atexit.cpp +++ b/llvm/libcxxabi/src/cxa_thread_atexit.cpp @@ -66,21 +66,52 @@ namespace { DtorList* next; }; - // The linked list of thread-local destructors to run - __thread DtorList* dtors = nullptr; - // True if the destructors are currently scheduled to run on this thread - __thread bool dtors_alive = false; - // Used to trigger destructors on thread exit; value is ignored + // THE LIST LIVES IN THE TLS KEY'S OWN VALUE, NOT IN A `__thread` VARIABLE. + // + // Upstream keeps `__thread DtorList* dtors` here and passes the key a dummy + // value purely to arm this destructor. That is correct wherever the compiler + // emits native thread-local storage, and WRONG wherever it emits EMULATED + // TLS --- which is every PE target this package builds, since the platform's + // own `_tls_index` machinery is not available over openkal and the runtime + // is built with `-femulated-tls`. + // + // MEASURED, on `x86_64-windows-gnu` under wine. Registration and the key + // destructor run on the same thread and see different storage: + // + // registered dtor, dtors=0x7ffffe994680, &dtors=0x7ffffe9946a8 + // run_dtors called, dtors=0, &dtors=0x7ffffe9946c8 + // + // emutls keeps its per-thread blocks behind a pthread key of its own, and + // that key's destructor had already released this thread's block. A read + // after that point allocates a fresh, ZEROED one --- a different address + // every time, as the two calls above show. So `run_dtors` walked an empty + // list and every `thread_local` destructor was silently skipped: the link + // succeeded, the program ran, and nothing happened. + // + // KEY DESTRUCTOR ORDER IS WHY AN EARLIER PROBE EXONERATED emutls. musl + // iterates keys in creation order, so a probe whose own key is created + // BEFORE the first thread-local access sees emutls still alive in its + // destructor and reads the expected value. Here the order is the other way + // round, and a probe cannot report an ordering it was built to avoid. + // + // The fix needs no new mechanism: the key destructor is already handed the + // key's value, and a list kept there cannot be affected by any other key's + // teardown. `dtors_alive` goes with it --- a non-null value IS the list. std::__libcpp_tls_key dtors_key; - void run_dtors(void*) { - while (auto head = dtors) { - dtors = head->next; + void run_dtors(void* p) { + auto head = static_cast(p); + // Cleared BEFORE the walk, so a destructor that itself constructs a + // `thread_local` starts a fresh list rather than appending to the one + // being walked. POSIX may then call this destructor again for that list, + // which is the behaviour upstream's `dtors_alive` reset also allowed. + std::__libcpp_tls_set(dtors_key, nullptr); + while (head) { + auto next = head->next; head->dtor(head->obj); ::free(head); + head = next; } - - dtors_alive = false; } struct DtorsManager { @@ -99,48 +130,104 @@ namespace { // call the destructor here. This runs at exit time (potentially earlier // if libc++abi is dlclose()'d). Any thread_locals initialized after this // point will not be destroyed. - run_dtors(nullptr); + run_dtors(std::__libcpp_tls_get(dtors_key)); } }; + + // THE FALLBACK'S BODY, NAMED so that the two callers are one implementation + // rather than two copies that drift apart. + int register_dtor(Dtor dtor, void* obj) { + // Initialize the dtors std::__libcpp_tls_key (uses __cxa_guard_*() for + // one-time initialization and __cxa_atexit() for destruction) + static DtorsManager manager; + + auto head = static_cast(::malloc(sizeof(DtorList))); + if (!head) { + return -1; + } + + head->dtor = dtor; + head->obj = obj; + head->next = static_cast(std::__libcpp_tls_get(dtors_key)); + if (std::__libcpp_tls_set(dtors_key, head) != 0) { + ::free(head); + return -1; + } + + return 0; + } } // namespace #endif // HAVE___CXA_THREAD_ATEXIT_IMPL -#if defined(__linux__) || defined(__Fuchsia__) +// THE GUARD ASKS WHICH OPERATING SYSTEM, AND THE QUESTION IS WHETHER ANOTHER +// C++ RUNTIME IS IN THE IMAGE. +// +// Upstream exports `__cxa_thread_atexit` on Linux and Fuchsia only, because +// everywhere else somebody else already does --- on an ordinary MinGW target +// it is `libmingw32.a`, which defines exactly one such symbol. openkal +// replaces the C library AND its runtime, so both sides assumed the other +// would supply it and neither did: `ld.lld: error: undefined symbol: +// __cxa_thread_atexit`, measured on two members of the index's compatibility +// suite (doctest, spdlog) on `x86_64-windows-gnu`. +// +// `OPENKAL_TARGET_WINDOWS` IS THIS PACKAGE'S OWN DEFINE, not the engine's +// `__MCPP_TARGET_WINDOWS__`, and the difference is whose compile reads the +// file: this one is a source of this package and is never installed, so a +// package-private define reaches it. The same distinction chose the define in +// `compiler-rt/lib/builtins/int_lib.h`. +// +// THE FALLBACK BELOW WAS ALREADY COMPLETE; only the export was missing. What +// was NOT complete is where it kept its list --- see `run_dtors` above. +#if defined(__linux__) || defined(__Fuchsia__) \ + || defined(OPENKAL_TARGET_WINDOWS) || defined(__APPLE__) extern "C" { _LIBCXXABI_FUNC_VIS int __cxa_thread_atexit(Dtor dtor, void* obj, void* dso_symbol) throw() { #ifdef HAVE___CXA_THREAD_ATEXIT_IMPL return __cxa_thread_atexit_impl(dtor, obj, dso_symbol); +#elif defined(__APPLE__) + // NO WEAK PROBE ON MACH-O, AND THE REASON IS THE OBJECT FORMAT. + // + // The `else` branch below declares `__cxa_thread_atexit_impl` weak and + // tests it against null. That is an ELF idiom: an unresolved weak symbol + // there IS zero. ld64 does not do that in a static link, so the probe + // itself becomes the error --- measured building `examples/cxx` for + // `aarch64-macos`: + // + // ld64.lld: error: undefined symbol: __cxa_thread_atexit_impl + // + // The test is also pointless here: what would define that symbol is + // libSystem, and libSystem is what this stack replaces. + (void)dso_symbol; + return register_dtor(dtor, obj); #else if (__cxa_thread_atexit_impl) { return __cxa_thread_atexit_impl(dtor, obj, dso_symbol); } else { - // Initialize the dtors std::__libcpp_tls_key (uses __cxa_guard_*() for - // one-time initialization and __cxa_atexit() for destruction) - static DtorsManager manager; - - if (!dtors_alive) { - if (std::__libcpp_tls_set(dtors_key, &dtors_key) != 0) { - return -1; - } - dtors_alive = true; - } - - auto head = static_cast(::malloc(sizeof(DtorList))); - if (!head) { - return -1; - } - - head->dtor = dtor; - head->obj = obj; - head->next = dtors; - dtors = head; - - return 0; + return register_dtor(dtor, obj); } #endif // HAVE___CXA_THREAD_ATEXIT_IMPL } + +#if defined(__APPLE__) + // APPLE ASKS FOR THE SAME THING UNDER A DIFFERENT NAME, and supplies it from + // libSystem --- which is exactly what openkal replaces. + // + // clang lowers a `thread_local` with a non-trivial destructor to a call to + // `_tlv_atexit` on Mach-O and to `__cxa_thread_atexit` elsewhere, so the + // Windows gap this file was opened for has an Apple twin, invisible until + // something in the examples declared such a variable: + // + // ld64.lld: error: undefined symbol: _tlv_atexit + // + // The two differ only in the dso handle, which the fallback ignores. There + // is nothing to implement a second time. + _LIBCXXABI_FUNC_VIS void _tlv_atexit(Dtor dtor, void* obj) { + __cxa_thread_atexit(dtor, obj, nullptr); + } +#endif + } // extern "C" #endif // defined(__linux__) || defined(__Fuchsia__) } // namespace __cxxabiv1 diff --git a/mcpp.toml b/mcpp.toml index 95ee8cf2..26c595b4 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-llvm-runtime" -version = "0.14.0" +version = "0.15.0" description = "LLVM's C++ runtime libraries — libc++, libc++abi and libunwind — configured for openkal-musl rather than for a host C library." license = "Apache-2.0" authors = ["mcpplibs"]