Skip to content

feat(pkg): compat.quickjs-ng 0.17.0 (QuickJS-ng JavaScript engine) - #466

Merged
Sunrisepeak merged 1 commit into
mcpplibs:mainfrom
yspbwx2010:feat/compat-quickjs-ng
Sep 25, 2026
Merged

Sunrisepeak merged 1 commit into
mcpplibs:mainfrom
yspbwx2010:feat/compat-quickjs-ng

Conversation

@yspbwx2010

Copy link
Copy Markdown
Contributor

Adds compat.quickjs-ng 0.17.0, the QuickJS-ng JavaScript engine as an embeddable C library. The engine is MIT; one generated table it compiles in carries the Unicode License V3, so licenses lists both. Line numbers below are for the v0.17.0 tag.

Shape

C-source compat, like compat.xxhash and compat.libaio. Upstream's qjs_sources (dtoa.c, libregexp.c, libunicode.c, quickjs.c; CMakeLists.txt:273-278) are built into one lib. The descriptor has a linux section only and a plain-string url. A consumer writes #include <quickjs.h>.

What is left out

quickjs-libc.c is not compiled. It is upstream's optional standard library: the std, os and bjson modules, which give scripts files, environment variables, timers, child processes and signal handlers. Upstream leaves it out of the library by default too. CMake adds it to libqjs only under QJS_BUILD_LIBC (meson: -Dlibc=true), and otherwise builds it as a separate static library for the qjs and qjsc programs, which it does not install. I use the engine embedded in a C++ host that runs scripts with no access to the host, and I think that is the right default for the base package. An embedder can add capabilities to a bare engine, but it cannot take quickjs-libc back out of a lib that already contains it. I have not tried carrying quickjs-libc as a feature.

Only quickjs.h reaches the consumer. The tarball keeps it at the root next to the engine's private headers, among them list.h, cutils.h and dtoa.h, and a consumer could easily have its own files with those names. Without quickjs-libc, upstream installs quickjs.h alone (CMakeLists.txt:557, meson.build:144-146 and 263). The descriptor does the same with a one-line forwarder in generated_files, as compat.libaio does. The engine's own TUs include their headers in quote form next to the .c files and need no -I. The test member refuses to compile if cutils.h or libregexp.h becomes reachable, and I checked that this guard fires (see below).

Flags

All four come from upstream's CMake and meson builds. They go through cflags, so they reach this package's C TUs and not the consumer's. I confirmed this in the compile database: the test's C++ TU gets none of them.

  • -std=gnu11. Upstream is C11 with GNU extensions (CMakeLists.txt:9-11, meson.build:6). The descriptor's c_standard does not decide the -std on the package's compile line. With c_standard = "gnu11" the line still has -std=c11, the same observation compat.libaio records, so the dialect is appended in cflags, where the later -std wins, as compat.wamr does. The engine also builds with plain -std=c11, but I followed upstream rather than rely on that.
  • -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. quickjs.h passes char only through pointers, so consumers do not need the flag.
  • -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 changes nothing on Linux today. I kept it so the engine's TUs see the same macros as in upstream's own build. Happy to drop it.

There is no CONFIG_VERSION. That macro belongs to the original QuickJS. QuickJS-ng takes its version from quickjs.h (QJS_VERSION_*, quickjs.h:1458-1461), and the test checks JS_GetVersion() against those macros.

NDEBUG is not set, as in most descriptors here, in either the dev or the release profile. The engine therefore keeps its assertions and its debug-dump code (quickjs.c:84-86). Upstream's default build type is Release in both CMake and meson, which would define NDEBUG. quickjs.h does not read NDEBUG, so either choice only affects the library's internals. Tell me if you would rather have it on.

Link libraries

