Skip to content

Commit a22f149

Browse files
committed
feat(port): 平台相关的换成 openkal 接口 —— 立规矩,并做掉 std::atomic 的等待
⭐ 规矩三条,写在 llvm/PATCHES.md 里,与 openkal-musl 的一致: 1. **可以动源码。** 移植就是动源码;假装不动只会把差异藏进别处。 2. **动过的地方要标注清楚。** 每一处夹在 `// ─── openkal ─── BEGIN/END` 之间, `grep -rn "openkal ─── BEGIN" llvm/` 一次数完。 3. **能不动的就不动。** 上游能自己走对的路,让它走。 判据:换的是「平台」不是「库」。每一处替换必须能填进这句话 ——「upstream 在这里 问『这是哪个 OS』,而这个问题真正的答案是 openkal 的 <接口>」。⚠️ 填不进去的不要 动:sizeof(long)、int64_t 的拼法、目标格式不是「下面是谁」,是目标自己的定义。 ## 已换:std::atomic 的等待与唤醒 上游一条链,五个分支,四个操作系统(SYS_futex / _umtx_op / futex / WaitOnAddress / 完全不等待)。openkal 的答案是 kal_task_wait / kal_task_wake —— 规范管它叫挂起 原语,而它在每一个目标上是同样的两个调用。 平台面**恰好两个函数**,其余 495 行全可移植 ⇒ 原地打标记的补丁,漂移面从 495 行 缩到约 50 行,而不是整文件拷贝。 ⚠️ 它本来就已经到 openkal 了,只是绕了一圈:Linux 上发 SYS_futex,musl 的 port 拦下来,__okm_futex 调 kal_task_wait。直接走去掉那一圈,并让另外两种目标格式也 能用 —— 在那里没有系统调用可拦。 ## 三处配套,而每一处都是「同一个事实的第二个说法」 - port/include/__atomic/contention_t.h —— 竞争计数器的宽度跟随挂起原语(4 字节) - port/include/__atomic/atomic_waitable_traits.h —— ⚠️ **只改前者不够**:开了 _LIBCPP_ABI_ATOMIC_WAIT_NATIVE_BY_SIZE 之后实例化清单来自这个宏而不是 sizeof(__cxx_contention_t)。两个头陈述一个事实,改了读者先找到的那个,报错一字 不变。(这是本移植第二次遇到「一个事实走两条通道」,链接线是第一次。) - OPENKAL_HAS_TASK —— ⚠️ 只提供 core 的实现没有挂起原语,实测裸机链接停在 undefined symbol: kal_task_wait。那是 6.1 在工作,不是缺陷;答案是落回上游那条 本来就正确的分支(单执行上下文的机器上 atomic::wait 没有东西可等,轮询是准确的 实现而不是替代品)。⚠️ 这与 openkal-musl 的 OKM_HAS_TASK 是同一个事实,说了 第二遍 —— 因为现在有第二个消费者。 ## PATCHES.md 里比「已换」更重要的一节:不需要动的 整棵树 #include <windows.h> 共 19 处。12 处守在 _LIBCPP_WIN32API 上,已由 port/include/__config 撤回 ⇒ **自己落到 POSIX 分支,而那条本来就在 openkal 上** (它调 fopen/clock_gettime/pthread_*,那些是 musl)。2 处守在 __SEH__ 上,已由 -fdwarf-exceptions 绕开。 ⇒ 这就是为什么整份移植是几十行而不是把 libc++ 重做一遍。**先问「上游有没有一条 路已经通向 openkal」,再考虑换。** 已验证三个目标:宿主 ✅ / 裸机 riscv64 ✅ / macOS 交叉产出 Mach-O arm64 ✅ (外部符号仍是那三个)。
1 parent ba4ba4d commit a22f149

5 files changed

Lines changed: 308 additions & 3 deletions

File tree

llvm/PATCHES.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# 这棵 vendored 树被动过的地方
2+
3+
**规矩三条,与 `openkal-musl` 的一致:**
4+
5+
1. **可以动源码。** 移植就是动源码;假装不动只会把差异藏进别处。
6+
2. **动过的地方要标注清楚。** 每一处都夹在
7+
`// ─── openkal ─── BEGIN` / `// ─── openkal ─── END` 之间,`grep` 一次能数完。
8+
3. **能不动的就不动。** 上游能自己走对的路,让它走 —— 见下面的「不需要动的」。
9+
10+
```sh
11+
# 数一遍
12+
grep -rn "openkal ─── BEGIN" llvm/
13+
```
14+
15+
---
16+
17+
## 判据:换的是「平台」,不是「库」
18+
19+
上游按**操作系统**选实现;openkal 的答案按**下面配置的是什么**。所以每一处替换
20+
都必须能填进这句话:
21+
22+
> upstream 在这里问「这是哪个 OS」,而这个问题真正的答案是 openkal 的 `<接口>`
23+
24+
⚠️ 填不进去的**不要动**`sizeof(long)``int64_t` 的拼法、目标格式、调用约定
25+
不是「下面是谁」,是**目标自己的定义** —— 那些归 C 库(`musl-generated/`
26+
(arch, os) 分档),不归这里。
27+
28+
---
29+
30+
## 已替换
31+
32+
### `libcxx/src/atomic.cpp``std::atomic` 的等待与唤醒
33+
34+
上游一条链,五个分支,四个操作系统:
35+
36+
```
37+
__linux__ → syscall(SYS_futex, …)
38+
__FreeBSD__ → _umtx_op
39+
__OpenBSD__ → futex()
40+
_WIN32 → WaitOnAddress / WakeByAddressSingle
41+
否则 → 完全不等待(自旋)
42+
```
43+
44+
平台面**恰好两个函数**(`__platform_wait_on_address` / `__platform_wake_by_address`),
45+
其余 495 行全是可移植的 —— 所以这里是**原地打标记的补丁**而不是整文件拷贝,
46+
漂移面从 495 行缩到约 50 行。
47+
48+
openkal 的答案是 `kal_task_wait` / `kal_task_wake` —— 规范管它叫**挂起原语**,
49+
而它在每一个目标上是同样的两个调用,**包括底下没有操作系统的那个**
50+
51+
⚠️ **它本来就已经到 openkal 了,只是绕了一圈。** 在 Linux 上,上游那条分支发
52+
`SYS_futex`,openkal-musl 的 port 拦下这个系统调用,它的 `__okm_futex`
53+
`kal_task_wait`。直接走去掉了那一圈,并且让另外两种目标格式也能用 —— 在那里
54+
「拦系统调用」没有系统调用可拦。
55+
56+
⚠️ 两处细节按上游的语义保留:两秒的默认超时(没有超时的等待注意不到与它竞争的
57+
唤醒),以及 `-1` 表示「全部唤醒」(上游写作 `INT_MAX`,openkal 的计数是
58+
`kal_uintptr`,所以按回绕写)。
59+
60+
---
61+
62+
## 不需要动的 —— 而这一节比上一节重要
63+
64+
⭐ 整棵树里 `#include <windows.h>`**19 处**。按守卫分类:
65+
66+
| 守卫 | 处数 | 怎么解决的 |
67+
|---|---|---|
68+
| `_LIBCPP_WIN32API` | **12** |`port/include/__config` 撤回了它 → **自己落到 POSIX 分支** |
69+
| `__SEH__` / `_LIBUNWIND_SUPPORT_DWARF_UNWIND` | 2 |`-fdwarf-exceptions`,异常机制跟随我们带的 unwinder |
70+
|`_WIN32` | 3 | 1 处已换(见上),2 处待办(见下) |
71+
72+
⭐⭐ **12 处不用换,因为 libc++ 的 POSIX 分支本来就已经在 openkal 上了。** 它调
73+
`fopen` / `clock_gettime` / `pthread_*`,那些是 musl,而 musl 就在 openkal 上。
74+
让谓词答对,它自己就走到那条路。
75+
76+
⇒ 这就是为什么整份移植是几十行而不是把 libc++ 重做一遍。**先问「上游有没有一条
77+
路已经通向 openkal」,再考虑换。**
78+
79+
---
80+
81+
## 待办
82+
83+
| 文件 | 平台面 | openkal 的答案 |
84+
|---|---|---|
85+
| `libunwind/src/RWMutex.hpp` | 读写锁 | `openkal.task`;或 `_LIBUNWIND_HAS_NO_THREADS`(unwinder 的锁只在缓存上) |
86+
| `libunwind/src/AddressSpace.hpp` | 段查询的 Windows 分支 | 已由 DWARF 路线绕开,待确认没有残留 |
87+
88+
---
89+
90+
## `port/include/` 的分工
91+
92+
| | 放哪 | 为什么 |
93+
|---|---|---|
94+
| **头文件**里的平台分派 | `port/include/` 覆盖 | 靠 include 顺序遮蔽,vendored 树逐字节不动,**不随上游漂移** |
95+
| **源码**里的平台分派 | 本文件记录的原地标记 | 消费者不 include `.cpp`,遮蔽不了 |
96+
97+
⚠️ 顺序是有偏好的:**能用覆盖就别用补丁**。覆盖的漂移面是零,补丁的漂移面是被
98+
标记的那几十行。

llvm/libcxx/src/atomic.cpp

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,52 @@
1919

2020
#include "include/apple_availability.h"
2121

22-
#ifdef __linux__
22+
// ─── openkal ─── BEGIN ────────────────────────────────────────────────────────
23+
//
24+
// This file's platform surface is exactly two functions, and the chain below
25+
// answers them once per operating system: Linux by `SYS_futex`, FreeBSD by
26+
// `_umtx_op`, OpenBSD by `futex`, Windows by `WaitOnAddress`, and everything
27+
// else by not waiting at all.
28+
//
29+
// openkal answers them once, full stop. `kal_task_wait` / `kal_task_wake` ARE
30+
// this primitive — the specification calls it the suspension primitive — and
31+
// they are the same two calls on every target this library is built for,
32+
// including one with no operating system underneath.
33+
//
34+
// ⚠️ IT WAS ALREADY REACHING openkal ON ONE TARGET, THE LONG WAY ROUND. On
35+
// Linux the branch below issues `SYS_futex`, openkal-musl's port intercepts the
36+
// system call, and its `__okm_futex` calls `kal_task_wait`. Going directly
37+
// removes that indirection AND makes the other object formats work, where the
38+
// interception has no system call to intercept.
39+
//
40+
// See llvm/PATCHES.md for every region of this vendored tree that is marked
41+
// like this and why.
42+
// ⚠️ AND ONLY WHEN THE IMPLEMENTATION BENEATH PROVIDES `openkal.task`.
43+
//
44+
// openkal is composable: an implementation provides an interface in whole or
45+
// not at all, and one with a single execution context has no suspension
46+
// primitive to offer — clause 6.2 says the remedy is that its absence be
47+
// expressed by its absence, so `kal_task_wait` is simply not there.
48+
//
49+
// ⚠️ Measured 2026-08-23 on riscv64 over SBI, after this block was written
50+
// without the guard: everything compiled and the link stopped on
51+
// `undefined symbol: kal_task_wait`. That is 6.1 working, not a defect — and
52+
// the answer is not to weaken openkal but to fall through to the branch that
53+
// was already correct there. `std::atomic::wait` on a machine with one
54+
// execution context has nothing to wait FOR; upstream's baseline polls, and
55+
// polling is the accurate implementation rather than a stand-in.
56+
//
57+
// ⚠️ THIS IS THE SAME FACT openkal-musl STATES AS `OKM_HAS_TASK`, said a second
58+
// time because there is now a second consumer. A manifest that could name the
59+
// interfaces an implementation provides would let both read one declaration;
60+
// today each states it for itself.
61+
#if defined(_LIBCPP_HAS_MUSL_LIBC) && OPENKAL_HAS_TASK
62+
63+
// No headers: openkal's are reached through the declarations below, and the two
64+
// entry points are declared where they are used.
65+
66+
#elif defined(__linux__)
67+
// ─── openkal ─── END ──────────────────────────────────────────────────────────
2368

