diff --git a/docs/descriptor-examples.md b/docs/descriptor-examples.md index fc706d9f..9cbfe21b 100644 --- a/docs/descriptor-examples.md +++ b/docs/descriptor-examples.md @@ -15,7 +15,7 @@ in the [root README](../README.md#reference-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.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) | | Form A whose consumer deps must be written by hand | [`huxerui.huxerui`](../pkgs/h/huxerui.huxerui.lua) (HuxerUI declares its GTK4 stack on the TARGET axis, which is the form mcpp recommends and which a descriptor structurally cannot carry — three platform blocks, and a cfg selector is not a platform. `mcpp emit xpkg` says so and emits empty `deps`, so the 36-entry closure is transcribed into `xpm.linux.deps` at PLATFORM level (a per-version `deps` is inert). Its `licenses`/`repo` also deliberately disagree with what emit produces) | | Native multi-module library with feature-scoped sources | [`gzj-creator.galay`](../pkgs/g/gzj-creator.galay.lua) (Galay 5.0.2 — the upstream Form-A manifest exposes `galay.utils` and `galay.kernel` by default, while SSL, HTTP, database, RPC, MCP, and tracing modules stay behind named features and their corresponding dependencies. The index keeps the upstream manifest intact and tests the default module surface on Unix.) | -| 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`) | · [`compat.xxhash`](../pkgs/c/compat.xxhash.lua) (one TU, one header, no features at all — the interesting decision is what is NOT compiled: `xxh_x86dispatch.c` selects an AVX2/AVX512 path at RUNTIME and needs per-file `-mavx2` plus `XXH_X86DISPATCH` at every call site, so the package ships the flagless SSE2 baseline instead. Nor is the header-only `XXH_INLINE_ALL` mode chosen: it re-emits the implementation in every TU that hashes anything, which is the right trade only when there is exactly one such TU — something a package cannot know) +| 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`) · [`compat.xxhash`](../pkgs/c/compat.xxhash.lua) (one TU, one header, no features at all — the interesting decision is what is NOT compiled: `xxh_x86dispatch.c` selects an AVX2/AVX512 path at RUNTIME and needs per-file `-mavx2` plus `XXH_X86DISPATCH` at every call site, so the package ships the flagless SSE2 baseline instead. Nor is the header-only `XXH_INLINE_ALL` mode chosen: it re-emits the implementation in every TU that hashes anything, which is the right trade only when there is exactly one such TU — something a package cannot know) · [`compat.quickjs-ng`](../pkgs/c/compat.quickjs-ng.lua) (QuickJS-ng 0.17.0 — upstream's four engine TUs and nothing else. `quickjs-libc.c`, the optional standard library that gives scripts files, the environment, timers and child processes, is NOT compiled, so an embedder gets an engine with no host access unless it adds its own. The tarball keeps `quickjs.h` beside private headers with generic names (`list.h`, `cutils.h`), so a `generated_files` forwarder is the only include directory, as in compat.libaio) | | C-source compat whose sources are chosen by ARCHITECTURE | [`compat.wamr`](../pkgs/c/compat.wamr.lua) (WAMR 2.4.5 — the descriptor schema varies `sources` and `cflags` per OS but has no per-architecture hook, and `archs` is package metadata rather than a selector. WAMR needs both: one of `BUILD_TARGET_X86_64`/`BUILD_TARGET_AARCH64` must be defined or `wasm_runtime_common.c` compiles its whole invoke-native section away and the link fails on `invokeNative`, and the implementation of that symbol is a hand-written assembly file per architecture. Both halves are handed to the preprocessor instead: a `generated_files` config header maps `__x86_64__`/`__aarch64__` to the matching `BUILD_TARGET_*` and reaches every TU through `-include`, and a generated `.S` — `.S`, because upstream's `arch/invokeNative_*.s` are lowercase and clang assembles those WITHOUT the preprocessor, so unlike libffi's `.S` files they cannot guard themselves — `#include`s the chosen one as text. Two further notes worth copying: `-std=gnu11` arrives through `cflags` because `c_standard = "c11"` defines `__STRICT_ANSI__`, under which the bare `asm` in `platform_internal.h` is not a keyword; and the POSIX files upstream drops when WASI is off are carried unconditionally rather than `!`-excluded and re-added by the `libc-wasi` feature, because an exclusion glob is global and wins over the feature's own entry for the same file) | | C-source compat where the library IS a kernel ABI | [`compat.libaio`](../pkgs/c/compat.libaio.lua) (libaio 0.3.113 — twelve syscall-wrapper TUs, and the only `xpm` section is `linux`, because there is no port to declare: `struct iocb` is the kernel's and every TU is `syscall(__NR_io_*, …)`. Consumers gate it with `[target.'cfg(linux)'.dependencies]`, the mirror image of compat.wil. Three things it teaches. **One public header out of a source dir**: upstream installs exactly one, `libaio.h`, but the tarball keeps it in `src/` beside the private headers — one of which is named `syscall.h` and would SHADOW glibc's for every consumer TU — so `include_dirs` names a `generated_files` forwarder and nothing else; the package's own sources reach the real header through it while their quote-form `#include "syscall.h"` still resolves next to the including `.c`, so no `-I` into `src/` is needed at all. **A `c_standard` that is a trap**: `-std=c11` sets `__STRICT_ANSI__`, which hides `syscall()` and `sigset_t`, and the public header then fails to parse at `io_pgetevents`; declaring `c_standard = "gnu11"` LOOKS like the fix but mcpp 2026.8.27.2 accepts the string and still emits `-std=c11` (visible in the emitted `compile_commands.json`), so `-D_GNU_SOURCE` in `cflags` is the spelling that takes effect. **Symbol versioning in a static package**: `io_getevents` and `io_cancel` have no ordinary definitions upstream — the functions are `io_getevents_0_4` etc. publishing short names through `.symver … @@LIBAIO_0.4` — which resolves for an executable under both ld.bfd and lld, but not when a consumer builds a `.so` straight out of these objects; that needs upstream's `src/libaio.map`, exactly as upstream's own `libaio.a` does) | | 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)) | · [`compat.sqlitecpp`](../pkgs/c/compat.sqlitecpp.lua) (the RAII C++ wrapper over SQLite. Upstream vendors sqlite3 as a GIT SUBMODULE, so a source tarball simply does not contain it and the library cannot link — the dependency edge on `compat.sqlite3` replaces the submodule, and does it better: two consumers of SQLite in one link now share ONE amalgamation instead of each embedding a private copy with its own compile-time options. Its two CMake knobs are deliberately not set — `SQLITECPP_USE_ASSERT_ON_ERRORS` changes the error model from throwing to aborting, and `SQLITE_ENABLE_COLUMN_METADATA` has to agree with how SQLite ITSELF was built; both are the consumer's call, and the headers already guard them with `#ifdef`) diff --git a/docs/zh/descriptor-examples.md b/docs/zh/descriptor-examples.md index efa3da25..5f976ef8 100644 --- a/docs/zh/descriptor-examples.md +++ b/docs/zh/descriptor-examples.md @@ -13,7 +13,7 @@ | 原生模块库(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.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 的消费者链进去的是同一份而非两份)| | 消费者依赖须手写的 Form A | [`huxerui.huxerui`](../../pkgs/h/huxerui.huxerui.lua)(HuxerUI 把 GTK4 栈声明在 TARGET 轴上 —— 这是 mcpp 推荐的写法,而描述符在结构上承载不了:只有三个平台块,cfg 选择器不是平台。`mcpp emit xpkg` 会如实说明并输出空 `deps`,因此 36 项闭包被手工抄进 PLATFORM 级的 `xpm.linux.deps`(按版本写的 `deps` 不生效)。它的 `licenses`/`repo` 也刻意与 emit 的产出不同) | | 原生多模块库(按 feature 管理源码) | [`gzj-creator.galay`](../../pkgs/g/gzj-creator.galay.lua)(Galay 5.0.2 —— 上游 Form-A manifest 默认提供 `galay.utils` 与 `galay.kernel`,SSL、HTTP、数据库、RPC、MCP、tracing 等模块及其依赖按具名 feature 开启。索引保持上游 manifest 原样,Unix 成员测试默认模块表面)| -| 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) |· [`compat.xxhash`](../../pkgs/c/compat.xxhash.lua)(单 TU、单头、无 feature —— 值得说的是**没有**编译什么:`xxh_x86dispatch.c` 在运行期选择 AVX2/AVX512 路径,需要 per-file `-mavx2` 并要求每个调用点定义 `XXH_X86DISPATCH`,故本包只出无需任何 flag 的 SSE2 基线。也没有选 header-only 的 `XXH_INLINE_ALL` 模式:它会在每个做哈希的 TU 里重新展开整份实现,那只有在「恰好只有一个这样的 TU」时才划算 —— 而这件事包本身无从知道) +| 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) · [`compat.xxhash`](../../pkgs/c/compat.xxhash.lua)(单 TU、单头、无 feature —— 值得说的是**没有**编译什么:`xxh_x86dispatch.c` 在运行期选择 AVX2/AVX512 路径,需要 per-file `-mavx2` 并要求每个调用点定义 `XXH_X86DISPATCH`,故本包只出无需任何 flag 的 SSE2 基线。也没有选 header-only 的 `XXH_INLINE_ALL` 模式:它会在每个做哈希的 TU 里重新展开整份实现,那只有在「恰好只有一个这样的 TU」时才划算 —— 而这件事包本身无从知道) · [`compat.quickjs-ng`](../../pkgs/c/compat.quickjs-ng.lua)(QuickJS-ng 0.17.0 —— 只编上游的四个引擎 TU。可选标准库 `quickjs-libc.c`(给脚本文件、环境变量、定时器与子进程能力)不编,嵌入方拿到的引擎碰不到宿主,除非自己提供。tarball 把 `quickjs.h` 和 `list.h`、`cutils.h` 这类名字很通用的私有头放在一起,所以 include 路径上只放一个 `generated_files` 转发头,与 compat.libaio 同法) | | C 源码 compat(源码按**架构**选择) | [`compat.wamr`](../../pkgs/c/compat.wamr.lua)(WAMR 2.4.5 —— 描述符能按 OS 分 `sources`/`cflags`,但**没有按架构分**的钩子,`archs` 是包级元数据而非选择器。而 WAMR 两处都要按架构走:不定义 `BUILD_TARGET_X86_64`/`BUILD_TARGET_AARCH64` 之一,`wasm_runtime_common.c` 会把整段 invoke-native 编没,链接期报 `invokeNative` 未定义;而该符号的实现本身就是每架构一份手写汇编。两处都改交给预处理器:`generated_files` 生成的配置头把 `__x86_64__`/`__aarch64__` 映射到对应 `BUILD_TARGET_*`,经 `-include` 到达每个 TU;另一份生成的 `.S` 用 `#include` 把选中的汇编原样拉进来 —— 之所以必须是 `.S`,是因为上游的 `arch/invokeNative_*.s` 是小写后缀,clang 汇编它们**不过预处理器**,因此不像 libffi 的 `.S` 那样能自己加架构守卫。另有两点值得抄:`-std=gnu11` 经 `cflags` 追加,因为 `c_standard = "c11"` 会定义 `__STRICT_ANSI__`,此时 `platform_internal.h` 里裸写的 `asm` 不是关键字;以及上游在关掉 WASI 时会剔除的那几个 POSIX 文件,这里**无条件常含**,而不是 base 用 `!` 排除、再由 `libc-wasi` feature 加回来 —— 排除 glob 是全局的,会盖过 feature 里同名文件的条目) | | C 源码 compat(库本身就是内核 ABI) | [`compat.libaio`](../../pkgs/c/compat.libaio.lua)(libaio 0.3.113 —— 12 个系统调用封装 TU,`xpm` 只有 `linux` 一段,因为根本不存在「移植」可声明:`struct iocb` 就是内核的结构体,每个 TU 都是 `syscall(__NR_io_*, …)`。消费者用 `[target.'cfg(linux)'.dependencies]` 门控,与 compat.wil 互为镜像。它给出三条经验。**把唯一的公开头从源码目录里择出来**:上游只安装 `libaio.h` 一个头,但 tarball 把它放在 `src/` 里、与私有头并列 —— 其中一个恰好叫 `syscall.h`,一旦上了 include 路径就会**遮蔽** glibc 的同名头。故 `include_dirs` 只指向一个 `generated_files` 转发头;包自身的源码经它拿到真头文件,而它们引号形式的 `#include "syscall.h"` 仍按「包含者所在目录优先」解析,于是整包**不需要任何指向 `src/` 的 `-I`**。**一个会骗人的 `c_standard`**:`-std=c11` 会定义 `__STRICT_ANSI__`,从而藏掉 `syscall()` 与 `sigset_t`,连公开头都会在 `io_pgetevents` 处解析失败;写 `c_standard = "gnu11"` **看起来**是解法,但 mcpp 2026.8.27.2 接受这个字符串却依然发 `-std=c11`(在产出的 `compile_commands.json` 里可见),真正生效的写法是 `cflags` 里的 `-D_GNU_SOURCE`。**静态包里的符号版本**:`io_getevents` / `io_cancel` 在上游并没有普通定义 —— 函数名是 `io_getevents_0_4` 之类,短名经 `.symver … @@LIBAIO_0.4` 发布 —— 链接**可执行文件**时 ld.bfd 与 lld 都能解析,但消费者若直接拿这些对象去构建 `.so` 就不行,那需要上游的 `src/libaio.map`,与上游自己的 `libaio.a` 完全同理) | | 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 同款手法)) · [`compat.sqlitecpp`](../../pkgs/c/compat.sqlitecpp.lua)(SQLite 的 RAII C++ 封装。上游用 **git submodule** 引 sqlite3,源码 tarball 里根本没有它,库因此无法链接 —— 依赖边指向 `compat.sqlite3` 替代了那个 submodule,而且更好:同一次链接里的两个 SQLite 消费者从此共享**一份** amalgamation,而不是各自内嵌一份带各自编译选项的副本。它的两个 CMake 开关有意不设 —— `SQLITECPP_USE_ASSERT_ON_ERRORS` 把错误模型从抛异常改成中止进程,`SQLITE_ENABLE_COLUMN_METADATA` 必须与 SQLite **自身**的构建一致;两者都该由消费者决定,而头文件本来就用 `#ifdef` 守着) | diff --git a/mcpp.toml b/mcpp.toml index 0494b2b0..cb71190f 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -176,6 +176,7 @@ members = [ "tests/examples/vulkan-memory-allocator", "tests/examples/wamr", "tests/examples/wamr-features", + "tests/examples/quickjs-ng", ] # ── The index redirect, hoisted to the workspace root ─────────────────── diff --git a/pkgs/c/compat.quickjs-ng.lua b/pkgs/c/compat.quickjs-ng.lua new file mode 100644 index 00000000..6d6773c7 --- /dev/null +++ b/pkgs/c/compat.quickjs-ng.lua @@ -0,0 +1,110 @@ +-- compat.quickjs-ng — QuickJS-ng, a small embeddable JavaScript engine. +-- +-- Shape A (C-source compat): upstream's library target and nothing else. A +-- consumer writes `#include ` and links the lib. +-- +-- WHAT IS COMPILED. Upstream's `qjs_sources` (CMakeLists.txt:273-278): dtoa.c, +-- libregexp.c, libunicode.c and quickjs.c. quickjs-libc.c is NOT compiled. It +-- is upstream's optional standard library, the `std` / `os` / `bjson` modules +-- (files, environment variables, timers, child processes, signal handlers). +-- Upstream's own library leaves it out by default too: CMake adds it only under +-- QJS_BUILD_LIBC (meson: -Dlibc=true) and otherwise builds it as a separate, +-- uninstalled static library for the qjs and qjsc programs. An embedder that +-- wants the engine without giving scripts access to the host needs it left +-- out. The other .c files at the root are the qjs/qjsc programs, tests and +-- generators. +-- +-- ONE PUBLIC HEADER. Without quickjs-libc, upstream installs quickjs.h alone +-- (CMakeLists.txt:557, meson.build:144-146 and 263), but the tarball keeps it +-- at the root beside the engine's private headers, among them list.h, cutils.h +-- and dtoa.h, names generic enough to shadow a consumer's own. So a one-line +-- forwarder through `generated_files` is the only directory on the include +-- path (the compat.libaio pattern). The engine's own TUs include their headers +-- in quote form next to the .c and need no -I. +-- +-- FLAGS, each one as upstream's CMake and meson builds set it: +-- -std=gnu11 C11 with GNU extensions (CMakeLists.txt:9-11, +-- meson.build:6). The -std on this package's compile +-- line does not follow `c_standard`: "gnu11" there still +-- gives -std=c11 (the same observation as compat.libaio), +-- so the dialect arrives through `cflags`, where the +-- later -std wins. +-- -funsigned-char CMakeLists.txt:109, meson.build:42, and /J for MSVC +-- (meson.build:70). x86_64 and aarch64 Linux disagree on +-- the sign of plain char, and upstream pins it so the +-- engine behaves the same on both. A consumer does not +-- need it: quickjs.h only passes char by pointer. +-- -D_GNU_SOURCE CMakeLists.txt:285 and meson.build:162, on every +-- platform. +-- -DQUICKJS_NG_BUILD CMakeLists.txt:46, meson.build:19. quickjs.h reads it +-- only for quickjs-libc's Windows export macro +-- (quickjs.h:82), so it has no effect on Linux today; it +-- is set so the engine's TUs see the macros upstream's +-- own build gives them. +-- All four go through `cflags`, so they reach this package's C TUs and never a +-- consumer's TU. NDEBUG is not set, as in most descriptors here, so the engine +-- keeps its assertions and its debug-dump code (quickjs.c:84-86); upstream's +-- default build type (Release) would define it. +-- +-- LINUX ONLY. Built and tested with llvm 22.1.8 on x86_64-linux-gnu, and in an +-- openkal graph for x86_64-linux-gnu. Upstream supports macOS and Windows, but +-- nothing here has built the package there, so those sections are not declared. +-- +-- NO CN MIRROR. The url is upstream's tag archive as a plain string, the +-- fallback docs/cn-mirror.md describes for a contributor without mcpp-res write +-- access. A maintainer can turn it into a GLOBAL/CN table later; the sha256 +-- does not change. +-- +-- LICENSES. The engine is MIT (LICENSE). libunicode-table.h, which libunicode.c +-- compiles in, is generated from the Unicode data files and carries the +-- Unicode License V3 in its header, so a binary built from this package needs +-- both notices. +package = { + spec = "1", + namespace = "compat", + name = "quickjs-ng", + description = "QuickJS-ng, a small embeddable JavaScript engine (engine only, without quickjs-libc)", + licenses = {"MIT", "Unicode-3.0"}, + repo = "https://github.com/quickjs-ng/quickjs", + type = "package", + + xpm = { + linux = { + ["0.17.0"] = { + url = "https://github.com/quickjs-ng/quickjs/archive/refs/tags/v0.17.0.tar.gz", + sha256 = "559bc4c420475e55c7ab4510adbc562f55d7524d75e8e89d79ce4bb02f5687d9", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", + include_dirs = { "quickjs-0.17.0/mcpp/include" }, + cflags = { "-std=gnu11", "-funsigned-char", "-D_GNU_SOURCE", "-DQUICKJS_NG_BUILD" }, + sources = { + "*/dtoa.c", + "*/libregexp.c", + "*/libunicode.c", + "*/quickjs.c", + }, + generated_files = { + ["quickjs-0.17.0/mcpp/include/quickjs.h"] = +[[ +#pragma once +/* compat.quickjs-ng: upstream installs quickjs.h alone, but the release + tarball keeps it beside the engine's private headers (list.h, cutils.h, + dtoa.h, ...). This forwarder is the only thing on the include path. */ +#include "../../quickjs.h" +]], + }, + -- Upstream links `m`, the dl library and the thread library PUBLIC + -- (CMakeLists.txt:291-311). A consumer with no C++ in it is linked by + -- the C driver, which adds no libm, and quickjs.c's Math functions then + -- fail to link. libdl is left out: only quickjs-libc.c calls dlopen. + linux = { ldflags = { "-lm", "-lpthread" } }, + targets = { ["quickjs-ng"] = { kind = "lib" } }, + deps = { }, + }, +} diff --git a/tests/examples/quickjs-ng/mcpp.toml b/tests/examples/quickjs-ng/mcpp.toml new file mode 100644 index 00000000..19c96c1c --- /dev/null +++ b/tests/examples/quickjs-ng/mcpp.toml @@ -0,0 +1,12 @@ +# QuickJS-ng test project: evaluate JavaScript through the engine and assert +# the values it computes, one group per compiled source file. +# +# compat.quickjs-ng carries a `linux` section only (see the descriptor's +# header), so the dependency is gated and the test compiles to a no-op main() +# elsewhere, the same shape as tests/examples/libaio and tests/examples/wamr. +[package] +name = "quickjs-ng-tests" +version = "0.1.0" + +[target.'cfg(linux)'.dependencies.compat] +quickjs-ng = "0.17.0" diff --git a/tests/examples/quickjs-ng/tests/engine.cpp b/tests/examples/quickjs-ng/tests/engine.cpp new file mode 100644 index 00000000..fdee6c0f --- /dev/null +++ b/tests/examples/quickjs-ng/tests/engine.cpp @@ -0,0 +1,139 @@ +// Behavioral test: JavaScript evaluated by the engine, checked by value. +// +// The package compiles four sources, and each group below needs one of them to +// be present and built with the right flags, so a missing or mis-built TU shows +// up as a wrong answer rather than only as a link error: +// +// quickjs.c evaluation, closures, exceptions, the promise job queue +// dtoa.c shortest round-trip number formatting (0.1 + 0.2, 5e-324) +// libregexp.c named groups, case-insensitive and sticky matching +// libunicode.c full case mapping, NFC normalization, script properties +// +// The expected strings are what the ECMAScript specification requires, written +// out by hand; none is computed by the code under test. +// +// The runtime is freed at the end. The package does not define NDEBUG, so +// JS_FreeRuntime asserts that no object is still alive (quickjs.c:2699) and an +// object this test forgot to free aborts it. + +#ifdef __linux__ + +#include + +#include +#include +#include + +// The descriptor exposes quickjs.h alone. The engine's private headers sit next +// to it in the tarball and must not reach a consumer. +#if __has_include() || __has_include() +#error "compat.quickjs-ng put the engine's private headers on the include path" +#endif + +namespace { + +int failures = 0; + +std::string to_std_string(JSContext* ctx, JSValueConst v) { + const char* s = JS_ToCString(ctx, v); + std::string out = s ? s : ""; + JS_FreeCString(ctx, s); + return out; +} + +// Global code; the completion value of the last statement is the result. +std::string run_js(JSContext* ctx, const char* src) { + JSValue v = JS_Eval(ctx, src, std::strlen(src), "", JS_EVAL_TYPE_GLOBAL); + std::string out; + if (JS_IsException(v)) { + JSValue exc = JS_GetException(ctx); + out = "exception " + to_std_string(ctx, exc); + JS_FreeValue(ctx, exc); + } else { + out = to_std_string(ctx, v); + } + JS_FreeValue(ctx, v); + return out; +} + +void expect(JSContext* ctx, const char* src, const char* want) { + std::string got = run_js(ctx, src); + if (got != want) { + std::fprintf(stderr, "FAIL %s\n want: %s\n got: %s\n", src, want, got.c_str()); + ++failures; + } +} + +} // namespace + +int main() { + // The library and the header it was built from agree on the version. + char header_version[32]; + std::snprintf(header_version, sizeof header_version, "%d.%d.%d%s", + QJS_VERSION_MAJOR, QJS_VERSION_MINOR, QJS_VERSION_PATCH, QJS_VERSION_SUFFIX); + std::printf("QuickJS-ng %s\n", JS_GetVersion()); + if (std::strcmp(JS_GetVersion(), "0.17.0") != 0 || + std::strcmp(JS_GetVersion(), header_version) != 0) { + std::fprintf(stderr, "FAIL version: library %s, header %s\n", JS_GetVersion(), header_version); + ++failures; + } + + JSRuntime* rt = JS_NewRuntime(); + JSContext* ctx = rt ? JS_NewContext(rt) : nullptr; + if (ctx == nullptr) { + std::fprintf(stderr, "FAIL could not create a runtime and context\n"); + return 1; + } + + // quickjs.c + expect(ctx, "const sq = [1, 2, 3].map(x => x * x); sq.reduce((a, b) => a + b, 0)", "14"); + expect(ctx, "(() => { let n = 0; const inc = () => ++n; inc(); inc(); return inc(); })()", "3"); + expect(ctx, "JSON.stringify({ a: [1, { b: null }], c: 'x' })", "{\"a\":[1,{\"b\":null}],\"c\":\"x\"}"); + expect(ctx, "throw new TypeError('boom')", "exception TypeError: boom"); + + // dtoa.c + expect(ctx, "String(0.1 + 0.2)", "0.30000000000000004"); + expect(ctx, "String(5e-324)", "5e-324"); + expect(ctx, "String(1e21)", "1e+21"); + expect(ctx, "(1.005).toFixed(2)", "1.00"); + expect(ctx, "(255).toString(16) + ' ' + parseFloat('2.5e-3') * 4", "ff 0.01"); + + // libregexp.c + expect(ctx, "const m = /(?\\d{4})-(?\\d{2})/.exec('on 2026-09'); m.groups.y + '/' + m.groups.m", + "2026/09"); + expect(ctx, "'aXbxc'.split(/x/i).join(',')", "a,b,c"); + expect(ctx, "const re = /o/y; re.lastIndex = 1; re.test('foo') + ' ' + re.lastIndex", "true 2"); + + // libunicode.c + expect(ctx, "'straße'.toUpperCase()", "STRASSE"); + expect(ctx, "'\\u0041\\u030A'.normalize('NFC') === '\\u00C5'", "true"); + expect(ctx, "/^\\p{Script=Greek}+$/u.test('αβγ') + ' ' + /\\p{Script=Greek}/u.test('abc')", "true false"); + + // quickjs.c, the job queue: a reaction runs only when the host drains it. + expect(ctx, "var out = 'pending'; Promise.resolve(20).then(v => { out = 'resolved ' + (v + 1); }); out", + "pending"); + JSContext* job_ctx = nullptr; + int r; + while ((r = JS_ExecutePendingJob(rt, &job_ctx)) > 0) { + } + if (r != 0) { + std::fprintf(stderr, "FAIL a pending job threw\n"); + ++failures; + } + expect(ctx, "out", "resolved 21"); + + JS_FreeContext(ctx); + JS_FreeRuntime(rt); + + if (failures != 0) { + std::fprintf(stderr, "%d check(s) failed\n", failures); + return 1; + } + return 0; +} + +#else + +int main() { return 0; } // compat.quickjs-ng declares linux only; nothing to assert here. + +#endif