Upstream links libm and the thread library PUBLIC on Unix (CMakeLists.txt:291, 299-311; meson.build:133-136), and the v0.17.0 release notes include "Link against libm unconditionally on Unices" (quickjs-ng/quickjs#1699). quickjs.c calls into libm for Math, and cutils.h uses pthread_once, mutexes and condition variables. A C++ consumer never notices, because the C++ driver links libm for its own runtime. A consumer with no C++ in it is linked by the C driver, and there the link fails with undefined references to floor, round, lrint and the rest. So the linux section carries ldflags = { "-lm", "-lpthread" }, as compat.libwebp and compat.wamr do. libdl is left out, because only quickjs-libc.c calls dlopen.

Linux only

Built and run with llvm 22.1.8 on x86_64-linux-gnu, and in an openkal graph for x86_64-linux-gnu; details are below. I also compiled the four TUs and the test outside mcpp with gcc 16.2.1 and the same flags, at -O0 and -O2. There were no warnings and the test passed. That is not the index's gcc leg, which uses its own sysroot, so the linux default leg will still be this package's first real gcc build. Upstream supports macOS and Windows and its CI builds both. I have not built this package there, so those sections are not declared. The member gates the dependency with cfg(linux), as libaio and wamr do.

Licenses

The engine is MIT (LICENSE). libunicode.c compiles in libunicode-table.h, which is generated from the Unicode data files, and that file's header is the Unicode License V3 (SPDX Unicode-3.0). A binary built from this package needs both notices, so both are listed.

No CN mirror

I do not have mcpp-res write access, so url is the plain-string upstream form described in docs/cn-mirror.md. The tarball can be mirrored as it is, and the sha256 stays the same.

Test member

tests/examples/quickjs-ng evaluates JavaScript and compares the resulting strings with values written out by hand from the specification. Each group needs a different compiled source file:

  • quickjs.c: closures, JSON, a thrown TypeError, and the promise job queue. A .then reaction must not run until the host drains the queue with JS_ExecutePendingJob.
  • dtoa.c: shortest round-trip formatting (0.1 + 0.2, 5e-324, 1e21), plus toFixed and radix conversion.
  • libregexp.c: named groups, case-insensitive split, and sticky matching with lastIndex.
  • libunicode.c: 'straße'.toUpperCase(), NFC normalization, and \p{Script=Greek}.

The test frees the runtime at the end. Because NDEBUG is unset, JS_FreeRuntime asserts that no object is still alive (quickjs.c:2699), so an object the test forgot to free aborts it.

I did not add the member to tests/openkal/members.toml. It runs there on x86_64 (below), so say the word if you want it measured.

Catalog entry

The entry goes after compat.xxhash in the "C-source compat" row of docs/descriptor-examples.md and its Chinese counterpart. That row has a stray | before · compat.xxhash, which makes it three cells under a two-column header, and GitHub drops the third cell. compat.xxhash does not appear on the rendered page today, and the new entry would not either. I removed that | in both files and closed the row with |. The compat.sqlitecpp and compat.reflectcpp entries sit behind the same stray | in their rows. I left those alone to keep this PR to one package, and I can fix them here if you prefer.

Verification

Run locally with mcpp 2026.9.21.3. validate.yml has since moved to 2026.9.25.1, which #465 says changes no descriptor grammar.

Lint: the per-file checks from validate.yml all pass for the new descriptor: Lua syntax, required fields, no leading v, check_mirror_urls.lua, check_package_name.lua and the c++fly check. check_cross_package_refs.lua, check_platform_version_parity.lua, check_duplicate_versions.lua and compat.py selftest pass for the whole tree.

$ mcpp xpkg parse --all-os pkgs/c/compat.quickjs-ng.lua
parse OK [linux]  sources 4  includes 1  generated 1
parse --   [macosx]  (no xpm versions declared)
parse --   [windows]  (no xpm versions declared)

On a cold run the member fetched the tarball from the url in the descriptor. The fetched file matched the declared sha256 and was byte-identical to a separate download of the same url. With the build cache off:

$ mcpp test -p quickjs-ng --toolchain llvm@22.1.8 --cache off
   Compiling quickjs-ng-tests v0.1.0 (.)
   Compiling compat.quickjs-ng v0.17.0
   Compiling engine (test)
     Running bin/engine
QuickJS-ng 0.17.0
engine ... ok (0.02s)
 test result ok. 1 passed; 0 failed

It also passes with --profile release, where the package's TUs build at -O2.

The package's compile line for quickjs.c (include path shortened):

-I<pkg>/quickjs-0.17.0/mcpp/include -std=c11 -O0 -g ... -std=gnu11 -funsigned-char -D_GNU_SOURCE -DQUICKJS_NG_BUILD

A C consumer, a project whose only source is src/main.c and which therefore links through the C driver, evaluates String(Math.pow(2, 10) + Math.round(Math.sin(0))). Without the ldflags its link stops at undefined symbol: round (then lrint, floor and more). With them it links and prints 1024.

openkal: I made the member copy with tests/openkal/compat.py's own prepare(), which adds openkal-llvm-runtime 0.15.1 and the [indices] redirect, and ran the command command_for() produces, mcpp test --toolchain llvm@22.1.8:

x86_64-unknown-linux-gnu   c-abi musl (openkal-musl@0.19.1), kernel-abi openkal (openkal-linux@0.15.0)
  engine ... ok (0.02s)   test result ok. 1 passed; 0 failed

Before the ldflags were added, the same graph's aarch64-linux-musl target also passed under qemu-aarch64. With -lm it no longer links, and the cause is not in this package. The graph passes no sysroot, so clang's driver appends the build host's library directories (-L/lib/../lib64 -L/usr/lib64 -L/lib -L/usr/lib) to the cross link. On my machine /usr/lib/libm.a is glibc's linker script, OUTPUT_FORMAT(elf64-x86-64) GROUP(/usr/lib/libm-2.44.a /usr/lib/libmvec.a), and lld stops with is incompatible with elf64-x86-64. The same clang 22.1.8 fails the same way when it links an empty aarch64 object with --target=aarch64-unknown-linux-musl -nostdlib -static -lm, so I expect any descriptor that passes -lm to fail there too. On x86_64 the link finds that same file, but lld extracts nothing from it (checked with --why-extract), because the graph's own libc objects already define every math function. tests/openkal/pins.toml measures x86_64 targets only, so CI does not see the aarch64 case. I reported the link-side cause as mcpp-community/mcpp#696.

Negative check: I pointed include_dirs back at the tarball root and moved the dialect to c_standard = "gnu11". The member then stops at its guard, and the compile line shows -std=c11:

tests/engine.cpp:30:2: error: "compat.quickjs-ng put the engine's private headers on the include path"
error: test result: FAILED. 0 passed; 1 failed

After I restored the descriptor, the member passed again.

How to reproduce

mcpp xpkg parse --all-os pkgs/c/compat.quickjs-ng.lua
mcpp test -p quickjs-ng --toolchain llvm@22.1.8

The C consumer:

# mcpp.toml
[package]
name = "qjs-c"
version = "0.1.0"
[indices]
compat = { path = "<this checkout>" }
[dependencies.compat]
quickjs-ng = "0.17.0"
[targets.qjs-c]
kind = "bin"
main = "src/main.c"

/* src/main.c */
#include <stdio.h>
#include <string.h>
#include <quickjs.h>
int main(void) {
    JSRuntime *rt = JS_NewRuntime();
    JSContext *ctx = JS_NewContext(rt);
    const char *src = "String(Math.pow(2, 10) + Math.round(Math.sin(0)))";
    JSValue v = JS_Eval(ctx, src, strlen(src), "<c>", JS_EVAL_TYPE_GLOBAL);
    const char *s = JS_ToCString(ctx, v);
    int ok = s && strcmp(s, "1024") == 0;
    printf("%s\n", s ? s : "(null)");
    JS_FreeCString(ctx, s);
    JS_FreeValue(ctx, v);
    JS_FreeContext(ctx);
    JS_FreeRuntime(rt);
    return ok ? 0 : 1;
}

mcpp build --toolchain llvm@22.1.8 && ./target/*/*/bin/qjs-c

Tarball: https://github.com/quickjs-ng/quickjs/archive/refs/tags/v0.17.0.tar.gz, sha256 559bc4c420475e55c7ab4510adbc562f55d7524d75e8e89d79ce4bb02f5687d9. Tag v0.17.0 is commit 6d46d07d04041b40f4f49eaa7fdebe44c314c699, released 2026-09-18.

The four engine TUs upstream builds as its library, with the flags
upstream's CMake and meson builds set, and libm and the thread library
on the linux link line because upstream makes them PUBLIC; quickjs-libc
is not built. quickjs.h reaches consumers through a forwarder, so the
engine's private headers stay off the include path. Linux only,
plain-string url (no CN mirror). The member evaluates JavaScript and
checks each compiled source file by value. The catalog row it joins
loses a stray `|` that hid its last entry on GitHub.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Sunrisepeak
Sunrisepeak merged commit 57f377e into mcpplibs:main Sep 25, 2026
16 checks passed
@yspbwx2010
yspbwx2010 deleted the feat/compat-quickjs-ng branch September 25, 2026 16:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants