Imported from gtn1024/zmq4cj (
AGENTS.md). Install upstream withnpx skills add gtn1024/zmq4cj. Copyright stays with the author.
AGENTS.md
Project Overview
zmq is a ZeroMQ (libzmq) binding library for the Cangjie (仓颉) programming language. It wraps the C library via FFI (foreign func) and provides a safe, idiomatic Cangjie API.
Tech Stack
- Language: Cangjie (仓颉) v1.1.0
- Package Manager: cjpm
- Build System:
build.cjpre-build hook compiles libzmq from source via cmake + make - FFI:
foreign funcdeclarations bind libzmq C functions - C Library: libzmq v4.3.5 (git submodule at
vendor/libzmq/)
Build & Test Commands
cjpm build # Compile (auto-builds libzmq on first run)
cjpm test # Run unit tests
cd benchmark && cjpm run # Run benchmarks (~5 min)
Project Structure
src/
├── zmq_ffi.cj # FFI declarations (foreign func) — unsafe layer
├── zmq_types.cj # SocketType, SocketOption, SendRecvFlags, PollEvent enums
├── zmq_error.cj # ZmqError exception class, checkResult helper
├── zmq_context.cj # ZmqContext class (Resource interface)
├── zmq_socket.cj # ZmqSocket class (Resource interface)
├── zmq_poll.cj # PollItem class, ZmqPoll static class (zmq_poll wrapper)
├── zmq_proxy_class.cj # ZmqProxy static class (zmq_proxy/zmq_proxy_steerable wrapper)
├── zmq_common.cj # Internal String↔CString conversion utilities
└── zmq_test.cj # Unit tests
build.cj # Pre-build script: compiles libzmq.a from source
cjpm.toml # Package config with [ffi.c] per target platform
benchmark/ # Standalone benchmark project (cjpm run)
vendor/libzmq/ # git submodule (libzmq source)
Architecture
Three-layer design:
- FFI layer (
zmq_ffi.cj): Rawforeign funcdeclarations, 1:1 mapping to C API. All calls requireunsafeblocks. - Internal layer (
zmq_common.cj,zmq_error.cj): CString conversion, error checking viacheckResult(rc). - Public API (
zmq_context.cj,zmq_socket.cj,zmq_poll.cj,zmq_proxy_class.cj,zmq_types.cj): Safe API usingResourceinterface, exceptions instead of error codes. Nounsafe/CPointer/CStringexposed to users.
Cangjie Gotchas
- No
pointeeon CPointer — useCPointer<T>.read()andCPointer<T>.write(val)instead - No
&address-of operator — useCPointer<T>(inout var)to get a pointer to a stack variable inoutonly works onvarlocals — cannot take address of array element, struct field in expression, orletvariable- Array has no literal constructor —
Array<Foo>([a, b])does not compile. UseArrayList<Foo>()+.add()+.toArray() - CPointer type cast —
CPointer<T>(CPointer<Unit>(ptr))for reinterpret cast, NOTptr.asCPointer<T>() @C structfields — must bevarwith explicit initializer (e.g.,var _0: UInt8 = 0)CString≠CPointer<UInt8>— usecs.getChars()to getCPointer<UInt8>from aCStringacquireArrayRawData<T>/releaseArrayRawData<T>— public API instd.corefor getting raw pointer to Array's memory. No malloc/free between acquire and release (GC deadlock risk).- Cangjie enums are ADTs; integer values exposed via
.valueproperty - Tests use
@Test/@TestCase/@Assertfromstd.unittest
Type Mapping (C ↔ Cangjie)
| C Type | Cangjie Type |
|---|---|
void* |
CPointer<Unit> |
size_t |
UIntNative |
int |
Int32 |
short |
Int16 |
const char* |
CString |
Key Conventions
- All FFI calls are wrapped in
unsafeblocks, never exposed in public API - All errors throw
ZmqError— no return-code checking by users ZmqContextandZmqSocketimplementResourcefortry-with-resourcesauto-cleanupclose()is idempotent — usesAtomicBool.compareAndSwapto ensure underlying C cleanup runs exactly onceZmqSocketoperations (send/recv/bind/connect) are NOT thread-safe — one socket per thread- libzmq is a C++ library; Linux needs
-lstdc++ -lgcc_s, macOS needs-lc++, Windows needs-lc++ -lunwind -lws2_32 -liphlpapiin link options - Windows supported natively: zig builds libzmq for
x86_64-windows-gnuproducing a libc++ ABI that matches Cangjie's llvm-mingw runtime (system MinGW g++ would produce libstdc++ and fail to link — the historical Windows CI failure). The whole Windows build is inlined inbuild.cj(no separate script). The same flow also works as a cross-build from Linux/macOS for local dev
Performance Notes
send()useszmq_send(1 FFI call) +acquireArrayRawData(zero-copy from Array)recv()useszmq_msg_recvwith stack-allocatedZmqMsg(inoutpointer, no malloc)copyCPointerToArrayusesmemcpyviaacquireArrayRawData- Benchmark: ~1.4M msg/s inproc 64B, ~2.3M msg/s tcp 64B on ARM64 Linux
CI
- Platforms: Linux (x86_64) + macOS (x86_64 + ARM64) + Windows (x86_64)
windowsjob: runs natively onwindows-latest; zig builds libzmq (libc++ ABI), thencjpm build+cjpm testrun natively (tests execute for real, unlike a cross-build)- Tests use unique TCP ports per test case to avoid conflicts
Windows build (build.cj + zig)
- The Windows branch is selected by
@When[target == "x86_64-w64-mingw32"](cjpm compilesbuild.cjwith--cfg=target=...). Everything is inline instageWindowsBuild()— no shell script. Works for native Windows builds (cjpm buildon Windows) and cross-builds (cjpm build --target x86_64-w64-mingw32on Linux/macOS) ZIGenv var must point at a zig binary- build.cj generates (via
std.fs) tiny platform-aware launcher scripts (.shon Unix /.baton Windows) forCMAKE_C/CXX_COMPILER(zig needs acc/c++subcommand) and a windres shim (zig has no windres; version.rc resources are meaningless in a static.a— the shim copies a prebuilt empty COFF object). Plus an iphlpapi stub (Cangjie's bundled mingw is minimal) CMAKE_AR=llvm-ar+CMAKE_RANLIB=no-op: cmake emits anld.lld-compatible archive directly. GNUar/ranlibarchives are mis-read byld.lld(all symbols come out undefined), soranlibmust NOT run afterllvm-ar- cmake workarounds:
-DZMQ_WIN32_WINNT=0x0A00(avoid empty_WIN32_WINNTwhen cross-compiling),-DZMQ_HAVE_IPC=OFF(nosys/socket.hon mingw),-O2 -fno-sanitize=undefinedin the zig launchers (avoid UBSan refs) - Native vs cross: on a native Windows build (host == Windows) ffi.c resolves directly to
vendor/build/windows/lib. On a cross-build, cjpm resolves ffi.c via the HOST target's path (vendor/build/<host>/lib), sobuild.cjmirrors the PElibzmq_vendor.ainto the host lib dir and drops a.cross-mirrormarker so a later native build detects the foreign-format artifact and rebuilds (ELF/Mach-O) instead of linking a PE lib natively
Workflow
- Do not auto-commit — only commit when user explicitly says "commit"
- Do not auto-push — only push when user explicitly says "push"
- Use OpenSpec (
openspec) for change management: propose → apply → verify → archive