2469
# include <linux/futex.h>
2570
# include <sys/syscall.h>
@@ -66,7 +111,60 @@ _LIBCPP_BEGIN_NAMESPACE_STD
66111

67112
struct NoTimeout {};
68113

69-
#ifdef __linux__
114+
// ─── openkal ─── BEGIN ────────────────────────────────────────────────────────
115+
#if defined(_LIBCPP_HAS_MUSL_LIBC) && OPENKAL_HAS_TASK
116+
117+
// ⚠️ A name for it. `__UINTPTR_TYPE__` expands to several tokens
118+
// (`long unsigned int`), so it cannot be written as a function-style cast —
119+
// the compiler reports `expected '(' for function-style cast`, which does not
120+
// mention the macro. One alias, used everywhere below.
121+
using __okl_uptr = __UINTPTR_TYPE__;
122+
123+
extern "C" {
124+
// Declared rather than included: openkal's headers are C, this file is C++ in
125+
// libc++'s own namespace, and two declarations are cheaper than arranging for a
126+
// header to be reachable from inside `_LIBCPP_BEGIN_NAMESPACE_STD`.
127+
//
128+
// ⚠️ The widths are openkal's own (`kal_u32`, `kal_u64`), spelled here as what
129+
// the specification says they are. openkal derives them from the compiler for
130+
// exactly this reason: a consumer can restate them without a header and cannot
131+
// get a different type than the implementation was built with.
132+
int kal_task_wait(const unsigned int* __word, unsigned int __expected, unsigned long long __timeout_ns);
133+
int kal_task_wake(const unsigned int* __word, __okl_uptr __count, __okl_uptr* __woken);
134+
}
135+
136+
template <std::size_t _Size, class MaybeTimeout>
137+
static void __platform_wait_on_address(void const* __ptr, void const* __val, MaybeTimeout maybe_timeout_ns) {
138+
static_assert(_Size == 4, "Can only wait on 4 bytes value");
139+
alignas(unsigned int) char buffer[_Size];
140+
std::memcpy(&buffer, const_cast<const void*>(__val), _Size);
141+
// ⚠️ The same two-second default the Linux branch uses, and for the same
142+
// reason: a wait that never times out cannot notice a wake it raced with.
143+
// Zero means "no timeout" to openkal, so the default is stated rather than
144+
// passed through.
145+
unsigned long long __ns = 2'000'000'000ull;
146+
if constexpr (!is_same_v<MaybeTimeout, NoTimeout>) {
147+
__ns = static_cast<unsigned long long>(maybe_timeout_ns);
148+
}
149+
kal_task_wait(reinterpret_cast<const unsigned int*>(__ptr),
150+
*reinterpret_cast<const unsigned int*>(&buffer),
151+
__ns);
152+
}
153+
154+
template <std::size_t _Size>
155+
static void __platform_wake_by_address(void const* __ptr, bool __notify_one) {
156+
static_assert(_Size == 4, "Can only wake up on 4 bytes value");
157+
// ⚠️ `-1` for "all", which is openkal's spelling of what the other branches
158+
// write as `INT_MAX`. The count is a `kal_uintptr`, so the maximum is stated
159+
// as the wrap rather than as a header's constant.
160+
__okl_uptr __woken = 0;
161+
kal_task_wake(reinterpret_cast<const unsigned int*>(__ptr),
162+
__notify_one ? __okl_uptr(1) : __okl_uptr(-1),
163+
&__woken);
164+
}
165+
166+
#elif defined(__linux__)
167+
// ─── openkal ─── END ──────────────────────────────────────────────────────────
70168

