From 433e88b1ee2610f1286d29b5c21e5596e66f05cc Mon Sep 17 00:00:00 2001 From: gong Date: Sat, 29 Aug 2026 10:08:21 +0800 Subject: [PATCH] feat(concurrentqueue): add compat.concurrentqueue 1.0.5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit moodycamel::ConcurrentQueue — the lock-free MPMC queue, plus its blocking sibling and a flat C API over both. Shape B (header-only): the three public headers sit at the tarball root, so `include_dirs = {"*"}` plus an anchor TU is the package; consumers write `#include ` exactly as upstream's README shows. The one compilable optional component — `c_api/`'s two extern "C" wrapper TUs — sits behind a default-off `c-api` feature (the compat.cjson `utils` precedent). Windows needs `MOODYCAMEL_STATIC` on BOTH sides: the header defaults to `__declspec(dllimport)`, which is wrong for the package's own TUs (they define the functions, and `cflags` never reaches a .cpp — hence `cxxflags`) and for a static-archive consumer (hence the feature's interface `defines`). The macro is only read under `#ifdef _WIN32`, so defining it everywhere is inert on linux/macos. Verified in the emitted compile_commands.json that all four TUs carry it. Tests: tests/examples/concurrentqueue asserts FIFO order, bulk round-trips, move-only elements, a blocking consumer that REALLY waits (empty-queue wait_dequeue_timed must burn its 50ms timeout), and a 4x4 MPMC race checked for exact-once delivery. tests/examples/concurrentqueue-c-api consumes the feature and round-trips integers through the void* C API. Both members register in the workspace manifest. Verified locally with mcpp 2026.8.27.1 (CI pins 2026.8.27.2; the release CDN times out from this host): both members green from cold; the gate is real — a TU referencing moodycamel_cq_create against the default build fails to LINK (undefined reference), and the default build's object dir holds the anchor alone. No CN mirror: no GITCODE_TOKEN on this host, so the descriptor uses the plain-string upstream url fallback and the descriptor catalog entry lands in docs/descriptor-examples.md (+ zh), per the post-#277 README layout. Co-Authored-By: Claude --- .../2026-08-29-add-concurrentqueue-plan.md | 118 ++++++++++++ docs/descriptor-examples.md | 2 +- docs/zh/descriptor-examples.md | 2 +- mcpp.toml | 2 + pkgs/c/compat.concurrentqueue.lua | 106 +++++++++++ .../examples/concurrentqueue-c-api/mcpp.toml | 15 ++ .../concurrentqueue-c-api/tests/c_api.cpp | 86 +++++++++ tests/examples/concurrentqueue/mcpp.toml | 18 ++ .../examples/concurrentqueue/tests/queue.cpp | 173 ++++++++++++++++++ 9 files changed, 520 insertions(+), 2 deletions(-) create mode 100644 .agents/docs/2026-08-29-add-concurrentqueue-plan.md create mode 100644 pkgs/c/compat.concurrentqueue.lua create mode 100644 tests/examples/concurrentqueue-c-api/mcpp.toml create mode 100644 tests/examples/concurrentqueue-c-api/tests/c_api.cpp create mode 100644 tests/examples/concurrentqueue/mcpp.toml create mode 100644 tests/examples/concurrentqueue/tests/queue.cpp diff --git a/.agents/docs/2026-08-29-add-concurrentqueue-plan.md b/.agents/docs/2026-08-29-add-concurrentqueue-plan.md new file mode 100644 index 00000000..50f5359e --- /dev/null +++ b/.agents/docs/2026-08-29-add-concurrentqueue-plan.md @@ -0,0 +1,118 @@ +# 收录 concurrentqueue 1.0.5(compat.concurrentqueue) + +日期:2026-08-29 · 分支:`feat/add-concurrentqueue` · 状态:本地验证通过 + +## 1. 来源与形态判定 + +concurrentqueue 属于来源 (a):第三方上游库,上游不提供 mcpp 支持。 + +- 上游:(Cameron Desrochers; + 即 `moodycamel::ConcurrentQueue`)。 +- 最新版本:`git ls-remote --tags` 排序后最大的是 **`v1.0.5`**(本地工作副本 checkout 在 + v1.0.5 之后 6 个 commit,未成 tag,不予采用;版本键用裸版本 `"1.0.5"`,下载 URL 保留 + 上游的 `refs/tags/v1.0.5` 拼写)。 +- License:**双许可** —— 文件头声明 Simplified BSD(BSD-2-Clause)或 Boost 软件许可 + (BSL-1.0)二选一,故 `licenses = {"BSD-2-Clause", "BSL-1.0"}`(compat.libarchive 已有 + 多值先例)。 +- 源码布局:`concurrentqueue-1.0.5/` 包一层。库本体是 tarball **根**下三个头: + `concurrentqueue.h`(核心)、`blockingconcurrentqueue.h`(阻塞变体,依赖 + `lightweightsemaphore.h`)、`internal/concurrentqueue_internal_debug.h`(调试头)。 + 另有 `c_api/`(2 个 `.cpp` + 1 个 `extern "C"` 头)、`tests/`、`benchmarks/`。 + +**形态 = B(header-only)**,叠加一个 source-gated 的 `c-api` feature。 + +## 2. 版本与下载源 + +`sha256 = 4d6368a27492d86011fde5ca0cf386dce7c49cd425aa3d9b063ca6ec373a6ef3` +(1567167 字节,连算两次一致)。GitHub 的 tag 归档字节稳定,可直接作 GLOBAL。 + +## 3. CN 镜像 + +本机**没有** `GITCODE_TOKEN`(gitcode 公开读 API 可用,但建仓/release 需要写权限)。 +按 `docs/cn-mirror.md` 的回退方案:描述符采用**纯字符串 url**(只填上游 release), +lint 的 `check_mirror_urls.lua` 对纯字符串不施加镜像约束,CN 用户回退上游源, +镜像由维护者后续补充(`mcpp-res` 组织下尚无 `concurrentqueue` 仓)。 + +## 4. 实现决策 + +### 4.1 `include_dirs = {"*"}`:公开头就在 tarball 根 + +三个公开头与 LICENSE、CMakeLists 并列在归档顶层,没有 `include/` 一层可挑 +(compat.gtl 的「只取 `*/include`」在这里无处安放),`*` 即上游 README 让你放进 +include 路径的那个目录。anchor TU 照 compat.eigen / compat.plf-hive 先例, +给 mcpp 一个可构建的 lib 目标。 + +一个需要点破的名字重叠:根下的 `concurrentqueue.h`(C++ 主头)与 +`c_api/concurrentqueue.h`(C API 头)**同名**。它不构成遮蔽危害,因为 include 路径上 +只有归档根一个目录 —— `` 永远命中前者, +`` 是唯一能命中后者的拼法,两条路径互不混淆。 + +### 4.2 `c-api` feature:包里唯一可编译的可选组件 + +feature 准则(「该组件是否为额外的可编译源码」)在本包只有一个命中:`c_api/` 的两个 +`.cpp`(把两个队列封装成 `moodycamel_cq_*` / `moodycamel_bcq_*` 平面 C 接口)。 +默认关闭,请求后编进同一个 `concurrentqueue` lib 目标(compat.cjson 的 `utils` 先例)。 + +**其余一概不收**:`tests/`、`benchmarks/` 自带 `main()`,lib 目标的对象会急切进入消费者 +链接,包里不能带 main(compat.libaio 同一推理);`unsupported` 一类纯头内容不存在 —— +这库没有纯头形式的可选项,因此没有「无从门控」的遗憾。 + +### 4.3 Windows 的 `MOODYCAMEL_STATIC`:`cxxflags` 与 feature `defines` 各管一半 + +`c_api/concurrentqueue.h` 自己挑 `MOODYCAMEL_EXPORT` 的拼法:未定义 +`MOODYCAMEL_STATIC`/`DLL_EXPORT` 时按 **DLL 客户端**处理,全部函数声明成 +`__declspec(dllimport)`。这在两边同时出错: + +- 包自身的 `.cpp` 要**定义**这些函数 —— 定义一个 dllimport 函数是硬错误; +- 静态库消费者本要链接符号,却被声明成导入。 + +于是包级 `cxxflags = { "-DMOODYCAMEL_STATIC" }` 覆盖包自身 TU(c_api 两个 TU 全是 +C++,`cflags` 到不了 —— compat.msdfgen 的教训),`c-api` feature 的 +`defines = { "MOODYCAMEL_STATIC" }` 覆盖消费者 TU(Feature System v2 的接口 define)。 +该宏只在 `#ifdef _WIN32` 内被读取,linux/macos 上处处定义等于无效操作,故不按平台拆分。 +已核对本地产出的 `compile_commands.json`:四个 TU(两个 c_api 源、anchor、消费者测试) +都带上了 `-DMOODYCAMEL_STATIC`。 + +### 4.4 Linux 侧无需任何 `-D`/`-l` + +`lightweightsemaphore.h` 在 Linux 走 POSIX `sem_t`(``),不碰 futex +syscall,也就不触发 compat.libaio 踩过的 `__STRICT_ANSI__` 藏符号问题; +`_GNU_SOURCE` 在该头里只控制一个 monotonic-clock 优化分支,不开只是退化不报错。 +`sem_*` 自 glibc 2.34 起在 libc 本体,本机验证未链 -lpthread 即通过。 + +## 5. 测试成员 + +- `tests/examples/concurrentqueue`(默认构建):单线程 FIFO、bulk 往返、ProducerToken、 + move-only 元素、阻塞队列(空队列 `wait_dequeue_timed` 须**真的等满** 50ms 才返回 + false —— 把「信号量提前 post」与「真等待」区分开的断言)、4 生产 × 4 消费的 MPMC + 竞争,逐值核验「恰好收到一次」(丢失/重复/ABA 都会表现为计数 ≠1)。 +- `tests/examples/concurrentqueue-c-api`(`features = ["c-api"]`):C API 的 + create/enqueue/try_dequeue/size_approx/destroy 与 bcq 的 wait_dequeue 顺序性; + 值以整数经 `void*` 往返,校验和必须分毫不差 —— 这正是「句柄类型装错队列」会暴露的地方。 +- 依赖一律**限定拼写** `[dependencies.compat]`(eigen 成员的推理:裸名会按请求的命名空间 + 去公开远端索引解析,静默脱离本 checkout)。 +- 两个成员都已登记进根 `mcpp.toml` 的 `[workspace].members`。 + +编写期自摆乌龙两处,恰好证明断言可失败:`wait_dequeue` 在 1.0.5 返回 `void`(只有 +`_timed` 拼写报告成败);`enqueue_bulk` 返回 `bool`(只有 `try_dequeue_bulk` 返回数量)。 + +## 6. 验证结论 + +mcpp 用**本地已有**的 2026.8.27.1(CI 锁 2026.8.27.2,差一个 patch;宿主到 +github.com 主站可达、release CDN 超时,CI 绿灯为最终裁决): + +- `mcpp test -p concurrentqueue` → `test result ok`(冷启动,自干净 target/.mcpp); +- `mcpp test -p concurrentqueue-c-api` → `test result ok`(独立重编包,feature 生效); +- **负向验证**:引用 `moodycamel_cq_create` 的 TU 链接默认构建的包对象 → + `undefined reference to moodycamel_cq_create`,证明门控真实;默认构建的包对象目录里 + 也只有 `concurrentqueue_anchor.o`,无任何 `c_api/*.o`; +- 定位/查找:`include_dirs = {"*"}` 下 `` 命中根主头, + `` 命中 C API 头,同名不冲突。 + +## 7. 与 skill 流程的两处偏差 + +- **README 未改**:#277 之后描述符目录已从 README 移入 `docs/descriptor-examples.md`, + README 只留每形态一行的示例表(该表无需新增行)。本 PR 按 #277 后的现状,把条目写进 + `docs/descriptor-examples.md` 与 `docs/zh/descriptor-examples.md` 的 + 「header-only(with features)」行。 +- **mcpp 版本**:本地 2026.8.27.1 vs CI 2026.8.27.2,见第 6 节。 diff --git a/docs/descriptor-examples.md b/docs/descriptor-examples.md index 76f07d9c..ff83d7a5 100644 --- a/docs/descriptor-examples.md +++ b/docs/descriptor-examples.md @@ -20,7 +20,7 @@ in the [root README](../README.md#reference-examples). | C++-source compat, zero-dep client + optional components | [`compat.websocket`](../pkgs/c/compat.websocket.lua) (IXWebSocket 12.0.1 — a pure RFC 6455 client compiled from upstream's `IXWEBSOCKET_SOURCES` minus the four server TUs, so the **base build has zero external dependencies**: TLS off (the OpenSSL/MbedTLS/AppleSSL TUs aren't built) and `IXWEBSOCKET_USE_ZLIB` unset, so the gzip codec compiles to a no-op. Two optional features add on top: `server` (the four server TUs — `IXWebSocketServer`, `IXSocketServer`, `IXHttpServer`, `IXWebSocketProxyServer` — needing nothing external, and it **implies `zlib`** because upstream's server advertises permessage-deflate by default, which the transport negotiates regardless of the define) and `zlib` (deps `compat.zlib` and turns the codec into real per-message-deflate compression). The default-feature test brings its own minimal RFC 6455 echo server on loopback sockets (handshake, masking, fragmentation and close all exercised offline); a second member, `websocket-features`, runs a real `ix::WebSocketServer` and asserts the compression is observable on the wire — a 64 KiB repeated payload round-trips with `wireSize` = 80) | | Database client + the driver manager it needs, built from source | [`compat.nanodbc`](../pkgs/c/compat.nanodbc.lua) (nanodbc 2.14.0, frozen upstream — one TU over the platform ODBC driver manager. Two fixes make the four-year-old source compile and RUN here: a force-included `char_traits` shim for libc++ (the standard's own customization point, guarded on `_LIBCPP_VERSION` so libstdc++/MSVC are untouched; and note `-include` reaches C++ TUs only through `cxxflags`, never `cflags`), and a per-platform answer to the manager itself — windows links the SDK's odbc32, macOS the OS's iODBC, while linux takes `compat.unixodbc` because mcpp's runtime closure rejects a NEEDED `libodbc.so.2` that only the host has. The test asserts the manager's own diagnostics surface through the wrapper — including nanodbc's frozen off-by-one that drops the last SQL-state character) · [`compat.unixodbc`](../pkgs/c/compat.unixodbc.lua) (unixODBC 2.3.14, Shape E over A — DM + odbcinst + ini/log/lst + libltdl compiled statically into one `odbc` target, exactly upstream's libodbc.a symbol set, so the consumer carries no `libodbc.so.2` NEEDED at all. The one non-obvious piece is libtool-free ltdl wiring: `-DLTDLOPEN=libltdlc` plus a generated `lt_libltdlc_LTX_preloaded_symbols` table (reconstructed from the libtool object's relocations) registers the dlopen loader. The frozen `config.h` merges ltdl's own configure output into the top-level one — ltdl sources never read the clashing identification macros, and the merge sidesteps a quoted `-DLT_CONFIG_H` that does not survive the pipeline. Verified against the libtool build of the same tarball: identical IM002 error path and identical `lt_dlopen` behaviour) | | C-source compat, an ISA tier turned off through the GENERATED config | [`compat.libwebp`](../pkgs/c/compat.libwebp.lua) (117 TUs as five directory globs rather than a transcribed file list, and one real decision. libwebp's SSE4.1 gate is `(__SSE4_1__ || WEBP_MSC_SSE41) && (!HAVE_CONFIG_H || WEBP_HAVE_SSE41)`, and `WEBP_MSC_SSE41` keys off `_MSC_VER` **alone** — which every MSVC-ABI compiler defines, including clang, while only cl.exe lets an intrinsic be used without a target flag. Under clang the SSE4.1 sources fail on `always_inline function '_mm_shuffle_epi8' requires target feature 'ssse3'`. Upstream answers with a PER-FILE `-msse4.1`, which no descriptor field expresses; adding it package-wide would let clang emit SSE4.1 in the BASELINE TUs too, past libwebp's own runtime dispatch — a SIGILL rather than a fallback. So the package uses the other half of upstream's own mechanism: `HAVE_CONFIG_H` plus a generated `src/webp/config.h` naming SSE2 and NEON and not SSE4.1, which turns `dec_sse41.c` and friends into upstream's `WEBP_DSP_INIT_STUB` and removes the matching `VP8DspInitSSE41()` call. `src/demux` and `src/mux` are separate upstream libraries with their own public headers and stay out until something asks for them) | -| header-only (with `features`) | [`compat.eigen`](../pkgs/c/compat.eigen.lua) | +| header-only (with `features`) | [`compat.eigen`](../pkgs/c/compat.eigen.lua) · [`compat.concurrentqueue`](../pkgs/c/compat.concurrentqueue.lua) (moodycamel's lock-free MPMC queue 1.0.5 — three public headers at the tarball root, so `*` plus an anchor TU is the package. The ONE compilable optional component, `c_api/`'s two `extern "C"` wrapper TUs, sits behind a `c-api` feature, and the Windows story is why the descriptor carries `cxxflags = {"-DMOODYCAMEL_STATIC"}` beside the feature's `defines`: the header defaults to `__declspec(dllimport)` unless told otherwise, which is wrong for both the package's own TUs (they DEFINE the functions — and `cflags` would never reach a `.cpp`) and a static-archive consumer; the macro is read only inside `#ifdef _WIN32`, so defining it everywhere is inert on linux/macos. Everything else upstream ships outside the library is either header-only (nothing to gate) or carries a `main()`/benchmarks, which a lib target cannot take — its objects enter the consumer's link eagerly) | | header-only, nothing to gate | [`compat.CLI11`](../pkgs/c/compat.CLI11.lua) (a command line parser whose every definition is `CLI11_INLINE`, so the package is `*/include` plus an anchor TU. Upstream's two extras stay out: `src/Precompile.cpp` only means anything when `CLI11_COMPILE` also reaches the CONSUMER's translation units — an interface define, not a sources-only gate — and `src/modules/CLI11.cppm` is a module layer, which is a package shape of its own rather than a feature of the compat package) · [`compat.gtl`](../pkgs/c/compat.gtl.lua) (Greg's Template Library — the Swiss-table `flat_hash_map` family plus btrees and a bit_vector. `*/include` exactly, not the tarball root: `tests/` and `examples/` carry headers of their own, and naming `include/` is what upstream's INTERFACE target exposes, so a consumer cannot accidentally resolve into test code) · [`compat.plf-hive`](../pkgs/c/compat.plf-hive.lua) (the reference implementation of the proposed `std::hive`; the whole library is one file at the tarball root, so `*` plus an anchor TU is the entire package. Untagged upstream, so the version is a DATE over a commit archive — the compat.khrplatform precedent) · [`compat.wil`](../pkgs/c/compat.wil.lua) (the Windows Implementation Library — RAII over Win32 handles, COM pointers and HRESULT. Windows-ONLY in an unusual sense: not a portable library with a Windows backend, but a library ABOUT Win32, so there are no other platform sections to declare and consumers gate the dependency with `[target.'cfg(windows)'.dependencies]` — compat.x11 and the gui-stack member in the other direction. Nothing is pre-configured: WIL's knobs (`WIL_ENABLE_EXCEPTIONS`, `RESULT_DIAGNOSTICS_LEVEL`, `WIL_USE_STL`) are macros the CONSUMER defines before including, and a header-only package has no compiled artifact for such a choice to be baked into anyway — pre-setting one would pick an error model on its consumers' behalf) | | single-header library + a GENERATED implementation TU | [`compat.nanosvg`](../pkgs/c/compat.nanosvg.lua) (two stb-style headers where the implementation hides behind `NANOSVG_IMPLEMENTATION` / `NANOSVGRAST_IMPLEMENTATION`. Upstream ships no `.c` — its examples define the macros inline — so the package generates one that instantiates BOTH halves once. That is what turns a header drop into something linkable, and it moves the duplicate-symbol hazard from every consumer to a single place: consumers must NOT define those macros again, and the test links `nsvgParse` and `nsvgRasterize` together precisely so a package that instantiated only one half fails here rather than downstream) · [`compat.vulkan-memory-allocator`](../pkgs/c/compat.vulkan-memory-allocator.lua) (VMA 3.4.0, same shape but the generated TU also has to make a POLICY choice. VMA defaults to `VMA_STATIC_VULKAN_FUNCTIONS 1`, which references `vkBindBufferMemory2` and seven siblings by name — eight undefined symbols against a headers-only dep. Pulling `compat.vulkan` to satisfy them would force a Vulkan loader on every consumer of a memory allocator and fight anyone dispatching through volk, so the generated TU selects the dynamic path instead and VMA resolves everything through `VmaVulkanFunctions`. Note the implementation is C++ despite the C-shaped API, so the generated file is `.cpp`) | | Runtime loader compat (pure sources, sidestepping upstream codegen/asm) | [`compat.vulkan`](../pkgs/c/compat.vulkan.lua) (the Khronos loader: `loader/generated/` is checked in, and the assembly path degrades to plain C through `UNKNOWN_FUNCTIONS_SUPPORTED`, so no CMake/Python/assembler is needed; windows deferred) · [`compat.vulkan-headers`](../pkgs/c/compat.vulkan-headers.lua) | diff --git a/docs/zh/descriptor-examples.md b/docs/zh/descriptor-examples.md index aedf1438..7680309e 100644 --- a/docs/zh/descriptor-examples.md +++ b/docs/zh/descriptor-examples.md @@ -18,7 +18,7 @@ | C++ 源码 compat(零依赖客户端 + 可选组件) | [`compat.websocket`](../../pkgs/c/compat.websocket.lua)(IXWebSocket 12.0.1 —— 从上游 `IXWEBSOCKET_SOURCES` 剔掉 4 个 server TU 后直编的纯 RFC 6455 客户端,**基座零外部依赖**:TLS 关闭(OpenSSL/MbedTLS/AppleSSL 三组 TU 均不编),`IXWEBSOCKET_USE_ZLIB` 不定义(gzip codec 编译为 no-op)。两个可选 feature 在基座上叠加:`server`(4 个 server TU —— `IXWebSocketServer`/`IXSocketServer`/`IXHttpServer`/`IXWebSocketProxyServer`,零新增外部依赖,且 **implies `zlib`** —— 因为上游 server 默认就宣称 permessage-deflate,而 transport 的协商不受宏门控)与 `zlib`(依赖 `compat.zlib`,把 codec 变成真正的 permessage-deflate 压缩)。默认构建的测试自带基于 loopback 原始 socket 的最小 RFC 6455 echo server(握手/掩码/分片/关闭全部离线实测);第二个成员 `websocket-features` 跑真实的 `ix::WebSocketServer`,并断言压缩在线路上可观测 —— 64 KiB 重复载荷往返,`wireSize` = 80) | | 数据库客户端 + 源码构建的驱动管理器 | [`compat.nanodbc`](../../pkgs/c/compat.nanodbc.lua)(nanodbc 2.14.0,上游已冻结 —— 单 TU 封装平台 ODBC 驱动管理器。两处修复让这份四年前的源码在此可编译、可运行:一个 force-include 的 `char_traits` 补丁头(标准留给用户的定制点,以 `_LIBCPP_VERSION` 为界,不影响 libstdc++/MSVC;注意 `-include` 只能经 `cxxflags` 到达 C++ TU,`cflags` 够不着),以及对驱动管理器本身的分平台答案 —— windows 链 SDK 的 odbc32、macOS 链系统自带的 iODBC,linux 则依赖 `compat.unixodbc`,因为 mcpp 的运行时闭包检查不接受只有宿主才有的 `libodbc.so.2`。测试断言管理器自身的诊断能穿过封装层 —— 包括 nanodbc 已冻结的、会把 SQL state 末字符截掉的 off-by-one)· [`compat.unixodbc`](../../pkgs/c/compat.unixodbc.lua)(unixODBC 2.3.14,E 叠 A 形态 —— DM + odbcinst + ini/log/lst + libltdl 静态编入单一 `odbc` 目标,与上游 libodbc.a 符号集一致,消费者不带任何 `libodbc.so.2` NEEDED。唯一非常规之处是无 libtool 的 ltdl 接线:`-DLTDLOPEN=libltdlc` 加一张生成的 `lt_libltdlc_LTX_preloaded_symbols` 表(从 libtool 目标文件的重定位记录还原)注册 dlopen loader。冻结的 `config.h` 把 ltdl 自己的 configure 输出合并进顶层(冲突宏 ltdl 源码并不读),绕开了无法在管道中幸存的带引号 `-DLT_CONFIG_H`。已与同 tarball 的 libtool 构建对比验证:IM002 错误路径与 `lt_dlopen` 行为完全一致) | | C 源码 compat(用**生成的 config** 关掉一档 ISA) | [`compat.libwebp`](../../pkgs/c/compat.libwebp.lua)(117 个 TU 用五条目录通配写完,外加一个真实的取舍。libwebp 的 SSE4.1 门是 `(__SSE4_1__ || WEBP_MSC_SSE41) && (!HAVE_CONFIG_H || WEBP_HAVE_SSE41)`,而 `WEBP_MSC_SSE41` **只**看 `_MSC_VER` —— 每个 MSVC ABI 编译器都定义它,clang 也不例外,但只有 cl.exe 允许不带 target flag 使用任意 intrinsic。于是 clang 下 SSE4.1 那批源码报 `always_inline function '_mm_shuffle_epi8' requires target feature 'ssse3'`。上游用 **per-file** `-msse4.1` 解决,而描述符没有这个字段;整包加上去,clang 就会在**基线** TU 里也发 SSE4.1,绕过 libwebp 自己的运行期分发 —— 那是 SIGILL 而不是回退。所以本包用上游同一套机制的另一半:`HAVE_CONFIG_H` + 生成的 `src/webp/config.h`,只声明 SSE2 与 NEON、不声明 SSE4.1,`dec_sse41.c` 等随之变成上游的 `WEBP_DSP_INIT_STUB`,对应的 `VP8DspInitSSE41()` 调用点也一并消失。`src/demux` / `src/mux` 是上游各自独立、各带公开头文件的库,在有人需要之前不收) | -| header-only(含 `features`) | [`compat.eigen`](../../pkgs/c/compat.eigen.lua) | +| header-only(含 `features`) | [`compat.eigen`](../../pkgs/c/compat.eigen.lua) · [`compat.concurrentqueue`](../../pkgs/c/compat.concurrentqueue.lua)(moodycamel 的无锁 MPMC 队列 1.0.5 —— 三个公开头都在 tarball 根下,故整包即 `*` 加一个 anchor TU。唯一可编译的可选组件是 `c_api/` 的两个 `extern "C"` 封装 TU,放进 `c-api` feature;而描述符里为何在 feature 的 `defines` 之外还写了 `cxxflags = {"-DMOODYCAMEL_STATIC"}`,原因在 Windows:该头在未被告知时默认按 `__declspec(dllimport)` 声明,这对包自身的 TU(它们要**定义**这些函数,而且 `cflags` 根本到不了 `.cpp`)和静态库消费者都不成立;这个宏只在 `#ifdef _WIN32` 内被读取,因此在 linux/macos 上处处定义等于无效操作。上游库目标之外的其余内容,要么是纯头文件(无从门控),要么自带 `main()`/基准程序 —— lib 目标吃不下,因为它的对象会急切进入消费者链接) | | header-only(无可门控组件) | [`compat.CLI11`](../../pkgs/c/compat.CLI11.lua)(命令行解析器,全部定义都是 `CLI11_INLINE`,故整包就是 `*/include` 加一个 anchor TU。上游两个额外件都不收:`src/Precompile.cpp` 只有在 `CLI11_COMPILE` 同时到达**消费者** TU 时才有意义 —— 那是 interface define,不是 sources 门控;`src/modules/CLI11.cppm` 属于模块层,是另一种包形态,而非 compat 包的 feature) · [`compat.gtl`](../../pkgs/c/compat.gtl.lua)(Greg's Template Library —— Swiss-table 的 `flat_hash_map` 家族,外加 btree 与 bit_vector。只取 `*/include` 而非 tarball 根:`tests/` 与 `examples/` 各自带头文件,而 `include/` 正是上游 INTERFACE target 暴露的范围,消费者不会误解析到测试代码) · [`compat.plf-hive`](../../pkgs/c/compat.plf-hive.lua)(提案中 `std::hive` 的参考实现;整库就是 tarball 根下一个文件,故全包即 `*` 加一个 anchor TU。上游不打 tag,于是版本用 commit 归档上的日期 —— 沿用 compat.khrplatform 的先例) · [`compat.wil`](../../pkgs/c/compat.wil.lua)(Windows Implementation Library —— Win32 句柄、COM 指针与 HRESULT 的 RAII 封装。它的「仅 Windows」性质与众不同:不是带 Windows 后端的可移植库,而是一个**关于** Win32 的库,因此没有别的平台段可声明,消费者用 `[target.'cfg(windows)'.dependencies]` 门控 —— 与 compat.x11 及 gui-stack 成员正好互为镜像。什么都不预设:WIL 的开关(`WIL_ENABLE_EXCEPTIONS`、`RESULT_DIAGNOSTICS_LEVEL`、`WIL_USE_STL`)都是**消费者**在 include 之前定义的宏,而 header-only 包根本没有可以把这个选择烤进去的产物 ——预设任何一个都等于替消费者选定了错误模型) | | 单头库 + **生成**实现 TU | [`compat.nanosvg`](../../pkgs/c/compat.nanosvg.lua)(两个 stb 风格头文件,实现藏在 `NANOSVG_IMPLEMENTATION` / `NANOSVGRAST_IMPLEMENTATION` 之后。上游不提供 `.c` —— 其示例是就地 define 宏 —— 故本包生成一个,把两半各实例化一次。这才让「一堆头文件」变成可链接的包,并把重复符号的风险从每个消费者收敛到唯一一处:消费者不得再次定义这两个宏。测试刻意同时链接 `nsvgParse` 与 `nsvgRasterize`,使「只实例化了一半」的包在此处就失败,而不是流到下游)· [`compat.vulkan-memory-allocator`](../../pkgs/c/compat.vulkan-memory-allocator.lua)(VMA 3.4.0,同一形态,但生成的 TU 还得做一个**策略**选择。VMA 默认 `VMA_STATIC_VULKAN_FUNCTIONS 1`,会按名字引用 `vkBindBufferMemory2` 等八个符号 —— 对「只依赖头文件」而言就是八个未定义引用。为此拉入 `compat.vulkan` 是错的:那会逼所有用内存分配器的消费者都链上 Vulkan loader,并与通过 volk 自行分发的项目冲突。故生成的 TU 改走动态路径,VMA 一律经 `VmaVulkanFunctions` 解析。注意其 API 形如 C 但实现是 C++,故生成文件为 `.cpp`)| | 运行时 loader compat(纯源码,绕开上游 codegen/asm) | [`compat.vulkan`](../../pkgs/c/compat.vulkan.lua)(Khronos loader:`loader/generated/` 已签入,汇编路径经 `UNKNOWN_FUNCTIONS_SUPPORTED` 降级为纯 C,故无需 CMake/Python/汇编器;windows 延后)· [`compat.vulkan-headers`](../../pkgs/c/compat.vulkan-headers.lua) | diff --git a/mcpp.toml b/mcpp.toml index 7d48a991..6178478d 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -28,6 +28,8 @@ members = [ "tests/examples/cjson", "tests/examples/cmdline", "tests/examples/cli11", + "tests/examples/concurrentqueue", + "tests/examples/concurrentqueue-c-api", "tests/examples/core", "tests/examples/curl", "tests/examples/eigen", diff --git a/pkgs/c/compat.concurrentqueue.lua b/pkgs/c/compat.concurrentqueue.lua new file mode 100644 index 00000000..812cf938 --- /dev/null +++ b/pkgs/c/compat.concurrentqueue.lua @@ -0,0 +1,106 @@ +-- compat.concurrentqueue — moodycamel::ConcurrentQueue 1.0.5, Cameron +-- Desrochers' lock-free multi-producer/multi-consumer queue, plus its blocking +-- sibling (BlockingConcurrentQueue, on top of a lightweight semaphore) and the +-- flat C API over both. +-- +-- Shape B (header-only), same as compat.gtl / compat.plf-hive: the entire +-- library is `concurrentqueue.h`, `blockingconcurrentqueue.h` and +-- `lightweightsemaphore.h` at the TARBALL ROOT, so `include_dirs = {"*"}` (the +-- `*` absorbs the archive's `concurrentqueue-/` wrap layer) and a trivial +-- anchor TU are the whole package. Consumers write `#include +-- ` exactly as upstream's README shows. On Linux the +-- blocking queue's semaphore is POSIX `sem_t` (``), so nothing +-- beyond a C++ compiler is needed — no `_GNU_SOURCE`, no -lpthread dance of +-- its own. +-- +-- The one genuinely optional COMPILABLE piece is `c_api/` — two .cpp TUs that +-- wrap the queues behind an `extern "C"` surface +-- (moodycamel_cq_create/enqueue/…). That is exactly the sources-only gate the +-- feature table is for (the compat.cjson `utils` precedent), and it is OFF by +-- default: a consumer that only wants the C++ header does not pay for two +-- TUs. Everything else that upstream leaves out of its library target is +-- header-only (nothing to gate) or carries its own main()/benchmarks (a lib +-- target's objects enter the consumer's link eagerly, so a main() shipped in +-- a package collides with the consumer's — the compat.libaio reasoning). +-- +-- WINDOWS AND THE C API. `c_api/concurrentqueue.h` picks its `MOODYCAMEL_EXPORT` +-- spelling on its own: without MOODYCAMEL_STATIC or DLL_EXPORT it assumes a +-- DLL client and declares every function `__declspec(dllimport)`, which is +-- wrong twice here — the package's own .cpp would DEFINE a dllimport function, +-- and a static-archive consumer would try to import what it is about to link. +-- So this package declares MOODYCAMEL_STATIC for both sides: `cxxflags` covers +-- the package's TUs (all two of them are C++; `cflags` would never reach them +-- — the compat.msdfgen lesson), and the feature's `defines` (Feature System +-- v2) covers the consumer's. The macro is only read inside `#ifdef _WIN32`, so +-- defining it unconditionally is inert on linux/macos. +package = { + spec = "1", + namespace = "compat", + name = "concurrentqueue", + description = "moodycamel::ConcurrentQueue — lock-free MPMC queue (header-only)", + licenses = {"BSD-2-Clause", "BSL-1.0"}, -- dual-licensed upstream: Simplified BSD or Boost + repo = "https://github.com/cameron314/concurrentqueue", + type = "package", + + xpm = { + linux = { + ["1.0.5"] = { + url = "https://github.com/cameron314/concurrentqueue/archive/refs/tags/v1.0.5.tar.gz", + sha256 = "4d6368a27492d86011fde5ca0cf386dce7c49cd425aa3d9b063ca6ec373a6ef3", + }, + }, + macosx = { + ["1.0.5"] = { + url = "https://github.com/cameron314/concurrentqueue/archive/refs/tags/v1.0.5.tar.gz", + sha256 = "4d6368a27492d86011fde5ca0cf386dce7c49cd425aa3d9b063ca6ec373a6ef3", + }, + }, + windows = { + ["1.0.5"] = { + url = "https://github.com/cameron314/concurrentqueue/archive/refs/tags/v1.0.5.tar.gz", + sha256 = "4d6368a27492d86011fde5ca0cf386dce7c49cd425aa3d9b063ca6ec373a6ef3", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", + + -- Tarball root: the three public headers live beside the license, so + -- consumers resolve ``, `` + -- and — with the feature — ``. The root + -- `concurrentqueue.h` wins over the same-named C API header because + -- `c_api/` is NOT itself an include dir; the two are reachable only + -- under distinct paths, so the name overlap never turns into a + -- shadowing hazard. + include_dirs = { "*" }, + + -- See the header comment: the package's own C++ TUs must not see the + -- DLL-client default on Windows. + cxxflags = { "-DMOODYCAMEL_STATIC" }, + + -- Header-only: a trivial anchor TU gives mcpp a buildable lib target. + generated_files = { + ["mcpp_generated/concurrentqueue_anchor.c"] = "int mcpp_compat_concurrentqueue_anchor(void) { return 0; }\n", + }, + sources = { "mcpp_generated/concurrentqueue_anchor.c" }, + targets = { ["concurrentqueue"] = { kind = "lib" } }, + + -- The C API: upstream's two wrapper TUs, compiled into the same lib + -- target when requested. OFF by default — verified that a consumer + -- referencing moodycamel_cq_* without the feature fails to LINK + -- (undefined reference), so the gate is real. + features = { + ["c-api"] = { + sources = { "*/c_api/*.cpp" }, + -- Consumer side of the MOODYCAMEL_STATIC story above: without + -- it, Windows consumers inherit the header's dllimport default + -- and fail at link for a library they are statically linking. + defines = { "MOODYCAMEL_STATIC" }, + }, + }, + deps = { }, + }, +} diff --git a/tests/examples/concurrentqueue-c-api/mcpp.toml b/tests/examples/concurrentqueue-c-api/mcpp.toml new file mode 100644 index 00000000..ffccad13 --- /dev/null +++ b/tests/examples/concurrentqueue-c-api/mcpp.toml @@ -0,0 +1,15 @@ +# concurrentqueue C-API test project: the OTHER half of the package — the +# `c-api` feature. The base member (tests/examples/concurrentqueue) consumes +# the default build, which does NOT compile c_api/*.cpp; this one asks for the +# feature in the long form and asserts the moodycamel_cq_* / moodycamel_bcq_* +# surface actually resolves and behaves, which is what proves the gate both +# compiles and links what it names. +# +# Same QUALIFIED-dependency reasoning as tests/examples/concurrentqueue: the +# request must resolve from this checkout's index. +[package] +name = "concurrentqueue-c-api-tests" +version = "0.1.0" + +[dependencies.compat] +concurrentqueue = { version = "1.0.5", features = ["c-api"] } diff --git a/tests/examples/concurrentqueue-c-api/tests/c_api.cpp b/tests/examples/concurrentqueue-c-api/tests/c_api.cpp new file mode 100644 index 00000000..c12e33ca --- /dev/null +++ b/tests/examples/concurrentqueue-c-api/tests/c_api.cpp @@ -0,0 +1,86 @@ +// compat.concurrentqueue `c-api` feature — drive the flat extern "C" surface +// over both queues. Values cross the boundary as void*, so the test round-trips +// integers through MoodycamelValue and checks them on the way out. A package +// that compiled the wrappers against the wrong queue type (cq vs bcq handles +// are both void*) would still link and swap semantics — which is exactly the +// failure these assertions catch. +#include + +#include +#include + +int main() { + bool ok = true; + auto check = [&](bool cond, const char* what) { + if (!cond) { + std::printf("FAIL: %s\n", what); + ok = false; + } + }; + + // ---- plain queue ------------------------------------------------------- + { + MoodycamelCQHandle q = nullptr; + check(moodycamel_cq_create(&q) == 1, "cq_create succeeds"); + check(q != nullptr, "cq_create yields a handle"); + check(moodycamel_cq_size_approx(q) == 0, "fresh cq reports size 0"); + + uint64_t sent = 0; + for (int i = 0; i < 100; ++i) { + uintptr_t v = 1000 + i; + sent += v; + if (moodycamel_cq_enqueue(q, (MoodycamelValue)v) != 1) { + check(false, "cq_enqueue succeeds"); + break; + } + } + check(moodycamel_cq_size_approx(q) == 100, "cq_size_approx after 100 enqueues"); + + uint64_t got = 0; + int count = 0; + MoodycamelValue v = nullptr; + while (moodycamel_cq_try_dequeue(q, &v) == 1) { + got += (uintptr_t)v; + ++count; + } + check(count == 100, "all 100 values come back out"); + check(got == sent, "values survive the void* round-trip intact"); + check(moodycamel_cq_try_dequeue(q, &v) == 0, "drained cq reports try_dequeue == 0"); + + check(moodycamel_cq_destroy(q) == 1, "cq_destroy succeeds"); + } + + // ---- blocking queue ------------------------------------------------------ + { + MoodycamelBCQHandle q = nullptr; + check(moodycamel_bcq_create(&q) == 1, "bcq_create succeeds"); + + MoodycamelValue v = nullptr; + check(moodycamel_bcq_try_dequeue(q, &v) == 0, "empty bcq reports try_dequeue == 0"); + + for (uintptr_t i = 0; i < 50; ++i) { + if (moodycamel_bcq_enqueue(q, (MoodycamelValue)i) != 1) { + check(false, "bcq_enqueue succeeds"); + break; + } + } + // wait_dequeue BLOCKS until an item is available; here the items are + // already queued, so each call returns without hanging — and in order. + uint64_t got = 0; + for (int i = 0; i < 50; ++i) { + if (moodycamel_bcq_wait_dequeue(q, &v) != 1 || (uintptr_t)v != i) { + check(false, "bcq_wait_dequeue hands back every value in order"); + break; + } + got += (uintptr_t)v; + } + check(got == 50L * 49 / 2, "bcq values sum to what went in"); + check(moodycamel_bcq_try_dequeue(q, &v) == 0, "drained bcq reports try_dequeue == 0"); + + check(moodycamel_bcq_destroy(q) == 1, "bcq_destroy succeeds"); + } + + std::printf("%s\n", ok ? "concurrentqueue c-api: all checks passed" + : "concurrentqueue c-api: FAILED"); + return ok ? 0 : 1; +} diff --git a/tests/examples/concurrentqueue/mcpp.toml b/tests/examples/concurrentqueue/mcpp.toml new file mode 100644 index 00000000..d65ddd24 --- /dev/null +++ b/tests/examples/concurrentqueue/mcpp.toml @@ -0,0 +1,18 @@ +# concurrentqueue test project: drive the C++ API of compat.concurrentqueue +# (built from source via this repo's own index) under `mcpp test`. Part of the +# mcpp-index self-referential workspace. +# +# The dependency is spelled QUALIFIED (`[dependencies.compat]`) so the request +# resolves from THIS checkout's index rather than the published remote — the +# one thing this member exists to test (same reasoning as tests/examples/eigen). +# +# What the test asserts, in descending order of "a broken package would pass +# this anyway": real concurrent delivery (4 producers / 4 consumers, every +# value received EXACTLY once), blocking semantics (a consumer actually blocks +# and times out), FIFO order, bulk ops, and the move-only element path. +[package] +name = "concurrentqueue-tests" +version = "0.1.0" + +[dependencies.compat] +concurrentqueue = "1.0.5" diff --git a/tests/examples/concurrentqueue/tests/queue.cpp b/tests/examples/concurrentqueue/tests/queue.cpp new file mode 100644 index 00000000..6be74aa8 --- /dev/null +++ b/tests/examples/concurrentqueue/tests/queue.cpp @@ -0,0 +1,173 @@ +// compat.concurrentqueue — exercise the C++ API the way the library's README +// shows it: enqueue/try_dequeue, bulk ops, a blocking consumer, and a real +// multi-producer/multi-consumer race. Every check asserts observable +// behaviour (order, exact-once delivery, timeout), so a package that links +// but miscompiles the lock-free paths still fails here. +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +int main() { + bool ok = true; + auto check = [&](bool cond, const char* what) { + if (!cond) { + std::printf("FAIL: %s\n", what); + ok = false; + } + }; + + // ---- single-threaded FIFO -------------------------------------------- + // The queue is MPMC, not ordered ACROSS producers — but a single producer + // hands its own items to the consumer in order, and that is what we hold + // it to here. + { + moodycamel::ConcurrentQueue q; + check(q.size_approx() == 0, "fresh queue reports size 0"); + for (int i = 0; i < 1000; ++i) q.enqueue(i); + check(q.size_approx() == 1000, "size_approx after 1000 enqueues"); + + int v = -1; + for (int i = 0; i < 1000; ++i) { + if (!q.try_dequeue(v) || v != i) { + check(false, "single-producer FIFO order preserved"); + break; + } + } + check(!q.try_dequeue(v), "drained queue reports try_dequeue == false"); + check(q.size_approx() == 0, "size_approx back to 0 after draining"); + } + + // ---- bulk ops --------------------------------------------------------- + { + moodycamel::ConcurrentQueue q; + std::vector in(5000); + for (int i = 0; i < 5000; ++i) in[i] = i; + // enqueue_bulk answers YES/NO (bool); only try_dequeue_bulk answers + // HOW MANY (size_t). Mixing the two up is exactly the kind of mistake + // this test exists to catch — starting with its own author's. + check(q.enqueue_bulk(in.data(), in.size()), + "enqueue_bulk reports everything enqueued"); + + std::vector out(5000, -1); + check(q.try_dequeue_bulk(out.data(), out.size()) == out.size(), + "try_dequeue_bulk reports everything dequeued"); + check(in == out, "bulk round-trip preserves contents and order"); + } + + // ---- producer token ---------------------------------------------------- + // Tokens are the documented fast path for a dedicated producer; exercise + // at least the enqueue side of them. + { + moodycamel::ConcurrentQueue q; + moodycamel::ProducerToken ptok(q); + q.enqueue(ptok, 7); + int v = 0; + check(q.try_dequeue(v) && v == 7, "token enqueue round-trips"); + } + + // ---- move-only elements ------------------------------------------------ + { + moodycamel::ConcurrentQueue> q; + q.enqueue(std::make_unique(41)); + q.enqueue(std::make_unique(42)); + std::unique_ptr p; + check(q.try_dequeue(p) && p && *p == 41, "move-only element survives the queue"); + check(q.try_dequeue(p) && p && *p == 42, "second move-only element comes out in order"); + check(!q.try_dequeue(p), "queue drains after move-only elements"); + } + + // ---- blocking consumer ------------------------------------------------- + // wait_dequeue must actually block until the producer lands an item, and + // wait_dequeue_timed on an EMPTY queue must come back false after the + // timeout — the second half is what separates "semaphore posted eagerly" + // from a real wait. + { + moodycamel::BlockingConcurrentQueue q; + + int missed = -1; + auto t0 = std::chrono::steady_clock::now(); + bool got = q.wait_dequeue_timed(missed, std::chrono::milliseconds(50)); + auto elapsed = std::chrono::steady_clock::now() - t0; + check(!got, "wait_dequeue_timed on an empty queue times out"); + check(elapsed >= std::chrono::milliseconds(45), + "the timeout was actually waited, not spun through"); + + std::thread producer([&q] { + for (int i = 0; i < 2000; ++i) q.enqueue(i); + }); + int prev = -1; + for (int i = 0; i < 2000; ++i) { + int v = -1; + // wait_dequeue returns void in 1.0.5 (only the _timed spelling + // reports success), so the assertion is on the VALUE alone: if + // the semaphore path were broken this call would either hang + // (test timeout) or hand back a value out of sequence. + q.wait_dequeue(v); + if (v != prev + 1) { + check(false, "blocking consumer receives every item in order"); + break; + } + prev = v; + } + producer.join(); + int drained = -1; + check(!q.wait_dequeue_timed(drained, std::chrono::milliseconds(50)), + "nothing left after the consumer caught up"); + } + + // ---- MPMC: exact-once delivery under contention ------------------------- + // 4 producers x 10000 unique values, 4 consumers racing them out. Every + // value must arrive EXACTLY once: a lost slot, a duplicated slot, or an + // ABA-shaped torn read all show up as a counter != 1. This is the check + // the single-threaded assertions cannot make. + { + constexpr int kProducers = 4, kConsumers = 4, kPerProducer = 10000; + constexpr int kTotal = kProducers * kPerProducer; + + moodycamel::ConcurrentQueue q; + std::vector> seen(kTotal); + for (auto& s : seen) s.store(0, std::memory_order_relaxed); + std::atomic dequeued{0}; + + std::vector producers, consumers; + for (int p = 0; p < kProducers; ++p) { + producers.emplace_back([&q, p] { + for (int i = 0; i < kPerProducer; ++i) q.enqueue(p * kPerProducer + i); + }); + } + for (int c = 0; c < kConsumers; ++c) { + consumers.emplace_back([&] { + int v; + // try_dequeue until the producers are done AND the queue is + // empty — the consumer that would otherwise spin on an empty + // queue while producers are still working is the whole point + // of a concurrent queue. + for (;;) { + while (q.try_dequeue(v)) { + seen[v].store(1, std::memory_order_relaxed); + dequeued.fetch_add(1, std::memory_order_relaxed); + } + if (dequeued.load(std::memory_order_relaxed) >= kTotal) return; + std::this_thread::yield(); + } + }); + } + for (auto& t : producers) t.join(); + for (auto& t : consumers) t.join(); + + check(dequeued.load() == kTotal, "every enqueued value was dequeued"); + int exactly_once = 0; + for (const auto& s : seen) exactly_once += (s.load() == 1) ? 1 : 0; + check(exactly_once == kTotal, "every value delivered exactly once (no loss, no duplicate)"); + } + + std::printf("%s\n", ok ? "concurrentqueue: all checks passed" : "concurrentqueue: FAILED"); + return ok ? 0 : 1; +}