Skip to content

Commit 293ef5f

Browse files
committed
feat: compat.websocket server + zlib features (gated; server implies zlib)
1 parent 6067168 commit 293ef5f

7 files changed

Lines changed: 358 additions & 5 deletions

File tree

.agents/docs/2026-08-06-add-websocket-plan.md

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,42 @@ mcpp test -p websocket → test result ok. 1 passed; 0 fa
9696
lint:`lua` 语法、`check_mirror_urls`(plain-string url 不触发镜像约束)、`check_package_name` 全过;
9797
`mcpp xpkg parse` 在本地较新 mcpp(2026.8.4.1)与 CI pin(2026.8.3.3)上均 OK。
9898

99-
## 7. 后续待办
99+
## 7. 后续:server + zlib features(同一天,独立 PR 叠加在 #158 上)
100+
101+
基座合流后,又补了两个可选 feature,`pkgs/c/compat.websocket.lua` 增加 `features` 表:
102+
103+
- **`server`**:把基座剔掉的 4 个 TU 编回来。逐文件核对过,它们只 include 标准库与库内已编的 IX 头,
104+
零新增外部依赖。消费端 `features = ["server"]` 即获得 `ix::WebSocketServer`(`SocketServer` 派生,
105+
自带 `getPort()` 等)。
106+
- **`zlib`**:`defines = { "IXWEBSOCKET_USE_ZLIB=1" }` + `deps = { ["compat.zlib"] = "1.3.2" }`,
107+
把 gzip codec 从 no-op 变成真正的 permessage-deflate 压缩。define 只到包自身 TU,消费端无需
108+
拿到 —— 客户端 `enablePerMessageDeflate()`、server 默认开。
109+
110+
**`server` implies `zlib` —— 一个靠读源码才发现的正确性坑**:`IXWebSocketServer` 默认开启
111+
permessage-deflate(`_enablePerMessageDeflate = true`),而 transport 的扩展协商**不受**
112+
`IXWEBSOCKET_USE_ZLIB` 门控(只有 codec 被门控)。若 server 编了而 zlib 没编,server 会宣称压缩、
113+
却执行不了 —— 因此用具名 feature 的 `implies = { "zlib" }` 强制一起开(参考 `compat.eigen`
114+
`use_blas`;`default` feature 的 implies 恒生效是另一个已知坑,具名 feature 不受影响)。
115+
116+
### 验证(与 CI 一致:mcpp 2026.8.3.3 + gcc@16.1.0 + `MCPP_BUILD_CACHE=local`)
117+
118+
```
119+
mcpp test -p websocket → test result ok. 1 passed; 0 failed (默认零依赖基座,回归)
120+
mcpp test -p websocket-features → test result ok. 1 passed; 0 failed (server + zlib)
121+
```
122+
123+
- `websocket-features` 成员:起真实 `ix::WebSocketServer` 于 loopback,text/binary 往返;**压缩在
124+
线路上可观测**:64 KiB 重复 `'a'` 往返,`message.str.size()` = 65536 而 `wireSize` = **80** ——
125+
该字段在 transport 里取原始帧负载大小(压缩后),`str` 是解压后内容,比值即压缩证据。
126+
- **负向验证**:默认成员(无 feature)的构建产物里,4 个 server TU 的 `.o` 不存在,`nm` 全量
127+
`WebSocketServer`/`SocketServer`/`HttpServer` 符号 —— 消费端不开 `server` 却引用它会链接报错,
128+
feature 门控真实生效。
129+
130+
## 8. 后续待办
100131

