diff --git a/.agents/docs/2026-10-27-add-lua-quickjs-mwrap-plan.md b/.agents/docs/2026-10-27-add-lua-quickjs-mwrap-plan.md new file mode 100644 index 00000000..9fc85d01 --- /dev/null +++ b/.agents/docs/2026-10-27-add-lua-quickjs-mwrap-plan.md @@ -0,0 +1,84 @@ +# Add mcpplibs.mwrap.lua (lua-m 5.4.7) and mcpplibs.mwrap.quickjs_ng (quickjs-ng-m 0.17.0) + +## Form + +- Lua: third-party upstream [lua.org](https://www.lua.org), release 5.4.7, MIT. The tarball + (`9fbf5e28…cfbf1e30`) is the one compat.lua 5.4.7 already pins. +- QuickJS-ng: third-party upstream [quickjs-ng/quickjs](https://github.com/quickjs-ng/quickjs), tag `v0.17.0`, + MIT. The tarball (`559bc4c4…5687d9`) is the one compat.quickjs-ng 0.17.0 already pins. +- Form A external mwrap module repositories: [mcpplibs/lua-m](https://github.com/mcpplibs/lua-m) (package + `mcpplibs.mwrap.lua`) and [mcpplibs/quickjs-ng-m](https://github.com/mcpplibs/quickjs-ng-m) (package + `mcpplibs.mwrap.quickjs_ng`, one atomic segment, as godot-cpp-m is `godot_cpp`). Versions are the upstream + ones (first release of each identity, no revision suffix); tag series `mwrap-${version}`. +- Implementation ownership: the libraries are compat.lua's and compat.quickjs-ng's (their configuration and ABI + are sufficient). Neither wrapper vendors upstream code; each retains upstream's README and license under + `upstream/` with checksums in `upstream/documents.json`. No implementation is linked twice. + +## Modules + +### mcpplibs.mwrap.lua + +- `mcpplibs.mwrap.lua`: every function (`LUA_API`, `LUALIB_API`, `LUAMOD_API`), type and `lua_ident` of + ``, `` and `` (171 declarations) by `export using` over `lua.hpp`. +- `mcpplibs.mwrap.lua.macros`: 135 macros in `mcpplibs::mwrap::lua::macros`. Constants are `inline constexpr`, + including the luaconf.h limits public macros are made of. Function-like macros are functions that evaluate + their arguments as the macro does. `lua_pushliteral` and `luaL_newlib` are templates over an array. +- Stay macros, in `` (= `lua.hpp`): `luaL_argcheck`, `luaL_argexpected` and `luaL_opt` + (lazily evaluated arguments), `luaL_intop` (a pasted operator), and the standalone interpreter's + `lua_write*`. Not mapped: the include guards and the `LUA_COMPAT_APIINTCASTS` aliases this build does not + define. + +### mcpplibs.mwrap.quickjs_ng + +- `mcpplibs.mwrap.quickjs_ng`: every type, enumerator of a named enum and function of `` (45, 33 + and 264), generated from the pinned header by `tools/gen_exports.py`. +- The 27 public `static inline` functions (`JS_NewInt32`, `JS_IsException`, `JS_ToCString`, `JS_NewCFunction` + …) have internal linkage and cannot be exported. They are adapted under their names and signatures in + `mcpplibs::mwrap::quickjs_ng`, each defined out of line by calling upstream's own inline body. +- `mcpplibs.mwrap.quickjs_ng.macros`: 130 macros. Constants are `inline constexpr` of each macro's type. The + `JS_TAG_*` enumerators of the header's unnamed (TU-local) enum are constants. The value representation is + constexpr: `JS_UNDEFINED` and friends are `constexpr JSValue`, while the header's are calls in C++. The + `JS_*_DEF` initialisers are constexpr functions, so property tables stay constant-initialised. +- Stay macros, in ``: `JS_CFUNC_SPECIAL_DEF` (token pasting) and + `JS_FreeValues` / `JS_FreeAtoms` (each freed before the next argument is evaluated). The struct + representation of `JSValue` (64-bit targets) is the one supported; the NaN-boxed one is refused at compile + time. + +Each repository's `tools/check_exports.py` parses the pinned headers and fails when a declaration is not +exported or a macro is neither mapped nor listed with its reason. Both were negative-checked by removing an +export and a mapping. + +## Verification + +Wrapper CI (Linux GCC 16.1 and LLVM 23.1.3, macOS LLVM, Windows LLVM, all required): build, gtest suites (the +API through the module, the macro constants, the header mixed with the import), the export audit, and +upstream's own tests through an example written against the module: + +- lua-m: `lua-5.4.7-tests` (the portable user tests, `lua -e"_U=true" all.lua`) on `examples/lua`, a standalone + interpreter: "final OK". The interpreter is built optimised, as upstream's is: an unoptimised build + overflowed Windows' 1 MB stack in the stack-overflow tests. +- quickjs-ng-m: the release's `tests/*.js` on `examples/qjs` (file imports, import attributes, print/gc), each + test's frontmatter honoured as `run-test262` does: 77 pass. Left out are the 9 that `tests.conf` excludes + and the 10 that import quickjs-libc, which compat.quickjs-ng does not build. On Windows the host links an + 8 MB stack, as upstream links its programs (the 1 MB default equals the JS stack limit). + +The templates jobs scaffold each default `script` template as an installed package and run it. + +Here, with the CI-pinned mcpp 2026.10.26.1 and `MCPP_INDEX_MIRROR=GLOBAL`: + +- `bash tests/run_members.sh lua-embed quickjs-embed`: the real release archives through the candidate index. + The consumers call C functions and closures from the scripts, read errors, and use userdata, classes and + property tables: `test result ok`. +- `python3 tests/mwrap/check_installed_template.py --package mcpplibs.mwrap.lua@5.4.7` (and `quickjs_ng@0.17.0`): + "Verified real indexed archive installation and unmodified template consumer". + +## Mirror and updates + +No `mcpp-res` access in this change, so the descriptors use the GitHub release URLs as plain strings (CN users +fall back to them); a maintainer can add the GitCode mirror with identical bytes. `extensions.autoupdate` +follows each repository's own `mwrap-${version}` tags. + +## Motivation + +openmui's script bindings (openmui.lang.lua, openmui.lang.js) embed Lua and QuickJS-ng. Through these modules, +their sources import the engines instead of including C headers in native units. diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 3033cdc4..a7171696 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1608,6 +1608,16 @@ jobs: shell: bash run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.md4c@0.5.3 + - name: Installed Lua wrapper archive and actual default script template + if: contains(format(' {0} ', env.MEMBERS), ' lua-embed ') && matrix.toolchain == 'default' + shell: bash + run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.lua@5.4.7 + + - name: Installed QuickJS-ng wrapper archive and actual default script template + if: contains(format(' {0} ', env.MEMBERS), ' quickjs-embed ') && matrix.toolchain == 'default' + shell: bash + run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.quickjs_ng@0.17.0 + - name: Installed Godot wrapper archive and actual standalone math template if: contains(format(' {0} ', env.MEMBERS), ' godot-math ') && matrix.toolchain == 'default' shell: bash diff --git a/mcpp.toml b/mcpp.toml index 095f9cfc..40eb2647 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -17,6 +17,8 @@ members = [ "tests/mwrap/ffmpeg-cpu", "tests/mwrap/imgui-headless", "tests/mwrap/md4c-render", + "tests/mwrap/lua-embed", + "tests/mwrap/quickjs-embed", "tests/mwrap/grpc-cpu", "tests/mwrap/grpc-tools", "tests/mwrap/godot-math", diff --git a/pkgs/mwrap/lua.lua b/pkgs/mwrap/lua.lua new file mode 100644 index 00000000..e22ee5ec --- /dev/null +++ b/pkgs/mwrap/lua.lua @@ -0,0 +1,38 @@ +-- Immutable upstream-aligned mwrap source release (lua-m mwrap-5.4.7). +-- The CN mirror is added by a maintainer with mcpp-res access; until then CN +-- users fall back to the GitHub release (docs/cn-mirror.md). +package = { + spec = "1", + namespace = "mcpplibs.mwrap", + name = "lua", + description = "lua-m: Lua 5.4 (the embeddable scripting language) as C++23 modules", + licenses = { "Apache-2.0" }, + repo = "https://github.com/mcpplibs/lua-m", + type = "package", + extensions = { + autoupdate = { + tag = "mwrap-${version}", + url = "https://github.com/mcpplibs/lua-m/releases/download/${tag}/lua-m-${version}.tar.gz", + }, + }, + xpm = { + linux = { + ["5.4.7"] = { + url = "https://github.com/mcpplibs/lua-m/releases/download/mwrap-5.4.7/lua-m-5.4.7.tar.gz", + sha256 = "75716263adb28f79e01048c73e8a7193bae5fb68baf18d93d937706b9ca7c1d5", + }, + }, + macosx = { + ["5.4.7"] = { + url = "https://github.com/mcpplibs/lua-m/releases/download/mwrap-5.4.7/lua-m-5.4.7.tar.gz", + sha256 = "75716263adb28f79e01048c73e8a7193bae5fb68baf18d93d937706b9ca7c1d5", + }, + }, + windows = { + ["5.4.7"] = { + url = "https://github.com/mcpplibs/lua-m/releases/download/mwrap-5.4.7/lua-m-5.4.7.tar.gz", + sha256 = "75716263adb28f79e01048c73e8a7193bae5fb68baf18d93d937706b9ca7c1d5", + }, + }, + }, +} diff --git a/pkgs/mwrap/quickjs_ng.lua b/pkgs/mwrap/quickjs_ng.lua new file mode 100644 index 00000000..fcd4d2c7 --- /dev/null +++ b/pkgs/mwrap/quickjs_ng.lua @@ -0,0 +1,38 @@ +-- Immutable upstream-aligned mwrap source release (quickjs-ng-m mwrap-0.17.0). +-- The CN mirror is added by a maintainer with mcpp-res access; until then CN +-- users fall back to the GitHub release (docs/cn-mirror.md). +package = { + spec = "1", + namespace = "mcpplibs.mwrap", + name = "quickjs_ng", + description = "quickjs-ng-m: QuickJS-ng (the small embeddable JavaScript engine) as C++23 modules", + licenses = { "Apache-2.0" }, + repo = "https://github.com/mcpplibs/quickjs-ng-m", + type = "package", + extensions = { + autoupdate = { + tag = "mwrap-${version}", + url = "https://github.com/mcpplibs/quickjs-ng-m/releases/download/${tag}/quickjs-ng-m-${version}.tar.gz", + }, + }, + xpm = { + linux = { + ["0.17.0"] = { + url = "https://github.com/mcpplibs/quickjs-ng-m/releases/download/mwrap-0.17.0/quickjs-ng-m-0.17.0.tar.gz", + sha256 = "33d95449080f2634611101b24afbb7d996c462fb3939698b76d10c0f7c87aca9", + }, + }, + macosx = { + ["0.17.0"] = { + url = "https://github.com/mcpplibs/quickjs-ng-m/releases/download/mwrap-0.17.0/quickjs-ng-m-0.17.0.tar.gz", + sha256 = "33d95449080f2634611101b24afbb7d996c462fb3939698b76d10c0f7c87aca9", + }, + }, + windows = { + ["0.17.0"] = { + url = "https://github.com/mcpplibs/quickjs-ng-m/releases/download/mwrap-0.17.0/quickjs-ng-m-0.17.0.tar.gz", + sha256 = "33d95449080f2634611101b24afbb7d996c462fb3939698b76d10c0f7c87aca9", + }, + }, + }, +} diff --git a/tests/mwrap/lua-embed/mcpp.toml b/tests/mwrap/lua-embed/mcpp.toml new file mode 100644 index 00000000..c69217aa --- /dev/null +++ b/tests/mwrap/lua-embed/mcpp.toml @@ -0,0 +1,11 @@ +[package] +name = "lua-embed" +version = "5.4.7" +description = "Consume the immutable Lua wrapper: a C function and a closure called from Lua, errors, userdata, the mapped macros" + +[indices] +# Use the complete candidate built-in index prepared by tests/run_members.sh. +# An explicit empty table overrides the legacy workspace redirect. + +[dependencies] +"mcpplibs.mwrap.lua" = "5.4.7" diff --git a/tests/mwrap/lua-embed/tests/behavior.cpp b/tests/mwrap/lua-embed/tests/behavior.cpp new file mode 100644 index 00000000..e8177970 --- /dev/null +++ b/tests/mwrap/lua-embed/tests/behavior.cpp @@ -0,0 +1,42 @@ +import std; +import mcpplibs.mwrap.lua; + +using namespace mcpplibs::mwrap::lua::macros; + +namespace { +int add(lua_State* L) { + lua_pushinteger(L, luaL_checkinteger(L, 1) + luaL_checkinteger(L, 2)); + return 1; +} +int counter(lua_State* L) { + lua_pushinteger(L, lua_tointeger(L, lua_upvalueindex(1)) + 1); + lua_copy(L, -1, lua_upvalueindex(1)); + return 1; +} +} // namespace + +int main() { + lua_State* L = luaL_newstate(); + luaL_openlibs(L); + lua_register(L, "add", add); + lua_pushinteger(L, 0); + lua_pushcclosure(L, counter, 1); + lua_setglobal(L, "count"); + // A chunk calling a C function and a closure; the result read back. + if (luaL_dostring(L, "count(); return add(40, count()), _VERSION")) return 1; + if (lua_tointeger(L, -2) != 42 || std::string_view(lua_tostring(L, -1)) != LUA_VERSION) return 2; + lua_pop(L, 2); + // An error through lua_pcall, with its message. + if (luaL_loadstring(L, "return add('x', 1)") != LUA_OK || lua_pcall(L, 0, 1, 0) != LUA_ERRRUN) return 3; + if (!std::string_view(lua_tostring(L, -1)).contains("bad argument #1")) return 4; + lua_pop(L, 1); + // Userdata with a metatable from the registry. + luaL_newmetatable(L, "Box"); + lua_pop(L, 1); + *static_cast(lua_newuserdatauv(L, sizeof(double), 0)) = 2.5; + luaL_setmetatable(L, "Box"); + if (*static_cast(luaL_checkudata(L, -1, "Box")) != 2.5) return 5; + lua_close(L); + std::println("lua-embed: ok ({})", LUA_RELEASE); + return 0; +} diff --git a/tests/mwrap/quickjs-embed/mcpp.toml b/tests/mwrap/quickjs-embed/mcpp.toml new file mode 100644 index 00000000..ccfceb9a --- /dev/null +++ b/tests/mwrap/quickjs-embed/mcpp.toml @@ -0,0 +1,11 @@ +[package] +name = "quickjs-embed" +version = "0.17.0" +description = "Consume the immutable QuickJS-ng wrapper: a C function and a property table called from JavaScript, exceptions, promises, a class" + +[indices] +# Use the complete candidate built-in index prepared by tests/run_members.sh. +# An explicit empty table overrides the legacy workspace redirect. + +[dependencies] +"mcpplibs.mwrap.quickjs_ng" = "0.17.0" diff --git a/tests/mwrap/quickjs-embed/tests/behavior.cpp b/tests/mwrap/quickjs-embed/tests/behavior.cpp new file mode 100644 index 00000000..f68754a0 --- /dev/null +++ b/tests/mwrap/quickjs-embed/tests/behavior.cpp @@ -0,0 +1,58 @@ +import std; +import mcpplibs.mwrap.quickjs_ng; + +using namespace mcpplibs::mwrap::quickjs_ng; +using namespace mcpplibs::mwrap::quickjs_ng::macros; + +namespace { +JSValue add(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) { + std::int64_t a = 0, b = 0; + if (argc < 2 || JS_ToInt64(ctx, &a, argv[0]) || JS_ToInt64(ctx, &b, argv[1])) return JS_ThrowTypeError(ctx, "add(a, b)"); + return JS_NewInt64(ctx, a + b); +} +constexpr JSCFunctionListEntry table[] = { + JS_CFUNC_DEF("add", 2, add), + JS_PROP_STRING_DEF("name", "maths", JS_PROP_CONFIGURABLE), +}; +std::string text(JSContext* ctx, JSValueConst v) { + const char* s = JS_ToCString(ctx, v); + std::string out = s ? s : ""; + JS_FreeCString(ctx, s); + return out; +} +} // namespace + +int main() { + JSRuntime* rt = JS_NewRuntime(); + JSContext* ctx = JS_NewContext(rt); + JSValue global = JS_GetGlobalObject(ctx); + JSValue maths = JS_NewObject(ctx); + if (JS_SetPropertyFunctionList(ctx, maths, table, 2) != 0) return 1; + JS_SetPropertyStr(ctx, global, "maths", maths); + JS_FreeValue(ctx, global); + // A script calling the table's C function. + constexpr std::string_view code = "`${maths.name}: ${maths.add(40, 2)}`"; + JSValue v = JS_Eval(ctx, code.data(), code.size(), "", JS_EVAL_TYPE_GLOBAL); + if (JS_IsException(v) || text(ctx, v) != "maths: 42") return 2; + JS_FreeValue(ctx, v); + // An exception, with its message. + constexpr std::string_view bad = "maths.add('x')"; + v = JS_Eval(ctx, bad.data(), bad.size(), "", JS_EVAL_TYPE_GLOBAL); + if (!JS_IsException(v)) return 3; + JSValue e = JS_GetException(ctx); + if (text(ctx, e) != "TypeError: add(a, b)") return 4; + JS_FreeValue(ctx, e); + // A promise, settled by running the jobs. + constexpr std::string_view promise = "Promise.resolve(20).then(x => x * 2 + 2)"; + v = JS_Eval(ctx, promise.data(), promise.size(), "", JS_EVAL_TYPE_GLOBAL); + for (JSContext* job = nullptr; JS_IsJobPending(rt);) JS_ExecutePendingJob(rt, &job); + if (JS_PromiseState(ctx, v) != JS_PROMISE_FULFILLED) return 5; + JSValue r = JS_PromiseResult(ctx, v); + if (text(ctx, r) != "42") return 6; + JS_FreeValue(ctx, r); + JS_FreeValue(ctx, v); + std::println("quickjs-embed: ok (QuickJS-ng {})", JS_GetVersion()); + JS_FreeContext(ctx); + JS_FreeRuntime(rt); + return JS_VALUE_GET_TAG(JS_UNDEFINED) == JS_TAG_UNDEFINED ? 0 : 7; +}