diff --git a/.agents/docs/2026-08-09-add-redis-plus-plus-plan.md b/.agents/docs/2026-08-09-add-redis-plus-plus-plan.md index ca8fcd9..6fa8e94 100644 --- a/.agents/docs/2026-08-09-add-redis-plus-plus-plan.md +++ b/.agents/docs/2026-08-09-add-redis-plus-plus-plan.md @@ -93,3 +93,43 @@ include 布局(镜像上游 target_include_directories): `'uint16_t' does not name a type` —— 1.3.3 的 `utils.h` 用 `uint16_t` 却未包含 `` (1.3.6+ 才补)。包级 `cxxflags = { "-include", "cstdint" }` 修复包自身,v133 成员 `[build] cxxflags` 修复测试 TU(先包含 redis++.h);对 1.3.13 无害。 + +--- + +## 8. 追加:compat.libuv + redis-plus-plus `async` feature + +**日期**: 2026-08-09(PR #188 已合并后,独立新 PR) +**目标**:补齐 redis-plus-plus 的 libuv 异步接口(上游 `REDIS_PLUS_PLUS_BUILD_ASYNC=libuv`)。 + +### 8.1 compat.libuv 1.48.0(Form A,C 源码) + +- **版本**:1.48.0(2024-04,Ubuntu 24.04 LTS 世代、vcpkg 长期默认;redis-plus-plus 只需 "libuv 1.x")。 + SHA:`8c253adb0f800926a6cbd1c6576abae0bc8eb86a4f891049b72f9e5b7dc58f33`(两次独立下载一致)。 +- **源码清单**:转录上游 CMakeLists 的逐 OS 集合。`*/src/*.c`(12 个通用)全平台; + `src/unix/*.c` **不能**整体 glob(每 OS 一个后端文件,aix.c/linux.c/darwin.c…),故 linux/macos + 显式列子集: + - linux:18 个 unix 公共 + `proctitle.c` + `linux.c procfs-exepath.c random-getrandom.c random-sysctl-linux.c`;cflags `_FILE_OFFSET_BITS=64 _LARGEFILE_SOURCE _GNU_SOURCE _POSIX_C_SOURCE=200112`;ldflags `-lpthread -ldl -lrt`。 + - macosx:18 个 unix 公共 + `proctitle.c bsd-ifaddrs.c kqueue.c random-getentropy.c darwin-proctitle.c darwin.c fsevents.c`;cflags 加 `_DARWIN_UNLIMITED_SELECT=1 _DARWIN_USE_64_BIT_INODE=1`;ldflags `-lpthread`。 + - windows:`*/src/win/*.c`(25 个,glob 安全);cflags `WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0602 _CRT_DECLARE_NONSTDC_NAMES=0`;ldflags `psapi user32 advapi32 iphlpapi userenv ws2_32 dbghelp ole32 shell32`。 +- **include_dirs** `{ "*/include", "*/src" }`:`internal.h` 引 `"uv-common.h"`(在 `src/`),`*/src` 仅供自身编译,随依赖传播(先例:c-ares/protobuf 同样暴露 src)。 +- **c_standard** = `c99`(上游 CMake 下限 90,c99 安全超集)。本机 mac 冒烟:37/37 TU 编译、`uv_version_string` 链接运行 OK。 +- **hiredis 侧**:新增 `hiredis/adapters/libuv.h` 薄包装头(`#include `,真实头用相对 `../hiredis.h`/`../async.h`)。 + +### 8.2 redis-plus-plus `async` feature + +- `features.async` = 9 个 async TU(`async_*.cpp` + `event_loop.cpp`)+ `deps = { ["compat.libuv"] = "1.48.0" }`。 +- 1.3.3 上其中两个 glob 零命中(`async_subscriber*.cpp` 1.3.6+ 才有),子集/超集并集与同步核心同款。 +- include_dirs 追加 `*/src/sw/redis++/future/std`(`async_connection.h` 引 `"sw/redis++/async_utils.h"`;feature 不能携带 include_dirs,基座加了对 1.3.3 也无害)。 +- ``、`` 经 feature 激活后的依赖 include 传播到达。 +- **测试**:新成员 `redis-plus-plus-async`(1.3.13 + `features=["async"]`),进程内迷你 RESP server **解析完整 RESP 数组**(异步客户端会把 PING+SET 管道进同一 TCP 段),驱动 `AsyncRedis`(EventLoop 后台线程跑 `uv_run`),`.get()` 阻塞取回 `PONG`/OK。钉版 mcpp 本地通过。 +- **负向**:不启用 async 时,async 符号不存在(现有同步成员不受影响,三成员全绿)。 + +### 8.3 Windows 修复:libuv `-DNDEBUG`(上游 redis-plus-plus#575) + +**CI 现象**(PR #195 windows 腿):async 测试运行期崩溃 +`Assertion failed: 0, src/win/handle.c:71`(exit 0xC0000409)。 + +**根因**(与上游 open issue [sewenew/redis-plus-plus#575](https://github.com/sewenew/redis-plus-plus/issues/575) 同一 bug,作者仅 Windows 可复现,与我们的 linux/mac 全绿一致): +`EventLoop::LoopDeleter` 析构时 `uv_walk` 对所有 handle 调 `uv_close`,其中**已被 hiredis libuv adapter cleanup 关过的 poll/timer handle** 在 Windows 上仍留在 loop 的 handle 队列(Unix 上 closing 阶段先跑完、handle 已摘链),于是二次 `uv_close` 命中 `UV_HANDLE_CLOSING` guard 里的 `assert(0)`。libuv 的二次 close 本身有 guard 会直接 return(无害),只有断言在非 NDEBUG 构建下 abort。 + +**修复**:`compat.libuv` windows `cflags` 加 `-DNDEBUG`(与 vcpkg/conan 的 libuv release 构建一致),guard 生效、二次 close 变 no-op;不加不会改变 unix 行为(unix 不发生二次 close)。已在描述符注释中写明并指向 #575。 diff --git a/README.md b/README.md index d639f18..f187532 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ Two kinds of packages live here: | Shape | Examples | |------|------| | Native module library (Form A) | [`mcpplibs.xpkg`](pkgs/x/xpkg.lua) · [`mcpplibs.tinyhttps`](pkgs/t/tinyhttps.lua) · [`tensorvia-cpu`](pkgs/t/tensorvia-cpu.lua) · [`ffmpeg`](pkgs/f/ffmpeg.lua) (module layer; sources compiled directly through `compat.ffmpeg`) · [`opencv`](pkgs/o/opencv.lua) (single repository: the module layer and the full OpenCV 5 source build both live in the package, and only this descriptor stays on the index side) · [`mcpplibs.grpc`](pkgs/g/grpc.lua) (gRPC 1.83.0 — the one library here that CANNOT be a compat descriptor: upstream publishes no self-contained source artifact, its tag archive carrying abseil/protobuf/re2/boringssl/zlib as empty submodule placeholders, so [grpc-m](https://github.com/mcpplibs/grpc-m)'s release tarball IS that artifact. It vendors only gRPC's own source and takes the five dependencies from this index, so a consumer that also uses protobuf links one copy rather than two) | -| C-source compat (with `features`) | [`compat.cjson`](pkgs/c/compat.cjson.lua) · [`compat.zlib`](pkgs/c/compat.zlib.lua) · [`compat.hiredis`](pkgs/c/compat.hiredis.lua) (the classic 1.2.0 — a 7-TU C build whose flat tarball headers get `hiredis/`-prefixed wrapper headers via `generated_files`, so consumers write `#include ` exactly like upstream's install layout) · [`compat.sqlite3`](pkgs/c/compat.sqlite3.lua) (plain C-source, no features: the single `sqlite3.c` amalgamation; 3.45.3, the final maintenance release of the most widely deployed 3.45.x line) | -| C++-source compat, one depending on the other | [`compat.abseil`](pkgs/c/compat.abseil.lua) (151 TUs; a wildcard over `absl/**` trimmed by upstream's test/benchmark naming conventions) · [`compat.protobuf`](pkgs/c/compat.protobuf.lua) (the libprotobuf runtime, 79 TUs transcribed from upstream's own `src/file_lists.cmake`; declares `compat.abseil` as a dependency because protobuf's public headers include `absl/…`, and its `gzip` feature defines `HAVE_ZLIB` and pulls `compat.zlib`, while `upb` adds protobuf's 64-TU C runtime out of the same tarball. It also exposes **`protoc`** as a `kind = "bin"` target, so a consumer writing `tools = ["protoc"]` gets the compiler built for its own machine out of the same package it links — making a generator/runtime version mismatch inexpressible) · [`compat.re2`](pkgs/c/compat.re2.lua) (22 TUs, upstream's own `RE2_SOURCES`) · [`compat.redis-plus-plus`](pkgs/c/compat.redis-plus-plus.lua) (redis++ 1.3.13 — the sync client, 17 TUs + `patterns/redlock.cpp`, depends on `compat.hiredis`; the one header CMake would generate, `hiredis_features.h`, is snapshotted via `generated_files`, and the async/TLS TUs are left out so the base build stays a two-package pair. Two versions, one on each side of the source-structure watershed, share this ONE source list: 1.3.13 (modern 17-TU layout) and 1.3.3 (pre-`redis_uri.cpp`/`redlock` 15-TU layout) — the union works because 1.3.3's TUs are a strict subset, so exactly two globs match nothing there (a warning, not an error; same trick as compat.catch2)) | +| C-source compat (with `features`) | [`compat.cjson`](pkgs/c/compat.cjson.lua) · [`compat.zlib`](pkgs/c/compat.zlib.lua) · [`compat.hiredis`](pkgs/c/compat.hiredis.lua) (the classic 1.2.0 — a 7-TU C build whose flat tarball headers get `hiredis/`-prefixed wrapper headers via `generated_files`, so consumers write `#include ` exactly like upstream's install layout) · [`compat.sqlite3`](pkgs/c/compat.sqlite3.lua) (plain C-source, no features: the single `sqlite3.c` amalgamation; 3.45.3, the final maintenance release of the most widely deployed 3.45.x line) · [`compat.libuv`](pkgs/c/compat.libuv.lua) (libuv 1.48.0 — the per-OS source sets transcribed from upstream's CMakeLists, because a `src/unix/*.c` glob would compile every OS's backend at once; linux/macos get explicit unix subsets, windows globs `src/win/*.c`) | +| C++-source compat, one depending on the other | [`compat.abseil`](pkgs/c/compat.abseil.lua) (151 TUs; a wildcard over `absl/**` trimmed by upstream's test/benchmark naming conventions) · [`compat.protobuf`](pkgs/c/compat.protobuf.lua) (the libprotobuf runtime, 79 TUs transcribed from upstream's own `src/file_lists.cmake`; declares `compat.abseil` as a dependency because protobuf's public headers include `absl/…`, and its `gzip` feature defines `HAVE_ZLIB` and pulls `compat.zlib`, while `upb` adds protobuf's 64-TU C runtime out of the same tarball. It also exposes **`protoc`** as a `kind = "bin"` target, so a consumer writing `tools = ["protoc"]` gets the compiler built for its own machine out of the same package it links — making a generator/runtime version mismatch inexpressible) · [`compat.re2`](pkgs/c/compat.re2.lua) (22 TUs, upstream's own `RE2_SOURCES`) · [`compat.redis-plus-plus`](pkgs/c/compat.redis-plus-plus.lua) (redis++ 1.3.13 — the sync client, 17 TUs + `patterns/redlock.cpp`, depends on `compat.hiredis`; the one header CMake would generate, `hiredis_features.h`, is snapshotted via `generated_files`, and the async/TLS TUs are left out so the base build stays a two-package pair. An `async` feature adds the libuv-backed `AsyncRedis` interface (the 9 async TUs + `compat.libuv`; `event_loop.cpp` runs `uv_run` on a background thread, and `` arrives through compat.hiredis' wrapper headers). Two versions, one on each side of the source-structure watershed, share this ONE source list: 1.3.13 (modern 17-TU layout) and 1.3.3 (pre-`redis_uri.cpp`/`redlock` 15-TU layout) — the union works because 1.3.3's TUs are a strict subset, so exactly two globs match nothing there (a warning, not an error; same trick as compat.catch2)) | | 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) | | header-only (with `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) | | 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) | diff --git a/README.zh-CN.md b/README.zh-CN.md index b329f3a..bf67520 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -35,8 +35,8 @@ mcpp self config --mirror CN # 切换至国内镜像,默认使用 GLOBAL 上 | 形态 | 示例 | |------|------| | 原生模块库(Form A) | [`mcpplibs.xpkg`](pkgs/x/xpkg.lua) · [`mcpplibs.tinyhttps`](pkgs/t/tinyhttps.lua) · [`tensorvia-cpu`](pkgs/t/tensorvia-cpu.lua) · [`ffmpeg`](pkgs/f/ffmpeg.lua)(模块层,源码经 `compat.ffmpeg` 直编) · [`opencv`](pkgs/o/opencv.lua)(单仓库:模块层与 OpenCV 5 全源码构建同在包内,索引侧只留本描述符) · [`mcpplibs.grpc`](pkgs/g/grpc.lua)(gRPC 1.83.0 —— 本索引里唯一**无法**做成 compat 描述符的库:上游不发布任何自包含源码产物,其 tag 归档里 abseil/protobuf/re2/boringssl/zlib 全是空 submodule 占位,因此 [grpc-m](https://github.com/mcpplibs/grpc-m) 的 release tarball 才是那个产物。它只 vendor gRPC 自己的源码,五个依赖全取自本索引,故同时直接使用 protobuf 的消费者链进去的是同一份而非两份)| -| C 源码 compat(含 `features`) | [`compat.cjson`](pkgs/c/compat.cjson.lua) · [`compat.zlib`](pkgs/c/compat.zlib.lua) · [`compat.hiredis`](pkgs/c/compat.hiredis.lua)(经典 1.2.0 —— 7 个 C TU;tarball 平铺头经 `generated_files` 补 `hiredis/` 前缀薄包装头,消费者可写 `#include `,与上游安装布局一致) · [`compat.sqlite3`](pkgs/c/compat.sqlite3.lua)(纯 C 源码、无 feature:单一 `sqlite3.c` amalgamation;3.45.3,部署最广的 3.45.x 线最后一个维护版) | -| C++ 源码 compat(彼此依赖) | [`compat.abseil`](pkgs/c/compat.abseil.lua)(151 TU;对 `absl/**` 取通配后,按上游自身的 test/benchmark 命名约定裁剪) · [`compat.protobuf`](pkgs/c/compat.protobuf.lua)(libprotobuf 运行时,79 TU 逐条转录自上游 `src/file_lists.cmake`;因 protobuf 公开头文件 include 了 `absl/…`,故显式依赖 `compat.abseil`;`gzip` feature 定义 `HAVE_ZLIB` 并拉入 `compat.zlib`,`upb` feature 则从同一个 tarball 里再编出 protobuf 的 64 TU C 运行时;还以 `kind = "bin"` target 暴露 **`protoc`**,消费者写 `tools = ["protoc"]` 即可从「自己链接的那个包」拿到为本机构建的编译器,使生成器与运行时的版本错配无法表达) · [`compat.re2`](pkgs/c/compat.re2.lua)(22 TU,取自上游自身的 `RE2_SOURCES`) · [`compat.redis-plus-plus`](pkgs/c/compat.redis-plus-plus.lua)(redis++ 1.3.13 —— 同步客户端,17 TU + `patterns/redlock.cpp`,依赖 `compat.hiredis`;CMake 唯一会生成的头 `hiredis_features.h` 用 `generated_files` 快照,async/TLS TU 不收,基座保持两包成对。两个版本分处源码结构分水岭两侧,共享同一份源列表:1.3.13(现代 17-TU 布局)与 1.3.3(缺 `redis_uri.cpp`/`redlock` 的 15-TU 旧布局)—— 并集之所以成立,是因为 1.3.3 的 TU 是 1.3.13 的严格子集,恰好两个 glob 在 1.3.3 上零命中(仅警告,非错误;与 compat.catch2 同款手法)) | +| C 源码 compat(含 `features`) | [`compat.cjson`](pkgs/c/compat.cjson.lua) · [`compat.zlib`](pkgs/c/compat.zlib.lua) · [`compat.hiredis`](pkgs/c/compat.hiredis.lua)(经典 1.2.0 —— 7 个 C TU;tarball 平铺头经 `generated_files` 补 `hiredis/` 前缀薄包装头,消费者可写 `#include `,与上游安装布局一致) · [`compat.sqlite3`](pkgs/c/compat.sqlite3.lua)(纯 C 源码、无 feature:单一 `sqlite3.c` amalgamation;3.45.3,部署最广的 3.45.x 线) · [`compat.libuv`](pkgs/c/compat.libuv.lua)(libuv 1.48.0 —— 逐 OS 源清单转录自上游 CMakeLists,因为 `src/unix/*.c` 通配会一次编进所有 OS 的后端;linux/macos 显式列 unix 子集,windows 用 `src/win/*.c` glob) | +| C++ 源码 compat(彼此依赖) | [`compat.abseil`](pkgs/c/compat.abseil.lua)(151 TU;对 `absl/**` 取通配后,按上游自身的 test/benchmark 命名约定裁剪) · [`compat.protobuf`](pkgs/c/compat.protobuf.lua)(libprotobuf 运行时,79 TU 逐条转录自上游 `src/file_lists.cmake`;因 protobuf 公开头文件 include 了 `absl/…`,故显式依赖 `compat.abseil`;`gzip` feature 定义 `HAVE_ZLIB` 并拉入 `compat.zlib`,`upb` feature 则从同一个 tarball 里再编出 protobuf 的 64 TU C 运行时;还以 `kind = "bin"` target 暴露 **`protoc`**,消费者写 `tools = ["protoc"]` 即可从「自己链接的那个包」拿到为本机构建的编译器,使生成器与运行时的版本错配无法表达) · [`compat.re2`](pkgs/c/compat.re2.lua)(22 TU,取自上游自身的 `RE2_SOURCES`) · [`compat.redis-plus-plus`](pkgs/c/compat.redis-plus-plus.lua)(redis++ 1.3.13 —— 同步客户端,17 TU + `patterns/redlock.cpp`,依赖 `compat.hiredis`;CMake 唯一会生成的头 `hiredis_features.h` 用 `generated_files` 快照,async/TLS TU 不收,基座保持两包成对。`async` feature 补齐 libuv 版 `AsyncRedis` 接口(9 个 async TU + `compat.libuv`;`event_loop.cpp` 在后台线程跑 `uv_run`,`` 经 compat.hiredis 的包装头到达)。两个版本分处源码结构分水岭两侧,共享同一份源列表:1.3.13(现代 17-TU 布局)与 1.3.3(缺 `redis_uri.cpp`/`redlock` 的 15-TU 旧布局)—— 并集之所以成立,是因为 1.3.3 的 TU 是 1.3.13 的严格子集,恰好两个 glob 在 1.3.3 上零命中(仅警告,非错误;与 compat.catch2 同款手法)) | | 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) | | header-only(含 `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) | | 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) | diff --git a/mcpp.toml b/mcpp.toml index 945fffc..286687f 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -59,6 +59,7 @@ members = [ "tests/examples/re2", "tests/examples/redis-plus-plus", "tests/examples/redis-plus-plus-v133", + "tests/examples/redis-plus-plus-async", "tests/examples/sdl2", "tests/examples/spdlog", "tests/examples/freetype", diff --git a/pkgs/c/compat.hiredis.lua b/pkgs/c/compat.hiredis.lua index 0620ea3..92cd1f7 100644 --- a/pkgs/c/compat.hiredis.lua +++ b/pkgs/c/compat.hiredis.lua @@ -88,6 +88,11 @@ package = { generated_files = { ["mcpp_generated/include/hiredis/hiredis.h"] = "#pragma once\n#include \n", ["mcpp_generated/include/hiredis/async.h"] = "#pragma once\n#include \n", + -- redis-plus-plus's async build (event_loop.cpp) includes + -- ; the real header uses relative + -- "../hiredis.h" / "../async.h", which resolve next to it in the + -- wrap dir. Same thin-wrapper trick as the two above. + ["mcpp_generated/include/hiredis/adapters/libuv.h"] = "#pragma once\n#include \n", }, windows = { diff --git a/pkgs/c/compat.libuv.lua b/pkgs/c/compat.libuv.lua new file mode 100644 index 0000000..d7707a7 --- /dev/null +++ b/pkgs/c/compat.libuv.lua @@ -0,0 +1,115 @@ +-- compat.libuv — libuv 1.48.0, multi-platform async I/O library. +-- +-- Shape A (C-source compat): the exact per-OS source sets from upstream's own +-- CMakeLists (`uv_sources` + the platform blocks), listed explicitly because +-- `src/unix/*.c` is NOT a safe glob — it holds one file per OS (aix.c, linux.c, +-- darwin.c, …) and compiling a foreign one fails. The 12 common files in +-- `src/*.c` are shared by every platform; each platform block adds its own. +-- `*/src` is on the include path for the package's own compile because +-- src/unix/internal.h does `#include "uv-common.h"` (lives in src/). +-- +-- VERSION. 1.48.0 (2024-04) is the classic, widely-shipped release — it is +-- the libuv of the Ubuntu 24.04 LTS line and was the long-standing vcpkg +-- default before the 2025 releases; redis-plus-plus's async interface only +-- needs "libuv 1.x", so the battle-tested LTS-era pick is the right one. +-- +-- Windows: link libs and defines mirror upstream CMakeLists verbatim +-- (psapi user32 advapi32 iphlpapi userenv ws2_32 dbghelp ole32 shell32). +-- +-- No CN mirror yet: plain-string upstream URL (see compat.hiredis). +package = { + spec = "1", + namespace = "compat", + name = "libuv", + description = "Multi-platform asynchronous I/O library (static, upstream per-OS source sets)", + licenses = {"MIT"}, + repo = "https://github.com/libuv/libuv", + type = "package", + + xpm = { + linux = { + ["1.48.0"] = { + url = "https://github.com/libuv/libuv/archive/refs/tags/v1.48.0.tar.gz", + sha256 = "8c253adb0f800926a6cbd1c6576abae0bc8eb86a4f891049b72f9e5b7dc58f33", + }, + }, + macosx = { + ["1.48.0"] = { + url = "https://github.com/libuv/libuv/archive/refs/tags/v1.48.0.tar.gz", + sha256 = "8c253adb0f800926a6cbd1c6576abae0bc8eb86a4f891049b72f9e5b7dc58f33", + }, + }, + windows = { + ["1.48.0"] = { + url = "https://github.com/libuv/libuv/archive/refs/tags/v1.48.0.tar.gz", + sha256 = "8c253adb0f800926a6cbd1c6576abae0bc8eb86a4f891049b72f9e5b7dc58f33", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c99", -- upstream CMake's C_STANDARD floor is 90; c99 is a safe superset + + include_dirs = { "*/include", "*/src" }, + + -- Common 12 TUs (upstream `uv_sources`), every platform. + sources = { "*/src/*.c" }, + + targets = { ["uv"] = { kind = "lib" } }, + deps = {}, + + -- Linux: 18 unix TUs + proctitle.c + the linux block. Defines and link + -- libs mirror upstream CMakeLists' Linux block. + linux = { + sources = { + "*/src/unix/async.c", "*/src/unix/core.c", "*/src/unix/dl.c", + "*/src/unix/fs.c", "*/src/unix/getaddrinfo.c", "*/src/unix/getnameinfo.c", + "*/src/unix/loop-watcher.c", "*/src/unix/loop.c", "*/src/unix/pipe.c", + "*/src/unix/poll.c", "*/src/unix/process.c", "*/src/unix/random-devurandom.c", + "*/src/unix/signal.c", "*/src/unix/stream.c", "*/src/unix/tcp.c", + "*/src/unix/thread.c", "*/src/unix/tty.c", "*/src/unix/udp.c", + "*/src/unix/proctitle.c", + "*/src/unix/linux.c", "*/src/unix/procfs-exepath.c", + "*/src/unix/random-getrandom.c", "*/src/unix/random-sysctl-linux.c", + }, + cflags = { "-D_FILE_OFFSET_BITS=64", "-D_LARGEFILE_SOURCE", "-D_GNU_SOURCE", "-D_POSIX_C_SOURCE=200112" }, + ldflags = { "-lpthread", "-ldl", "-lrt" }, + }, + + -- macOS: 18 unix TUs + proctitle + the APPLE-or-BSD extras + darwin set. + macosx = { + sources = { + "*/src/unix/async.c", "*/src/unix/core.c", "*/src/unix/dl.c", + "*/src/unix/fs.c", "*/src/unix/getaddrinfo.c", "*/src/unix/getnameinfo.c", + "*/src/unix/loop-watcher.c", "*/src/unix/loop.c", "*/src/unix/pipe.c", + "*/src/unix/poll.c", "*/src/unix/process.c", "*/src/unix/random-devurandom.c", + "*/src/unix/signal.c", "*/src/unix/stream.c", "*/src/unix/tcp.c", + "*/src/unix/thread.c", "*/src/unix/tty.c", "*/src/unix/udp.c", + "*/src/unix/proctitle.c", "*/src/unix/bsd-ifaddrs.c", "*/src/unix/kqueue.c", + "*/src/unix/random-getentropy.c", "*/src/unix/darwin-proctitle.c", + "*/src/unix/darwin.c", "*/src/unix/fsevents.c", + }, + cflags = { "-D_FILE_OFFSET_BITS=64", "-D_LARGEFILE_SOURCE", "-D_DARWIN_UNLIMITED_SELECT=1", "-D_DARWIN_USE_64_BIT_INODE=1" }, + ldflags = { "-lpthread" }, + }, + + -- Windows: the 25 src/win/*.c TUs (the glob is safe — every .c under + -- src/win/ is library code; tests live in test/). + windows = { + sources = { "*/src/win/*.c" }, + -- NDEBUG: upstream redis-plus-plus#575 — EventLoop::LoopDeleter's + -- uv_walk re-closes handles already uv_close'd by hiredis's libuv + -- adapter cleanup (on Windows they are still in the loop's handle + -- queue at teardown, unlike unix where the closing phase runs + -- first). uv_close has a UV_HANDLE_CLOSING guard that makes the + -- second call a harmless no-op; only the assert(0) aborts, so a + -- release-style build (NDEBUG, matching vcpkg/conan libuv) fixes + -- it. Unfixable package-side otherwise without shadowing + -- event_loop.cpp. Regression: PR #195 windows workspace leg. + cflags = { "-DWIN32_LEAN_AND_MEAN", "-D_WIN32_WINNT=0x0602", "-D_CRT_DECLARE_NONSTDC_NAMES=0", "-DNDEBUG" }, + ldflags = { "-lpsapi", "-luser32", "-ladvapi32", "-liphlpapi", "-luserenv", "-lws2_32", "-ldbghelp", "-lole32", "-lshell32" }, + }, + }, +} diff --git a/pkgs/c/compat.redis-plus-plus.lua b/pkgs/c/compat.redis-plus-plus.lua index 8b67f20..7006b8a 100644 --- a/pkgs/c/compat.redis-plus-plus.lua +++ b/pkgs/c/compat.redis-plus-plus.lua @@ -91,6 +91,7 @@ package = { "*/src", "*/src/sw/redis++/cxx17", "*/src/sw/redis++/no_tls", + "*/src/sw/redis++/future/std", -- async only: "sw/redis++/async_utils.h" "mcpp_generated", }, @@ -118,6 +119,31 @@ package = { "*/src/sw/redis++/patterns/redlock.cpp", }, + -- `async` — upstream REDIS_PLUS_PLUS_BUILD_ASYNC=libuv: the 9 async + -- TUs (event_loop.cpp runs uv_run on a background thread) plus the + -- compat.libuv dependency. For 1.3.3 exactly two of the nine globs + -- match nothing (async_subscriber*.cpp were added in 1.3.6) — the + -- same subset/superset union as the sync core. The include dirs the + -- async TUs need ( from compat.libuv, + -- from compat.hiredis) arrive through the deps' propagated include + -- dirs when this feature is active. + features = { + ["async"] = { + sources = { + "*/src/sw/redis++/async_connection.cpp", + "*/src/sw/redis++/async_connection_pool.cpp", + "*/src/sw/redis++/async_redis.cpp", + "*/src/sw/redis++/async_redis_cluster.cpp", + "*/src/sw/redis++/async_sentinel.cpp", + "*/src/sw/redis++/async_shards_pool.cpp", + "*/src/sw/redis++/async_subscriber.cpp", + "*/src/sw/redis++/async_subscriber_impl.cpp", + "*/src/sw/redis++/event_loop.cpp", + }, + deps = { ["compat.libuv"] = "1.48.0" }, + }, + }, + targets = { ["redis_plus_plus"] = { kind = "lib" } }, deps = { ["compat.hiredis"] = "1.2.0" }, diff --git a/tests/examples/redis-plus-plus-async/mcpp.toml b/tests/examples/redis-plus-plus-async/mcpp.toml new file mode 100644 index 0000000..1e0b796 --- /dev/null +++ b/tests/examples/redis-plus-plus-async/mcpp.toml @@ -0,0 +1,16 @@ +# redis-plus-plus ASYNC test project: consumes compat.redis-plus-plus with the +# `async` feature (which pulls compat.libuv) and asserts the libuv-backed async +# interface under `mcpp test`. Part of the mcpp-index self-referential +# workspace — the workspace-root `[indices]` redirect points at this repo. +# +# The test is entirely OFFLINE: same minimal RESP server on loopback sockets as +# the sync members, driven through sw::redis::AsyncRedis (which runs the libuv +# event loop on a background thread). PING + SET are fired back-to-back, so the +# server parses real RESP arrays — the async client may pipeline both commands +# into one TCP segment. +[package] +name = "redis-plus-plus-async-tests" +version = "0.1.0" + +[dependencies.compat] +redis-plus-plus = { version = "1.3.13", features = ["async"] } diff --git a/tests/examples/redis-plus-plus-async/tests/redis_test.cpp b/tests/examples/redis-plus-plus-async/tests/redis_test.cpp new file mode 100644 index 0000000..dcff10e --- /dev/null +++ b/tests/examples/redis-plus-plus-async/tests/redis_test.cpp @@ -0,0 +1,206 @@ +// redis_test.cpp — offline behavioral test for the `async` feature of +// compat.redis-plus-plus (libuv-backed AsyncRedis). +// +// No redis-server binary and no network access: a minimal RESP server runs +// inside the process (raw loopback sockets, same platform abstraction as the +// websocket member) and the sw::redis::AsyncRedis client is driven against it. +// The async client can pipeline PING + SET into a single TCP segment, so the +// server parses complete RESP arrays instead of scanning for keywords. +#include + +#include +#include +#include +#include +#include +#include +#include + +// ── Platform socket abstraction (same as tests/examples/websocket) ───────── + +#ifdef _WIN32 +#include +#include +using SockType = SOCKET; +constexpr SockType kInvalidSocket = INVALID_SOCKET; +using AddrLenType = int; +#else +#include +#include +#include +#include +#include +using SockType = int; +constexpr SockType kInvalidSocket = -1; +using AddrLenType = socklen_t; +#endif + +void init_sockets() +{ +#ifdef _WIN32 + WSADATA wsa; + WSAStartup(MAKEWORD(2, 2), &wsa); +#endif +} + +void close_socket(SockType s) +{ +#ifdef _WIN32 + closesocket(s); +#else + ::close(s); +#endif +} + +void set_recv_timeout(SockType s, int ms) +{ +#ifdef _WIN32 + DWORD t = static_cast(ms); + setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&t), sizeof(t)); +#else + timeval tv{}; + tv.tv_sec = ms / 1000; + tv.tv_usec = (ms % 1000) * 1000; + setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); +#endif +} + +// ── Minimal RESP server: PING -> +PONG, SET -> +OK ───────────────────────── +// Parses complete RESP arrays so pipelined commands (async client) are served +// one reply each instead of one reply per recv(). +class RespServer +{ +public: + ~RespServer() + { + if (client != kInvalidSocket) close_socket(client); + if (listener != kInvalidSocket) close_socket(listener); + if (thread.joinable()) thread.join(); + } + + bool start() + { + init_sockets(); + listener = socket(AF_INET, SOCK_STREAM, 0); + if (listener == kInvalidSocket) return false; + int one = 1; +#ifdef _WIN32 + setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, + reinterpret_cast(&one), sizeof(one)); +#else + setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); +#endif + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; // ephemeral + if (bind(listener, reinterpret_cast(&addr), sizeof(addr)) != 0) return false; + AddrLenType alen = sizeof(addr); + getsockname(listener, reinterpret_cast(&addr), &alen); + port = ntohs(addr.sin_port); + if (listen(listener, 1) != 0) return false; + thread = std::thread([this] { run(); }); + return true; + } + + int port = 0; + +private: + void run() + { + SockType s = accept(listener, nullptr, nullptr); + if (s == kInvalidSocket) return; + client = s; + set_recv_timeout(client, 5000); + + std::string buf; + int served = 0; + while (served < 16) { + char tmp[1024]; + const int n = static_cast(recv(client, tmp, sizeof(tmp), 0)); + if (n <= 0) break; + buf.append(tmp, static_cast(n)); + + size_t off = 0; + while (served < 16) { + // `*\r\n` then `` bulk strings `$\r\n\r\n`. + if (buf.size() < off + 3 || buf[off] != '*') break; + const size_t nl = buf.find("\r\n", off); + if (nl == std::string::npos) break; + const int count = std::atoi(buf.substr(off + 1, nl - off - 1).c_str()); + off = nl + 2; + std::string cmd; + bool complete = true; + for (int i = 0; i < count; ++i) { + if (buf.size() < off + 1 || buf[off] != '$') { complete = false; break; } + const size_t nl2 = buf.find("\r\n", off); + if (nl2 == std::string::npos) { complete = false; break; } + const int len = std::atoi(buf.substr(off + 1, nl2 - off - 1).c_str()); + off = nl2 + 2; + if (buf.size() < off + static_cast(len) + 2) { complete = false; break; } + if (i == 0) cmd.assign(buf, off, static_cast(len)); + off += static_cast(len) + 2; + } + if (!complete) break; + buf.erase(0, off); + off = 0; + + if (cmd == "PING") { + static constexpr char kPong[] = "+PONG\r\n"; + send(client, kPong, static_cast(sizeof(kPong) - 1), 0); + } else if (cmd == "SET") { + static constexpr char kOk[] = "+OK\r\n"; + send(client, kOk, static_cast(sizeof(kOk) - 1), 0); + } + ++served; + } + } + } + + SockType listener = kInvalidSocket; + SockType client = kInvalidSocket; + std::thread thread; +}; + +int main() +{ + RespServer server; + if (!server.start()) { + std::cerr << "failed to start RESP server\n"; + return 1; + } + + sw::redis::ConnectionOptions opts; + opts.host = "127.0.0.1"; + opts.port = server.port; + opts.connect_timeout = std::chrono::milliseconds(3000); + opts.socket_timeout = std::chrono::milliseconds(3000); + + sw::redis::ConnectionPoolOptions pool_opts; + pool_opts.size = 1; + + try { + sw::redis::AsyncRedis redis(opts, pool_opts); + + // Fire both commands back-to-back; the libuv loop runs on a background + // thread and the server may see them pipelined in one segment. + auto ping_res = redis.ping(); + auto set_res = redis.set("mcpp", "async"); + + const std::string pong = ping_res.get(); + if (pong != "PONG") { + std::cerr << "unexpected async PING reply: '" << pong << "'\n"; + return 2; + } + if (!set_res.get()) { + std::cerr << "async SET did not return OK\n"; + return 3; + } + + std::cout << "OK: async PING -> " << pong << ", SET -> OK\n"; + return 0; + } catch (const sw::redis::Error &e) { + std::cerr << "redis error: " << e.what() << "\n"; + return 4; + } +}