101132
- **CN 镜像**:有 `mcpp-res` 写权限后,`gtc``mcpp-res/websocket`、传与 GLOBAL 字节一致的
102133
`IXWebSocket-12.0.1.tar.gz`,把三平台 url 改写为 `{ GLOBAL, CN }`(sha 不变)。
103-
- **TLS feature**(可选):索引里有 `compat.openssl`/`compat.mbedtls`,若要支持 wss,可加 feature 把对应
104-
TLS TU 与依赖编入 —— 本包刻意先做零依赖纯客户端。
134+
- **TLS feature**(最后再考虑):索引里有 `compat.openssl`/`compat.mbedtls`,若要支持 wss,可加 feature
135+
把对应 TLS TU 与依赖编入(linux+openssl / windows+mbedtls / mac+SecureTransport)。它是唯一会显著
136+
拖慢 CI 的选项(openssl 走 install() 重型构建),故排在最后。
105137
- **C++23 薄封装头**(可选):用户计划稿提到的 `websocket.hpp`(span/format/RAII 薄层)不阻塞本包,可后续单独加。

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Two kinds of packages live here:
4040
| 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) |
4141
| C-source compat (with `features`) | [`compat.cjson`](pkgs/c/compat.cjson.lua) · [`compat.zlib`](pkgs/c/compat.zlib.lua) |
4242
| 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) · [`compat.re2`](pkgs/c/compat.re2.lua) (22 TUs, upstream's own `RE2_SOURCES`) |
43-
| C++-source compat, client-only with zero deps | [`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. TLS is off (no `IXWEBSOCKET_USE_TLS`; the OpenSSL/MbedTLS/AppleSSL TUs aren't built) and zlib is off too (`IXWEBSOCKET_USE_ZLIB` unset, so the gzip codec compiles to a no-op) — no external dependency at all. Its test brings its own minimal RFC 6455 echo server on loopback sockets, so the handshake, masking, fragmentation and close are exercised offline) |
43+
| 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) |
4444
| header-only (with `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) |
4545
| 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) |
4646
| Whole-source direct build + generated config (only where a platform lacks one) | [`compat.curl`](pkgs/c/compat.curl.lua) (win32 uses upstream's checked-in config, unix generates one) · [`compat.sdl2`](pkgs/c/compat.sdl2.lua) (win/mac use upstream's checked-in config; linux generates one and enables X11 by hand) · [`compat.c-ares`](pkgs/c/compat.c-ares.lua) (91 TUs; the release tarball already ships `ares_build.h` and a Windows config, so only `ares_config.h` is snapshotted per OS) |