71169
template <std::size_t _Size, class MaybeTimeout>
72170
static void __platform_wait_on_address(void const* __ptr, void const* __val, MaybeTimeout maybe_timeout_ns) {

mcpp.toml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,11 @@ include_dirs = ["llvm-generated/freestanding"]
288288
# The symbols are `__eh_frame_start` / `__eh_frame_end` (and the `_hdr_`
289289
# pair), and AddressSpace.hpp carries the linker script fragment that defines
290290
# them. examples/same-source/link.ld is that fragment.
291-
cxxflags = ["-D_LIBUNWIND_IS_BAREMETAL=1"]
291+
# ⚠️ `-U` then `-D`: the package-wide list above already defined it, and a
292+
# second `-D` of a different value is a redefinition warning rather than an
293+
# override.
294+
cxxflags = ["-D_LIBUNWIND_IS_BAREMETAL=1", "-UOPENKAL_HAS_TASK", "-DOPENKAL_HAS_TASK=0"]
295+
cflags = ["-UOPENKAL_HAS_TASK", "-DOPENKAL_HAS_TASK=0"]
292296
# ⚠️ THE FLAG BELONGS TO THIS PACKAGE AND LANDS ON THE CONSUMER'S LINK.
293297
#
294298
# `_LIBUNWIND_IS_BAREMETAL` above makes the unwinder read `__eh_frame_hdr_start`
@@ -380,6 +384,18 @@ cxxflags = [
380384
# macOS target then failed on `unknown type name 'Dl_info'` — the same fact,
381385
# asked again, at a place that had not been told.
382386
"-D_LIBUNWIND_USE_DLADDR=0",
387+
# ⭐ WHETHER THE IMPLEMENTATION BENEATH PROVIDES `openkal.task`.
388+
#
389+
# `llvm/libcxx/src/atomic.cpp` asks openkal for the suspension primitive
390+
# rather than asking an operating system — see llvm/PATCHES.md. An
391+
# implementation with a single execution context has none to offer, and
392+
# openkal expresses that by the interface being absent, so the C++ runtime has
393+
# to know before it is compiled.
394+
#
395+
# ⚠️ THE SAME FACT openkal-musl STATES AS `OKM_HAS_TASK`. Two consumers, two
396+
# declarations, because a manifest cannot yet read what an implementation
397+
# provides. Cleared for `cfg(os = "none")` below, exactly as musl's is.
398+
"-DOPENKAL_HAS_TASK=1",
383399
# ⭐ musl's non-standard names live behind this, and they are not optional for
384400
# libc++: the locale backend calls `vasprintf`, `strtof_l`, `strtod_l`,
385401
# `strtold_l`, and libc++abi's guard reaches `syscall`. All of them are musl's
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Which widths the platform's wait primitive serves natively.
2+
//
3+
// ⭐ THE COMPANION TO `__atomic/contention_t.h` BESIDE THIS FILE, AND BOTH ARE
4+
// NEEDED — WHICH IS THE FINDING.
5+
//
6+
// Upstream answers by operating system: 4 on Linux, 4 and 8 on Apple, 8 on
7+
// FreeBSD and Windows. `atomic.cpp` instantiates its templates at exactly these
8+
// widths, and its platform block `static_assert`s the one it can serve.
9+
//
10+
// openkal's suspension primitive is `kal_task_wait(const kal_u32*, …)` — four
11+
// bytes, on every target, because that is what the specification says it takes.
12+
//
13+
// ⚠️ SETTING THE CONTENTION TYPE ALONE DID NOT WORK, and the way it failed is
14+
// worth keeping. Measured 2026-08-23: after `__cxx_contention_t` was made
15+
// `int32_t` here, the same assertion came back unchanged —
16+
//
17+
// static assertion failed due to requirement '8UL == 4'
18+
//
19+
// — because when `_LIBCPP_ABI_ATOMIC_WAIT_NATIVE_BY_SIZE` is on, the
20+
// instantiation list is THIS macro and not `sizeof(__cxx_contention_t)`. Two
21+
// headers state one fact, and fixing the one a reader finds first leaves the
22+
// error identical. (This is the second time in this port that a fact reached
23+
// its destination through two channels; the link line was the first.)
24+
#ifndef OPENKAL_LIBCXX_ATOMIC_WAITABLE_TRAITS_OVERLAY
25+
#define OPENKAL_LIBCXX_ATOMIC_WAITABLE_TRAITS_OVERLAY
26+
27+
#include <__config>
28+
29+
#if _LIBCPP_HAS_MUSL_LIBC
30+
// The width openkal serves, stated before upstream's chain can answer by OS.
31+
# define _LIBCPP_NATIVE_PLATFORM_WAIT_SIZES(_APPLY) _APPLY(4)
32+
#endif
33+
34+
#include_next <__atomic/atomic_waitable_traits.h>
35+
36+
#endif // OPENKAL_LIBCXX_ATOMIC_WAITABLE_TRAITS_OVERLAY
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// The width `std::atomic`'s contention counter has — which is the width the
2+
// platform's wait primitive takes, and openkal's is four bytes.
3+
//
4+
// ⭐ THE SAME QUESTION AS EVERY OTHER OVERLAY HERE, AND IT IS THE ONE THAT
5+
// DECIDES WHICH `__platform_wait_on_address<N>` GETS INSTANTIATED.
6+
//
7+
// Upstream picks by operating system — `int32_t` on Linux, `int64_t` on Apple,
8+
// FreeBSD and Windows — because each of those has a wait primitive of that
9+
// width. It is not a taste: `atomic.cpp` instantiates the templates at
10+
// `sizeof(__cxx_contention_t)` and the platform block `static_assert`s the size
11+
// it can serve.
12+
//
13+
// openkal's is `kal_task_wait(const kal_u32*, kal_u32, kal_u64)` — four bytes,
14+
// on every target, because that is what the specification says the suspension
15+
// primitive takes. So the answer here follows the C library, exactly as the
16+
// locale backend and the CRT selection do.
17+
//
18+
// ⚠️ Measured 2026-08-23, after `atomic.cpp`'s platform block was replaced but
19+
// before this file existed:
20+
//
21+
// static assertion failed due to requirement '8UL == 4':
22+
// Can only wait on 4 bytes value
23+
//
24+
// — the host build passed and the other two targets did not, because on Linux
25+
// upstream had already chosen four and elsewhere it had chosen eight. The
26+
// failure named the assertion rather than the type that produced the eight.
27+
//
28+
// ⚠️ AND THIS IS AN ABI DECISION. `__cxx_contention_t` appears in the mangled
29+
// names of the four exported entry points, so a program and a libc++ that
30+
// disagree about it do not link. That is the right failure — and it is why this
31+
// belongs in a header both sides read rather than in a flag one side passes.
32+
#ifndef OPENKAL_LIBCXX_CONTENTION_T_OVERLAY
33+
#define OPENKAL_LIBCXX_CONTENTION_T_OVERLAY
34+
35+
#include <__config>
36+
37+
// ⚠️ THE CONDITION IS THE C LIBRARY ALONE. It used to also require
38+
// `_LIBCPP_ABI_ATOMIC_WAIT_NATIVE_BY_SIZE`, mirroring upstream's own structure
39+
// — and measured, the overlay was reached and took the other branch, because
40+
// that macro is not defined at the point this header is first pulled in. The
41+
// fact this file states is true either way: openkal's wait primitive is four
42+
// bytes wide whether or not the ABI selects per size.
43+
#if _LIBCPP_HAS_MUSL_LIBC
44+
45+
# include <__atomic/support.h>
46+
# include <cstdint>
47+
48+
_LIBCPP_BEGIN_NAMESPACE_STD
49+
using __cxx_contention_t _LIBCPP_NODEBUG = int32_t;
50+
using __cxx_atomic_contention_t _LIBCPP_NODEBUG = __cxx_atomic_impl<__cxx_contention_t>;
51+
_LIBCPP_END_NAMESPACE_STD
52+
53+
#else
54+
# include_next <__atomic/contention_t.h>
55+
#endif
56+
57+
#endif // OPENKAL_LIBCXX_CONTENTION_T_OVERLAY

0 commit comments

Comments
 (0)