Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .agents/docs/2026-10-27-add-lua-quickjs-mwrap-plan.md
Original file line number Diff line number Diff line change
@@ -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
`<lua.h>`, `<lauxlib.h>` and `<lualib.h>` (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 `<mwrap/lua/macros.hpp>` (= `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 `<quickjs.h>` (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 `<mwrap/quickjs_ng/macros.hpp>`: `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.
10 changes: 10 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
38 changes: 38 additions & 0 deletions pkgs/mwrap/lua.lua
Original file line number Diff line number Diff line change
@@ -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",
},
},
},
}
38 changes: 38 additions & 0 deletions pkgs/mwrap/quickjs_ng.lua
Original file line number Diff line number Diff line change
@@ -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",
},
},
},
}
11 changes: 11 additions & 0 deletions tests/mwrap/lua-embed/mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"
42 changes: 42 additions & 0 deletions tests/mwrap/lua-embed/tests/behavior.cpp
Original file line number Diff line number Diff line change
@@ -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<double*>(lua_newuserdatauv(L, sizeof(double), 0)) = 2.5;
luaL_setmetatable(L, "Box");
if (*static_cast<double*>(luaL_checkudata(L, -1, "Box")) != 2.5) return 5;
lua_close(L);
std::println("lua-embed: ok ({})", LUA_RELEASE);
return 0;
}
11 changes: 11 additions & 0 deletions tests/mwrap/quickjs-embed/mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"
58 changes: 58 additions & 0 deletions tests/mwrap/quickjs-embed/tests/behavior.cpp
Original file line number Diff line number Diff line change
@@ -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(), "<code>", 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(), "<bad>", 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(), "<promise>", 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;
}
Loading