README.zh-CN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ mcpp self config --mirror CN # 切换至国内镜像,默认使用 GLOBAL 上
3737
| 原生模块库(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 的消费者链进去的是同一份而非两份)|
3838
| C 源码 compat(含 `features`) | [`compat.cjson`](pkgs/c/compat.cjson.lua) · [`compat.zlib`](pkgs/c/compat.zlib.lua) |
3939
| 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 运行时) · [`compat.re2`](pkgs/c/compat.re2.lua)(22 TU,取自上游自身的 `RE2_SOURCES`) |
40-
| C++ 源码 compat(纯客户端,零依赖) | [`compat.websocket`](pkgs/c/compat.websocket.lua)(IXWebSocket 12.0.1 —— 从上游 `IXWEBSOCKET_SOURCES` 剔掉 4 个 server TU 后直编的纯 RFC 6455 客户端TLS 关闭(不定义 `IXWEBSOCKET_USE_TLS`,OpenSSL/MbedTLS/AppleSSL 三组 TU 均不编),zlib 也关闭(`IXWEBSOCKET_USE_ZLIB` 不定义,gzip codec 编译为 no-op),于是零外部依赖。其测试自带一个基于 loopback 原始 socket 的最小 RFC 6455 echo server,握手/掩码/分片/关闭全部离线实测) |
40+
| 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) |
4141
| header-only(含 `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) |
4242
| 运行时 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) |
4343
| 全源码直编 + 生成 config(仅缺口平台) | [`compat.curl`](pkgs/c/compat.curl.lua)(win32 用上游签入 config,unix 生成) · [`compat.sdl2`](pkgs/c/compat.sdl2.lua)(win/mac 用上游签入 config,linux 生成 + 手工开 X11) · [`compat.c-ares`](pkgs/c/compat.c-ares.lua)(91 TU;release tarball 已自带 `ares_build.h` 与 Windows 配置,故只需按 OS 冻结 `ares_config.h`) |

mcpp.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ members = [
6363
"tests/examples/tinyhttps",
6464
"tests/examples/vulkan",
6565
"tests/examples/websocket",
66+
"tests/examples/websocket-features",
6667
"tests/examples/tray",
6768
"tests/examples/yyjson",
6869
"tests/examples/magic_enum",

pkgs/c/compat.websocket.lua

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,52 @@ package = {
117117

118118
targets = { ["websocket"] = { kind = "lib" } },
119119

120+
-- ── Optional components ──────────────────────────────────────────
121+
-- The base build is the zero-dependency client above; two components
122+
-- can be enabled on top. mcpp features only ADD, so a consumer naming
123+
-- one keeps everything else:
124+
--
125+
-- websocket = "12.0.1" -> client only
126+
-- websocket = { …, features = ["server"] } -> + the server (implies zlib)
127+
-- websocket = { …, features = ["zlib"] } -> + compression
128+
-- websocket = { …, features = ["server", "zlib"] } -> + both
129+
--
130+
-- `server` brings the four TUs the base build leaves out. It needs
131+
-- nothing external: every IX* header it touches is already compiled
132+
-- into the client, and its external includes are stdlib only (checked
133+
-- against the four .cpp files). `IXWebSocketServer` derives from
134+
-- `SocketServer`, so consumers reach `getPort()` etc. through it.
135+
--
136+
-- `server` IMPLIES `zlib`: upstream's server enables permessage-deflate
137+
-- by default and the transport's extension negotiation is NOT gated on
138+
-- IXWEBSOCKET_USE_ZLIB (only the gzip codec is). A server built without
139+
-- the define would offer compression its codec cannot perform, so the
140+
-- implication keeps every server build capable of what it advertises.
141+
--
142+
-- `zlib` turns the gzip codec from its no-op into real
143+
-- permessage-deflate compression. Only
144+
-- IXWebSocketPerMessageDeflateCodec.cpp is gated by
145+
-- IXWEBSOCKET_USE_ZLIB; the negotiation logic in
146+
-- IXWebSocketPerMessageDeflate.cpp is zlib-free. The define reaches
147+
-- this package's own TUs — consumers need no define, they just call
148+
-- enablePerMessageDeflate() on their client (and the server enables it
149+
-- by default).
150+
features = {
151+
["server"] = {
152+
implies = { "zlib" },
153+
sources = {
154+
"*/ixwebsocket/IXSocketServer.cpp",
155+
"*/ixwebsocket/IXHttpServer.cpp",
156+
"*/ixwebsocket/IXWebSocketServer.cpp",
157+
"*/ixwebsocket/IXWebSocketProxyServer.cpp",
158+
},
159+
},
160+
["zlib"] = {
161+
defines = { "IXWEBSOCKET_USE_ZLIB=1" },
162+
deps = { ["compat.zlib"] = "1.3.2" },
163+
},
164+
},
165+
120166
-- ── Platform-specific ──────────────────────────────────────────────
121167
-- Upstream: Threads::Threads on UNIX, wsock32/ws2_32/shlwapi +
122168
-- _CRT_SECURE_NO_WARNINGS on Windows. shlwapi is only reached from the
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# websocket-features test project: consumes compat.websocket with the `server`
2+
# and `zlib` features enabled and asserts them under `mcpp test`. Part of the
3+
# mcpp-index self-referential workspace — the workspace-root `[indices]`
4+
# redirect points at this repo, so the dependency resolves to the checked-in
5+
# recipe (pkgs/c/compat.websocket.lua).
6+
#
7+
# The test is entirely OFFLINE: it runs an ix::WebSocketServer (the `server`
8+
# feature) on loopback and drives the ix::WebSocket client against it, then
9+
# checks that per-message-deflate (the `zlib` feature) actually compresses on
10+
# the wire. The sibling member tests/examples/websocket covers the zero-dep
11+
# client with no features; this member exists to prove the features.
12+
[package]
13+
name = "websocket-features-tests"
14+
version = "0.1.0"
15+
16+
[dependencies.compat]
17+
websocket = { version = "12.0.1", features = ["server", "zlib"] }

0 commit comments

Comments
 (0)