From 86f900534e1ec4bc6bcda62fd42c21bf560fdfdf Mon Sep 17 00:00:00 2001 From: Cloud_Yun Date: Sun, 30 Aug 2026 19:32:30 +0900 Subject: [PATCH] feat(pkg): compat.wamr 2.4.5 (WebAssembly Micro Runtime) C-source compat, interpreter runtime only, with libc-builtin and libc-wasi behind features. Linux section only; consumers gate the dependency the way compat.libaio does. The descriptor solves one problem the schema does not have a hook for. WAMR needs architecture-specific input twice: a BUILD_TARGET_* define, without which wasm_runtime_common.c compiles its whole invoke-native section away and the link fails on invokeNative, and the implementation of that symbol, which is hand-written assembly per architecture. `sources` and `cflags` vary per OS, not per architecture, and `archs` is package metadata rather than a selector, so both decisions are handed to the preprocessor instead: a generated config header maps __x86_64__/__aarch64__ to the matching BUILD_TARGET_* and reaches every TU through -include, and a generated .S includes the chosen assembly file as text. The dispatcher has to be .S because upstream's arch/invokeNative_*.s are lowercase, which clang assembles without the preprocessor -- so unlike libffi's .S files they cannot guard themselves. Two further notes carried in the descriptor comments: -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 platform files upstream drops when WASI is off are carried unconditionally rather than `!`-excluded and re-added by the feature, because an exclusion glob is global and beats the feature's own entry for the same file (that arrangement compiled and then failed to link on os_file_get_access_mode and friends). Two members. tests/examples/wamr runs a hand-assembled module whose export calls back into a host native function -- compute(6,7) returns 42 only if invokeNative works -- and asserts the negative half of the feature gate: env.putchar must be refused. tests/examples/wamr-features asserts the positive half for both features. The gate has to be tested by calling, not by instantiating: WAMR does not reject an unresolved import at load time, it warns and defers, and the refusal only appears on the call. Verified with the pinned mcpp 2026.8.27.2 from a cleaned target/, .mcpp/ and global build cache, so both members show `Compiling compat.wamr` rather than `Cached`. Design notes in .agents/docs/2026-08-30-add-wamr-plan.md. No CN mirror: no mcpp-res write access, so `url` uses the plain-string upstream form and CN users fall back to GitHub. Co-Authored-By: Claude Opus 5 (1M context) --- .agents/docs/2026-08-30-add-wamr-plan.md | 258 +++++++++++++++++ docs/descriptor-examples.md | 1 + docs/zh/descriptor-examples.md | 1 + mcpp.toml | 2 + pkgs/c/compat.wamr.lua | 264 ++++++++++++++++++ tests/examples/wamr-features/mcpp.toml | 11 + .../wamr-features/tests/libc_features.cpp | 166 +++++++++++ tests/examples/wamr/mcpp.toml | 15 + tests/examples/wamr/tests/run_module.cpp | 176 ++++++++++++ 9 files changed, 894 insertions(+) create mode 100644 .agents/docs/2026-08-30-add-wamr-plan.md create mode 100644 pkgs/c/compat.wamr.lua create mode 100644 tests/examples/wamr-features/mcpp.toml create mode 100644 tests/examples/wamr-features/tests/libc_features.cpp create mode 100644 tests/examples/wamr/mcpp.toml create mode 100644 tests/examples/wamr/tests/run_module.cpp diff --git a/.agents/docs/2026-08-30-add-wamr-plan.md b/.agents/docs/2026-08-30-add-wamr-plan.md new file mode 100644 index 0000000..ae8dcfa --- /dev/null +++ b/.agents/docs/2026-08-30-add-wamr-plan.md @@ -0,0 +1,258 @@ +# Adding compat.wamr (WebAssembly Micro Runtime 2.4.5) + +Shape decision, mirror status, feature evaluation, verification results, and the +things a later reader should not have to rediscover. + +## Source and shape + +Upstream is [bytecodealliance/wasm-micro-runtime](https://github.com/bytecodealliance/wasm-micro-runtime), +Apache-2.0 WITH LLVM-exception, pure C. Latest tag at time of writing is +`WAMR-2.4.5` (`git ls-remote --tags | sort -V | tail`). It offers no mcpp +support, so this is case (a) — a third-party upstream adapted as `compat`. + +Shape is **C-source compat**, the same template as `compat.mbedtls`: a source +glob compiled into one static archive with the public headers exposed. The +tarball wraps everything in `wasm-micro-runtime-WAMR-2.4.5/`, absorbed by the +leading `*/` in every glob. + +`sha256 = 1ab09d51099f276ca4a1d6629f6b589aab2bd0caa01445e05031a4bed22c199b`, +computed twice on separate downloads to rule out a repacking archive source. + +## The one thing this shape could not express: per-architecture selection + +WAMR needs architecture-specific input in two places, and the descriptor schema +has a per-OS hook but no per-architecture one (`archs` is package metadata that +declares support, not a selector). + +1. **A `BUILD_TARGET_*` define.** The whole invoke-native section of + `core/iwasm/common/wasm_runtime_common.c` sits inside + `#if defined(BUILD_TARGET_X86_64) || defined(BUILD_TARGET_AMD_64) || + defined(BUILD_TARGET_AARCH64) || …`. With none defined the file still + compiles — it just contains no `invokeNative` caller — and the failure + arrives at link time. Upstream's CMake sets the right one from + `WAMR_BUILD_TARGET`. +2. **The `invokeNative` implementation**, which is hand-written assembly, one + file per architecture under `core/iwasm/common/arch/`. + +Both are resolved by moving the decision into the preprocessor, which does know +the target: + +* `generated_files` emits `mcpp_generated/include/mcpp_wamr_config.h`, mapping + `__x86_64__`/`__aarch64__` to the matching `BUILD_TARGET_*` and erroring out + on anything else. It reaches every TU through `cflags = { "-include", + "mcpp_wamr_config.h" }`, the mechanism `compat.zlib` already uses for + `Z_HAVE_UNISTD_H`. +* `generated_files` also emits `mcpp_generated/mcpp_wamr_invoke_native.S`, + which `#include`s the chosen `arch/invokeNative_*.s` as text. + +**Why the dispatcher must be `.S` and the upstream files could not simply be +listed.** `compat.libffi` lists `src/x86/unix64.S` and friends directly and lets +each guard itself, because those are uppercase `.S` — clang runs the +preprocessor on them. WAMR's are lowercase `.s`, which clang assembles with no +preprocessing at all, so an `#ifdef` inside them is not evaluated. (They do +contain `#ifndef BH_PLATFORM_DARWIN`, which upstream's build does honour; that +guard starts working again once the file is pulled into a `.S`.) Listing all of +them is also not an option: each defines `invokeNative`, and the aarch64 file +does not assemble on x86_64. + +Verified directly, before any mcpp involvement: + +``` +$ clang -c mcpp_wamr_invoke_native.S -I…/common/arch -o x64.o && nm x64.o | grep -i invokenative +0000000000000000 T invokeNative +$ clang --target=aarch64-unknown-linux-gnu -c mcpp_wamr_invoke_native.S -I…/common/arch -o a64.o && nm a64.o | grep -i invokenative +0000000000000000 T invokeNative +``` + +## `-std=gnu11` through cflags, not `c_standard` + +`core/shared/platform/linux/platform_internal.h` writes the GS base with a bare +`asm volatile`. `c_standard = "c11"` emits `-std=c11`, which defines +`__STRICT_ANSI__`, under which `asm` is not a keyword — only `__asm__` is — and +`wasm_memory.c` and `wasm_interp_fast.c` both fail with *use of undeclared +identifier 'asm'*. Appending `-std=gnu11` via `cflags` wins over the earlier +flag and matches how upstream builds. + +The alternative is `-DWASM_DISABLE_WRITE_GS_BASE=1`, a real upstream knob that +also compiles cleanly (both call sites are the only bare-`asm` uses in the +build). It was not chosen: it turns off an x86_64 fast path to work around a +language-mode choice, which is a behaviour change made for a formatting reason. + +This is adjacent to the `compat.libaio` lesson — there `-std=c11` hid +`syscall()` and `sigset_t` behind `__STRICT_ANSI__` disabling `_DEFAULT_SOURCE`, +fixed with `-D_GNU_SOURCE`. Same macro, different consequence; `_GNU_SOURCE` +does not help with the `asm` keyword. + +## Linux only, on purpose + +`xpm` carries a `linux` section and nothing else, and consumers gate with +`[target.'cfg(linux)'.dependencies]` — the `compat.libaio` shape. WAMR itself is +portable: there are `core/shared/platform/darwin` and `…/windows` trees, and the +assembly files carry Darwin guards that this descriptor's `.S` dispatcher would +honour. macOS is likely to be a small delta. But neither macOS nor Windows was +built or run here, and Windows additionally has an unresolved choice between +upstream's MASM `.asm` and the MinGW `.s` variant. Declaring a platform because +it looks symmetric is how a red CI job gets iterated on blind, so the sections +are left for someone who can verify them. + +The single-platform `xpm` keeps `check_platform_version_parity.lua` quiet by +design: it only compares platforms that both carry versions. + +## What the base is, and why + +Interpreter runtime only — classic and fast interpreter, bulk memory and +reference types (both on in upstream's own default configuration), no AOT, no +JIT, no guest-facing libc. That is what an embedder wants when wasm modules are +plugins reached only through host-provided imports, and it keeps AOT's LLVM +dependency out of the index entirely. + +Sources follow upstream's cmake fragments: `iwasm/common/*.c` +(`iwasm_common.cmake`), the loader + runtime + one interpreter from +`iwasm_interp.cmake`, `shared/platform/linux/*.c` plus `platform/common/posix` +(`shared_platform.cmake` → `platform_api_posix.cmake`), `mem_alloc.c` + `ems/` +(`mem_alloc.cmake` — its `tlsf/` glob matches nothing in 2.4.5), and +`shared/utils/*.c` (`shared_utils.cmake`). + +`wasm_mini_loader.c` and `wasm_interp_classic.c` are deliberately absent: they +are upstream's *alternatives* to `wasm_loader.c` and `wasm_interp_fast.c`, not +additions, and listing both would define the same symbols twice. + +## Features + +Both were evaluated against the ABI rule that keeps `compat.recastnavigation`'s +`DT_POLYREF64` out of a feature: a feature's `defines` reach only the package's +own TUs, so a macro that changes the layout of a type crossing the library +boundary cannot be a feature. Neither macro here does — `WASM_ENABLE_LIBC_WASI` +and `WASM_ENABLE_LIBC_BUILTIN` appear **zero** times in `core/iwasm/include/` +outside comments, so a consumer compiling without them sees the same +`wasm_export.h` the library was built against. + +* **`libc-builtin`** — `libraries/libc-builtin/*.c` plus + `WASM_ENABLE_LIBC_BUILTIN=1`. One TU. +* **`libc-wasi`** — `libraries/libc-wasi/**/*.c` plus `WASM_ENABLE_LIBC_WASI=1` + **and `WASM_ENABLE_MODULE_INST_CONTEXT=1`**. The second define is not + optional and not obvious: `build-scripts/runtime_lib.cmake:97-99` sets + `WAMR_BUILD_MODULE_INST_CONTEXT` whenever `WAMR_BUILD_LIBC_WASI` is on, and + without it `wasm_native.c` calls `wasm_native_get_context`, + `wasm_native_set_context` and the context-key helpers that `wasm_native.h` + only declares under that macro. + +### Two things the feature table cannot do, and what was done instead + +**A feature cannot carry `include_dirs`.** With `WASM_ENABLE_LIBC_WASI=1`, +`common/wasm_runtime_common.h` itself opens `#include "posix.h"`, so it is not +only the feature's own sources that need the WASI header roots — +`wasm_loader.c` and `wasm_runtime.c` stop compiling without them. The five +header roots therefore live in the base `include_dirs`, where they cost nothing +when the features are off (they are `-I` paths into directories no base source +includes from). + +**An exclusion glob is global and beats a feature's own entry for the same +file.** Upstream's `platform_api_posix.cmake` drops `posix_file.c`, +`posix_clock.c` and `posix_socket.c` (and pulls in `libc-util`) only when WASI +is on, and the natural translation is `!`-exclusions in the base with the +feature adding them back. That produced a build that compiled and then failed +to link: + +``` +ld.lld: error: undefined symbol: os_file_get_access_mode +ld.lld: error: undefined symbol: os_is_dir_stream_valid +ld.lld: error: undefined symbol: os_closedir +``` + +Each was referenced from `libc-wasi/sandboxed-system-primitives/src/posix.c`, +i.e. the feature's own sources, while the file defining them stayed excluded. +All four files were then compiled individually **with WASI off** and all four +succeeded, so the base simply carries them unconditionally. The cost is a few KB +of unreferenced objects the linker drops. + +## Verification + +CI's pinned mcpp (`MCPP_VERSION: 2026.8.27.2`, matching `index.toml`'s +`min_mcpp`) was used throughout, with `MCPP_INDEX_MIRROR=GLOBAL`. + +Descriptor: + +``` +$ mcpp xpkg parse pkgs/c/compat.wamr.lua +package compat.wamr (namespace 'compat') +versions linux 2.4.5 +sources 11 includes 16 +generated mcpp_generated/include/mcpp_wamr_config.h (679 bytes) +generated mcpp_generated/mcpp_wamr_invoke_native.S (724 bytes) +target wamr features 2 +parse OK +``` + +Lint, reproduced locally: lua syntax, required fields, no leading `v`, +`check_mirror_urls.lua`, `check_package_name.lua`, no `c++fly` — plus +`check_cross_package_refs.lua`, `check_duplicate_versions.lua` and +`check_platform_version_parity.lua`. All pass. + +Members, from a cleaned `target/` and `.mcpp/` **and a cleared global build +cache**, so both runs show `Compiling compat.wamr` rather than `Cached`: + +``` +$ mcpp test -p wamr +compute(6,7) = 42 (expected 42) +expecting env.putchar to be unlinked (libc-builtin is off): + exception: Exception: failed to call unlinked import function (env, putchar) +libc-builtin gated out: yes +run_module ... ok + test result ok. 1 passed; 0 failed + +$ mcpp test -p wamr-features +libc-builtin load ok +libc-builtin instantiate ok +libc-builtin putchar call ok +libc-wasi load ok +libc-wasi instantiate ok + call=0 exception=Exception: wasi proc exit exit_code=3 +libc-wasi proc_exit linked ok +libc-wasi exit code ok +libc_features ... ok + test result ok. 1 passed; 0 failed +``` + +### What the tests actually assert, and one dead end + +Both modules are hand-assembled wasm bytes in the test source, so nothing here +needs a wasm toolchain at build time. + +`tests/examples/wamr` runs a module that calls **back into the host**: +`compute(6,7)` returns 42 only because a native `host_mul` was invoked through +`invokeNative`. That is the assertion that notices a missing `BUILD_TARGET_*` or +a mis-dispatched assembly file; a "does it link" test would not. + +The feature gate is asserted in both directions — positively in +`wamr-features`, negatively in `wamr`, where the same `env.putchar` module must +be refused. + +**The dead end worth recording:** the negative check was first written as *does +the module instantiate*. It does. WAMR does not reject an unresolved import at +load or instantiation — it logs `warning: failed to link import function (env, +putchar)` and carries on, and the refusal only materialises when the guest calls +the import, as `Exception: failed to call unlinked import function`. Any feature +gate test for this package has to call, not instantiate. + +Two smaller ones: `putchar_wrapper` returns **1**, not the character written, so +an assertion copied from C's `putchar` contract fails; and WAMR refuses to load +a module importing WASI apis unless it exports a memory (*"a module with WASI +apis must export memory by default"*), so the WASI test module carries a +one-page memory and exports it. + +## CN mirror + +Not created — no `mcpp-res` write access. `url` uses the plain-string upstream +form, which lint accepts and which makes CN users fall back to GitHub. A +maintainer can promote it to `{ GLOBAL=…, CN=… }` after mirroring the identical +tarball. + +## Note on the skill's step 8 + +`.agents/skills/add-mcpp-index-package` says to add a row to the category table +in `README.md` and `README.zh-CN.md`. Those tables no longer exist — the README +now points at the online index site and keeps only a short "Reference examples" +list. The equivalent record was added to `docs/descriptor-examples.md` and +`docs/zh/descriptor-examples.md` instead, as a new shape row: *C-source compat +whose sources are chosen by architecture*. diff --git a/docs/descriptor-examples.md b/docs/descriptor-examples.md index 2e5a332..54ea5ed 100644 --- a/docs/descriptor-examples.md +++ b/docs/descriptor-examples.md @@ -15,6 +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) | | 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 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`) | C transport + the header-only C++ server on top of it | [`compat.usockets`](../pkgs/c/compat.usockets.lua) · [`compat.uwebsockets`](../pkgs/c/compat.uwebsockets.lua) (uSockets picks ONE event loop for all three platforms — libuv, via `compat.libuv` — because the alternative makes `us_loop_t` a different struct per platform for no gain; SSL and QUIC are left out so the base package's only dependency is that loop. The pair's real lesson is that `LIBUS_USE_LIBUV` / `LIBUS_NO_SSL` / `UWS_NO_ZLIB` are INTERFACE facts: `libusockets.h` changes the layout of `us_loop_t` under the first and gates its SSL declarations on the second, and uWS is header-only so its templates are instantiated in the CONSUMER's translation unit. An index descriptor's `cflags` reach only the package's own TUs, so every consumer must declare all three — a mismatch does not fail to build, it corrupts. The usockets test therefore writes to loop-attached extension memory from a timer callback and reads it back, which is exactly the assertion a layout disagreement breaks) | diff --git a/docs/zh/descriptor-examples.md b/docs/zh/descriptor-examples.md index d0f5269..0d13a6f 100644 --- a/docs/zh/descriptor-examples.md +++ b/docs/zh/descriptor-examples.md @@ -13,6 +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 的消费者链进去的是同一份而非两份)| | 原生多模块库(按 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(源码按**架构**选择) | [`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` 守着) | | C 传输层 + 其上的 header-only C++ 服务端 | [`compat.usockets`](../../pkgs/c/compat.usockets.lua) · [`compat.uwebsockets`](../../pkgs/c/compat.uwebsockets.lua)(uSockets 三平台统一选 libuv 一个事件循环(经 `compat.libuv`),因为按平台各选后端只会让 `us_loop_t` 每个平台一个形状而毫无收益;SSL 与 QUIC 不收,于是基础包的唯一依赖就是那个循环。这一对真正的教训是 `LIBUS_USE_LIBUV` / `LIBUS_NO_SSL` / `UWS_NO_ZLIB` 是**接口级**事实:`libusockets.h` 会因前者改变 `us_loop_t` 的布局、因后者门控 SSL 声明,而 uWS 是 header-only —— 它的模板是在**消费者**的 TU 里实例化的。描述符的 `cflags` 只作用于包自身的 TU,所以每个消费者都必须自己声明这三个;不一致不会构建失败,而是内存损坏。usockets 的测试因此从定时器回调里写loop 附属的扩展内存再读回来 —— 布局一旦不一致,正是这条断言会断) | diff --git a/mcpp.toml b/mcpp.toml index 0f4aa13..655c23e 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -134,6 +134,8 @@ members = [ "tests/examples/plf-hive", "tests/examples/spirv-reflect", "tests/examples/vulkan-memory-allocator", + "tests/examples/wamr", + "tests/examples/wamr-features", ] # ── The index redirect, hoisted to the workspace root ─────────────────── diff --git a/pkgs/c/compat.wamr.lua b/pkgs/c/compat.wamr.lua new file mode 100644 index 0000000..0912493 --- /dev/null +++ b/pkgs/c/compat.wamr.lua @@ -0,0 +1,264 @@ +-- Form B inline descriptor for WAMR (WebAssembly Micro Runtime) — Bytecode +-- Alliance's small-footprint WebAssembly runtime. Pure-C source build, same +-- shape as compat.mbedtls: compile the interpreter runtime into one lib and +-- expose `wasm_export.h` / `wasm_c_api.h`. +-- +-- All `mcpp` paths are GLOBS relative to the verdir; the leading `*/` absorbs +-- the GitHub tarball's `wasm-micro-runtime-WAMR-2.4.5/` wrap layer. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHY A GENERATED CONFIG HEADER AND A GENERATED .S +-- +-- WAMR does not select its target from the compiler's own predefined macros. +-- `wasm_runtime_common.c` wraps the whole invoke-native section in +-- `#if defined(BUILD_TARGET_X86_64) || defined(BUILD_TARGET_AARCH64) || …`, +-- so with none of them defined the runtime compiles with no way to call a +-- native function at all — the build succeeds and the link fails. Upstream's +-- CMake sets the right one from `WAMR_BUILD_TARGET`. A descriptor cannot: the +-- schema varies `sources`/`cflags` per OS, not per architecture (`archs` is +-- package-level metadata, not a selector). +-- +-- Both halves of that problem are solved by letting the preprocessor read the +-- compiler's architecture macros instead: +-- +-- * `mcpp_wamr_config.h` maps `__x86_64__`/`__aarch64__` to the matching +-- `BUILD_TARGET_*`, and arrives on every TU through `-include`. Same +-- mechanism compat.zlib uses for `Z_HAVE_UNISTD_H`. +-- * `mcpp_wamr_invoke_native.S` picks the matching assembly implementation. +-- Upstream's files are `arch/invokeNative_em64.s` and +-- `arch/invokeNative_aarch64.s` — lowercase `.s`, which clang assembles +-- WITHOUT running the preprocessor, so they cannot guard themselves the +-- way libffi's `.S` files do. Naming our own dispatcher `.S` gets it +-- preprocessed, `#include` pulls the chosen file in as text, and the +-- `#ifndef BH_PLATFORM_DARWIN` guard already inside those files then +-- works as upstream intended. +-- +-- Verified: both branches assemble and define `invokeNative` +-- (`clang -c` and `clang --target=aarch64-unknown-linux-gnu -c`). +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHY -std=gnu11 AND NOT c_standard = "c11" +-- +-- `core/shared/platform/linux/platform_internal.h` writes the GS base with a +-- bare `asm volatile`. `-std=c11` defines __STRICT_ANSI__, under which `asm` +-- is not a keyword (only `__asm__` is), and `wasm_memory.c` plus +-- `wasm_interp_fast.c` fail to compile. Upstream builds as a GNU dialect. +-- The alternative — `-DWASM_DISABLE_WRITE_GS_BASE=1`, which is a real upstream +-- knob — also compiles, but turns off a fast-path memory access on x86_64 to +-- work around a language-mode choice, so the dialect flag is the honest fix. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- LINUX ONLY, DELIBERATELY +-- +-- WAMR itself is portable (there are `platform/darwin` and `platform/windows` +-- trees, and the assembly files carry Darwin guards), but neither was built or +-- run while writing this descriptor, and Windows additionally has to settle +-- whether it takes upstream's MASM `.asm` or the MinGW `.s` variant. Rather +-- than declare platforms on the strength of them looking symmetric, `xpm` +-- carries `linux` only and consumers gate with `[target.'cfg(linux)'…]` — +-- the same shape compat.libaio uses. macOS and Windows sections can be added +-- by someone able to verify them. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHAT THE BASE CONTAINS +-- +-- Interpreter runtime only: classic + fast interpreter, bulk memory and +-- reference types (both on in upstream's own default build), no AOT, no JIT, +-- and no libc for the guest. That is the configuration an embedder wants when +-- wasm modules are plugins reached only through host-provided imports. +-- Guest-facing libc comes in through the two features. +-- +-- * `libc-builtin` — upstream's built-in libc wrappers (printf, memcpy, +-- malloc … exported to the guest under `env`). +-- * `libc-wasi` — WASI preview1, which also needs the three POSIX +-- platform files upstream excludes when WASI is off +-- (`posix_file.c`, `posix_clock.c`, `posix_socket.c`) plus `libc-util`. +-- +-- Both are safe as features: `core/iwasm/include/wasm_export.h` never branches +-- on `WASM_ENABLE_LIBC_BUILTIN` or `WASM_ENABLE_LIBC_WASI` (both appear zero +-- times outside comments), so a consumer compiled without the define still +-- sees the same types as the library — the ABI hazard that keeps +-- compat.recastnavigation's DT_POLYREF64 out of the feature table does not +-- apply here. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- CN MIRROR +-- +-- Not created: no `mcpp-res` write access. `url` is the plain-string upstream +-- form, which lint accepts and which makes CN users fall back to GitHub. A +-- maintainer can promote it to the `{ GLOBAL=…, CN=… }` table later. +package = { + spec = "1", + namespace = "compat", + name = "wamr", + description = "WebAssembly Micro Runtime — small-footprint WebAssembly interpreter from the Bytecode Alliance", + licenses = {"Apache-2.0 WITH LLVM-exception"}, + repo = "https://github.com/bytecodealliance/wasm-micro-runtime", + type = "package", + + xpm = { + linux = { + ["2.4.5"] = { + url = "https://github.com/bytecodealliance/wasm-micro-runtime/archive/refs/tags/WAMR-2.4.5.tar.gz", + sha256 = "1ab09d51099f276ca4a1d6629f6b589aab2bd0caa01445e05031a4bed22c199b", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", + + include_dirs = { + -- public: wasm_export.h / wasm_c_api.h, and the platform types + -- wasm_export.h refers to + "*/core/iwasm/include", + "*/core/shared/platform/include", + -- internal: WAMR's TUs include each other by bare filename + "*/core", + "*/core/iwasm/common", + "*/core/iwasm/interpreter", + "*/core/shared/include", + "*/core/shared/platform/linux", + "*/core/shared/mem-alloc", + "*/core/shared/utils", + -- reached by the generated dispatcher's #include + "*/core/iwasm/common/arch", + -- The two features' header roots. They live here rather than in + -- the feature entries because a feature can only carry sources, + -- defines, deps, implies and requires -- there is no include_dirs + -- in a feature. And they are needed by BASE translation units, not + -- just the feature's own: with WASM_ENABLE_LIBC_WASI=1, + -- common/wasm_runtime_common.h itself opens `#include "posix.h"`, + -- so wasm_loader.c and wasm_runtime.c stop compiling without the + -- sandboxed-system-primitives roots. Listing them unconditionally + -- costs nothing when the features are off: they are -I paths into + -- directories no base source includes from. + "*/core/iwasm/libraries/libc-builtin", + "*/core/iwasm/libraries/libc-wasi", + "*/core/iwasm/libraries/libc-wasi/sandboxed-system-primitives/include", + "*/core/iwasm/libraries/libc-wasi/sandboxed-system-primitives/src", + "*/core/shared/platform/common/libc-util", + "mcpp_generated/include", + }, + + linux = { + cflags = { + -- see "WHY -std=gnu11" above; appended after c_standard so it wins + "-std=gnu11", + "-D_GNU_SOURCE", + "-DBH_PLATFORM_LINUX", + -- upstream's iwasm_common.cmake sets both unconditionally + "-DBH_MALLOC=wasm_runtime_malloc", + "-DBH_FREE=wasm_runtime_free", + "-DWASM_ENABLE_INTERP=1", + "-DWASM_ENABLE_FAST_INTERP=1", + "-DWASM_ENABLE_BULK_MEMORY=1", + "-DWASM_ENABLE_REF_TYPES=1", + -- glibc/musl both have mremap; upstream probes for it with + -- check_symbol_exists and falls back to its own allocator when + -- absent. Linux always has it. + "-DWASM_HAVE_MREMAP=1", + "-include", "mcpp_wamr_config.h", + }, + ldflags = { "-lpthread", "-lm" }, + }, + + generated_files = { + ["mcpp_generated/include/mcpp_wamr_config.h"] = +[==[ +#ifndef MCPP_WAMR_CONFIG_H +#define MCPP_WAMR_CONFIG_H +/* WAMR takes its target from BUILD_TARGET_*, which upstream's CMake sets from + WAMR_BUILD_TARGET. A descriptor has no per-architecture hook, so derive it + from the compiler's own macros instead. Without one of these defined, the + invoke-native section of wasm_runtime_common.c compiles to nothing and the + link fails on `invokeNative`. */ +#if defined(__x86_64__) || defined(__amd64__) || defined(_M_X64) +#define BUILD_TARGET_X86_64 +#elif defined(__aarch64__) || defined(_M_ARM64) +#define BUILD_TARGET_AARCH64 +#else +#error "compat.wamr: no BUILD_TARGET_* for this architecture" +#endif +#endif /* MCPP_WAMR_CONFIG_H */ +]==], + ["mcpp_generated/mcpp_wamr_invoke_native.S"] = +[==[ +/* Architecture dispatch for WAMR's invokeNative. + + Upstream ships one hand-written assembly implementation per architecture as + `arch/invokeNative_.s`. Lowercase `.s` is assembled without the C + preprocessor, so those files cannot select themselves and a descriptor + cannot select between them either. This file is `.S`, so it IS + preprocessed: the #include below pastes the chosen implementation in, and + the `#ifndef BH_PLATFORM_DARWIN` guard already inside it is honoured. */ +#if defined(__x86_64__) || defined(__amd64__) +#include "invokeNative_em64.s" +#elif defined(__aarch64__) +#include "invokeNative_aarch64.s" +#else +#error "compat.wamr: no invokeNative implementation for this architecture" +#endif +]==], + }, + + sources = { + -- runtime core + "*/core/iwasm/common/*.c", + "mcpp_generated/mcpp_wamr_invoke_native.S", + -- interpreter: loader + runtime + the fast interpreter. + -- wasm_mini_loader.c and wasm_interp_classic.c are upstream's + -- alternatives to these two, not additions — including them would + -- define the same symbols twice. + "*/core/iwasm/interpreter/wasm_loader.c", + "*/core/iwasm/interpreter/wasm_runtime.c", + "*/core/iwasm/interpreter/wasm_interp_fast.c", + -- platform layer + "*/core/shared/platform/linux/*.c", + -- posix_file.c, posix_clock.c and posix_socket.c are in here. + -- Upstream's platform_api_posix.cmake drops those three (and + -- libc-util below) unless LIBC_WASI or the debug interpreter is on, + -- and the natural translation would be a `!` exclusion in the base + -- with the libc-wasi feature adding them back. That does not work: + -- an exclusion glob is global, so the feature's own entry for the + -- same file is still excluded and the WASI build fails to link + -- (os_file_get_access_mode, os_closedir, os_is_dir_stream_valid). + -- They compile cleanly with WASI off — verified file by file — so + -- the base simply always carries them. The cost is a few KB of + -- unreferenced objects the linker drops. + "*/core/shared/platform/common/posix/*.c", + "*/core/shared/platform/common/libc-util/*.c", + -- allocator and utils + "*/core/shared/mem-alloc/mem_alloc.c", + "*/core/shared/mem-alloc/ems/*.c", + "*/core/shared/utils/*.c", + }, + + targets = { ["wamr"] = { kind = "lib" } }, + + features = { + -- Built-in libc wrappers exported to the guest under `env`. + ["libc-builtin"] = { + sources = { "*/core/iwasm/libraries/libc-builtin/*.c" }, + defines = { "WASM_ENABLE_LIBC_BUILTIN=1" }, + }, + -- WASI preview1. The POSIX support files this needs are already + -- unconditionally in the base sources; see the note there. + ["libc-wasi"] = { + sources = { "*/core/iwasm/libraries/libc-wasi/**/*.c" }, + -- runtime_lib.cmake:97-99 turns MODULE_INST_CONTEXT on + -- together with LIBC_WASI; without it wasm_native.c calls + -- wasm_native_{get,set}_context and the context-key helpers + -- that wasm_native.h only declares under that macro. + defines = { + "WASM_ENABLE_LIBC_WASI=1", + "WASM_ENABLE_MODULE_INST_CONTEXT=1", + }, + }, + }, + + deps = { }, + }, +} diff --git a/tests/examples/wamr-features/mcpp.toml b/tests/examples/wamr-features/mcpp.toml new file mode 100644 index 0000000..5be10dd --- /dev/null +++ b/tests/examples/wamr-features/mcpp.toml @@ -0,0 +1,11 @@ +# WAMR feature test project: both optional guest-libc layers turned on. +# +# Split from tests/examples/wamr because the base member asserts the opposite — +# that `env.putchar` does NOT resolve without the feature. The two members +# together are the feature gate's positive and negative evidence. +[package] +name = "wamr-features-tests" +version = "0.1.0" + +[target.'cfg(linux)'.dependencies.compat] +wamr = { version = "2.4.5", features = ["libc-builtin", "libc-wasi"] } diff --git a/tests/examples/wamr-features/tests/libc_features.cpp b/tests/examples/wamr-features/tests/libc_features.cpp new file mode 100644 index 0000000..2ae4bdc --- /dev/null +++ b/tests/examples/wamr-features/tests/libc_features.cpp @@ -0,0 +1,166 @@ +// Feature test for compat.wamr: assert that the two optional guest-libc layers +// are actually linked in when requested. +// +// The discriminator is a CALL, not instantiation. WAMR does not reject a +// module whose import is unresolved — the loader logs "failed to link import +// function" and carries on, so instantiation succeeds either way. The refusal +// only appears when the guest calls the import, as an exception naming it +// "unlinked". tests/examples/wamr asserts exactly that for env.putchar against +// the base package; here the same call must go through. +#ifdef __linux__ + +#include + +#include +#include +#include + +namespace { + +// (module (import "env" "putchar" (func (param i32) (result i32))) +// (func (export "emit") (param i32) (result i32) local.get 0 call 0)) +const unsigned char kNeedsLibcBuiltin[] = { + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x06, 0x01, 0x60, 0x01, 0x7f, 0x01, 0x7f, + 0x02, 0x0f, 0x01, 0x03, 'e', 'n', 'v', + 0x07, 'p', 'u', 't', 'c', 'h', 'a', 'r', 0x00, 0x00, + 0x03, 0x02, 0x01, 0x00, + 0x07, 0x08, 0x01, 0x04, 'e', 'm', 'i', 't', 0x00, 0x01, + 0x0a, 0x08, 0x01, 0x06, 0x00, 0x20, 0x00, 0x10, 0x00, 0x0b, +}; + +// (module (import "wasi_snapshot_preview1" "proc_exit" (func (param i32))) +// (memory (export "memory") 1) +// (func (export "exit0") (param i32) local.get 0 call 0)) +// +// The memory is not optional decoration: WAMR refuses to load a module that +// imports WASI apis without an exported memory ("a module with WASI apis must +// export memory by default"), because the WASI wrappers address the guest's +// linear memory to return their results. +const unsigned char kNeedsLibcWasi[] = { + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x05, 0x01, 0x60, 0x01, 0x7f, 0x00, + 0x02, 0x24, 0x01, + 0x16, 'w', 'a', 's', 'i', '_', 's', 'n', 'a', 'p', 's', 'h', 'o', 't', + '_', 'p', 'r', 'e', 'v', 'i', 'e', 'w', '1', + 0x09, 'p', 'r', 'o', 'c', '_', 'e', 'x', 'i', 't', 0x00, 0x00, + 0x03, 0x02, 0x01, 0x00, + 0x05, 0x03, 0x01, 0x00, 0x01, + 0x07, 0x12, 0x02, 0x05, 'e', 'x', 'i', 't', '0', 0x00, 0x01, + 0x06, 'm', 'e', 'm', 'o', 'r', 'y', 0x02, 0x00, + 0x0a, 0x08, 0x01, 0x06, 0x00, 0x20, 0x00, 0x10, 0x00, 0x0b, +}; + +bool g_ok = true; + +void check(const char *what, bool cond) +{ + std::printf("%-28s %s\n", what, cond ? "ok" : "FAILED"); + g_ok = g_ok && cond; +} + +} // namespace + +int main() +{ + RuntimeInitArgs init; + std::memset(&init, 0, sizeof init); + init.mem_alloc_type = Alloc_With_Allocator; + init.mem_alloc_option.allocator.malloc_func = reinterpret_cast(std::malloc); + init.mem_alloc_option.allocator.realloc_func = reinterpret_cast(std::realloc); + init.mem_alloc_option.allocator.free_func = reinterpret_cast(std::free); + + if (!wasm_runtime_full_init(&init)) { + std::puts("wasm_runtime_full_init failed"); + return 1; + } + + char err[192] = { 0 }; + + // ── libc-builtin: env.putchar must link and run ──────────────────────── + { + unsigned char image[sizeof kNeedsLibcBuiltin]; + std::memcpy(image, kNeedsLibcBuiltin, sizeof image); + wasm_module_t mod = wasm_runtime_load(image, sizeof image, err, sizeof err); + check("libc-builtin load", mod != nullptr); + if (mod) { + wasm_module_inst_t inst = + wasm_runtime_instantiate(mod, 8192, 8192, err, sizeof err); + check("libc-builtin instantiate", inst != nullptr); + if (inst) { + wasm_function_inst_t fn = wasm_runtime_lookup_function(inst, "emit"); + wasm_exec_env_t env = wasm_runtime_create_exec_env(inst, 8192); + if (fn && env) { + // putchar writes the byte and returns 1 (see + // libc_builtin_wrapper.c putchar_wrapper) -- it is not the + // C library's "returns the character written". + uint32_t argv[1] = { 0x0a }; + bool called = wasm_runtime_call_wasm(env, fn, 1, argv); + if (!called) + std::printf(" exception: %s\n", + wasm_runtime_get_exception(inst)); + check("libc-builtin putchar call", called && argv[0] == 1); + } + else { + check("libc-builtin lookup/env", false); + } + if (env) + wasm_runtime_destroy_exec_env(env); + wasm_runtime_deinstantiate(inst); + } + wasm_runtime_unload(mod); + } + } + + // ── libc-wasi: wasi_snapshot_preview1.proc_exit must link and run ────── + // proc_exit does not end the process; the wrapper raises "wasi proc exit" + // and records the code, which the embedder then reads. That round trip is + // only possible with the feature's sources compiled in. + { + unsigned char image[sizeof kNeedsLibcWasi]; + std::memcpy(image, kNeedsLibcWasi, sizeof image); + wasm_module_t mod = wasm_runtime_load(image, sizeof image, err, sizeof err); + if (!mod) + std::printf(" load error: %s\n", err); + check("libc-wasi load", mod != nullptr); + if (mod) { + // Gives the module a wasi context; the wrapper dereferences it. + wasm_runtime_set_wasi_args(mod, nullptr, 0, nullptr, 0, nullptr, 0, + nullptr, 0); + wasm_module_inst_t inst = + wasm_runtime_instantiate(mod, 8192, 8192, err, sizeof err); + check("libc-wasi instantiate", inst != nullptr); + if (inst) { + wasm_function_inst_t fn = wasm_runtime_lookup_function(inst, "exit0"); + wasm_exec_env_t env = wasm_runtime_create_exec_env(inst, 8192); + if (fn && env) { + uint32_t argv[1] = { 3 }; + bool called = wasm_runtime_call_wasm(env, fn, 1, argv); + const char *ex = wasm_runtime_get_exception(inst); + std::printf(" call=%d exception=%s exit_code=%u\n", called ? 1 : 0, + ex ? ex : "(none)", + wasm_runtime_get_wasi_exit_code(inst)); + // The one thing that must NOT happen is the unlinked-import + // refusal; that is the signal the feature was not compiled in. + bool unlinked = ex && std::strstr(ex, "unlinked") != nullptr; + check("libc-wasi proc_exit linked", !unlinked); + check("libc-wasi exit code", wasm_runtime_get_wasi_exit_code(inst) == 3); + } + else { + check("libc-wasi lookup/env", false); + } + if (env) + wasm_runtime_destroy_exec_env(env); + wasm_runtime_deinstantiate(inst); + } + wasm_runtime_unload(mod); + } + } + + wasm_runtime_destroy(); + return g_ok ? 0 : 1; +} + +#else +int main() { return 0; } +#endif diff --git a/tests/examples/wamr/mcpp.toml b/tests/examples/wamr/mcpp.toml new file mode 100644 index 0000000..5d32b55 --- /dev/null +++ b/tests/examples/wamr/mcpp.toml @@ -0,0 +1,15 @@ +# WAMR test project: load a hand-assembled wasm module, instantiate it, call an +# export, and have the guest call back into a host native function. +# +# The module bytes are written out by hand rather than produced by a wasm +# toolchain, so the test has no build-time dependency beyond the package itself. +# +# compat.wamr 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 tests/examples/libaio uses. +[package] +name = "wamr-tests" +version = "0.1.0" + +[target.'cfg(linux)'.dependencies.compat] +wamr = "2.4.5" diff --git a/tests/examples/wamr/tests/run_module.cpp b/tests/examples/wamr/tests/run_module.cpp new file mode 100644 index 0000000..043cf68 --- /dev/null +++ b/tests/examples/wamr/tests/run_module.cpp @@ -0,0 +1,176 @@ +// Behavioral test for compat.wamr: run a real wasm module through the +// interpreter and let it call back into the host. +// +// Nothing here is mocked. A package that links but mis-configures the runtime +// would pass a "does it link" test: without BUILD_TARGET_* the invoke-native +// section compiles to nothing, and without the generated dispatcher there is +// no invokeNative at all — either way the guest→host call below is what +// notices. +#ifdef __linux__ + +#include + +#include +#include +#include + +namespace { + +// (module +// (import "env" "host_mul" (func $host_mul (param i32 i32) (result i32))) +// (func (export "compute") (param i32 i32) (result i32) +// local.get 0 local.get 1 call $host_mul)) +const unsigned char kCallsHost[] = { + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f, + 0x02, 0x10, 0x01, 0x03, 'e', 'n', 'v', + 0x08, 'h', 'o', 's', 't', '_', 'm', 'u', 'l', 0x00, 0x00, + 0x03, 0x02, 0x01, 0x00, + 0x07, 0x0b, 0x01, 0x07, 'c', 'o', 'm', 'p', 'u', 't', 'e', 0x00, 0x01, + 0x0a, 0x0a, 0x01, 0x08, 0x00, 0x20, 0x00, 0x20, 0x01, 0x10, 0x00, 0x0b, +}; + +// Same shape, but importing `env.putchar` — one of the wrappers the +// `libc-builtin` feature adds. The base package must NOT resolve it; this is +// the negative half of the feature gate, with the positive half in +// tests/examples/wamr-features. +const unsigned char kNeedsLibcBuiltin[] = { + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x06, 0x01, 0x60, 0x01, 0x7f, 0x01, 0x7f, + 0x02, 0x0f, 0x01, 0x03, 'e', 'n', 'v', + 0x07, 'p', 'u', 't', 'c', 'h', 'a', 'r', 0x00, 0x00, + 0x03, 0x02, 0x01, 0x00, + 0x07, 0x08, 0x01, 0x04, 'e', 'm', 'i', 't', 0x00, 0x01, + 0x0a, 0x08, 0x01, 0x06, 0x00, 0x20, 0x00, 0x10, 0x00, 0x0b, +}; + +int host_mul(wasm_exec_env_t, int a, int b) { return a * b; } + +NativeSymbol g_natives[] = { + { "host_mul", reinterpret_cast(host_mul), "(ii)i", nullptr }, +}; + +// Calls `emit` in the given module and reports whether WAMR refused because +// the import was never linked. +// +// An unresolved import is NOT an instantiation error in WAMR — the loader only +// logs "failed to link import function" and defers, so asking whether the +// module instantiates says nothing about the feature gate. The refusal only +// materialises on the call, as an exception naming the unlinked function. +bool refused_as_unlinked(const unsigned char *bytes, unsigned size) +{ + char err[192] = { 0 }; + // wasm_runtime_load takes a mutable buffer: it may patch the module image + // in place, so hand it a copy rather than the const literal. + unsigned char *buf = static_cast(std::malloc(size)); + if (!buf) + return false; + std::memcpy(buf, bytes, size); + + bool unlinked = false; + wasm_module_t mod = wasm_runtime_load(buf, size, err, sizeof err); + if (!mod) { + std::printf(" load rejected: %s\n", err); + } + else { + wasm_module_inst_t inst = + wasm_runtime_instantiate(mod, 8192, 8192, err, sizeof err); + if (!inst) { + std::printf(" instantiate rejected: %s\n", err); + } + else { + wasm_function_inst_t fn = wasm_runtime_lookup_function(inst, "emit"); + wasm_exec_env_t env = wasm_runtime_create_exec_env(inst, 8192); + if (fn && env) { + uint32_t argv[1] = { 65 }; + if (!wasm_runtime_call_wasm(env, fn, 1, argv)) { + const char *ex = wasm_runtime_get_exception(inst); + std::printf(" exception: %s\n", ex ? ex : "(none)"); + unlinked = ex && std::strstr(ex, "unlinked") != nullptr; + } + } + if (env) + wasm_runtime_destroy_exec_env(env); + wasm_runtime_deinstantiate(inst); + } + wasm_runtime_unload(mod); + } + std::free(buf); + return unlinked; +} + +} // namespace + +int main() +{ + RuntimeInitArgs init; + std::memset(&init, 0, sizeof init); + init.mem_alloc_type = Alloc_With_Allocator; + init.mem_alloc_option.allocator.malloc_func = reinterpret_cast(std::malloc); + init.mem_alloc_option.allocator.realloc_func = reinterpret_cast(std::realloc); + init.mem_alloc_option.allocator.free_func = reinterpret_cast(std::free); + init.native_module_name = "env"; + init.native_symbols = g_natives; + init.n_native_symbols = 1; + + bool ok = wasm_runtime_full_init(&init); + if (!ok) { + std::puts("wasm_runtime_full_init failed"); + return 1; + } + + char err[192] = { 0 }; + unsigned char image[sizeof kCallsHost]; + std::memcpy(image, kCallsHost, sizeof image); + + wasm_module_t mod = wasm_runtime_load(image, sizeof image, err, sizeof err); + ok = ok && mod != nullptr; + if (!mod) + std::printf("load failed: %s\n", err); + + wasm_module_inst_t inst = nullptr; + if (mod) { + inst = wasm_runtime_instantiate(mod, 8192, 8192, err, sizeof err); + ok = ok && inst != nullptr; + if (!inst) + std::printf("instantiate failed: %s\n", err); + } + + if (inst) { + wasm_function_inst_t fn = wasm_runtime_lookup_function(inst, "compute"); + ok = ok && fn != nullptr; + if (fn) { + wasm_exec_env_t env = wasm_runtime_create_exec_env(inst, 8192); + ok = ok && env != nullptr; + if (env) { + uint32_t argv[2] = { 6, 7 }; + bool called = wasm_runtime_call_wasm(env, fn, 2, argv); + if (!called) + std::printf("call failed: %s\n", + wasm_runtime_get_exception(inst)); + // 6 * 7 computed by the HOST and returned through the guest. + ok = ok && called && argv[0] == 42u; + std::printf("compute(6,7) = %u (expected 42)\n", argv[0]); + wasm_runtime_destroy_exec_env(env); + } + } + wasm_runtime_deinstantiate(inst); + } + if (mod) + wasm_runtime_unload(mod); + + // Feature gate, negative direction: libc-builtin is not in the base build, + // so a module importing env.putchar must be refused. + std::puts("expecting env.putchar to be unlinked (libc-builtin is off):"); + bool libc_absent = + refused_as_unlinked(kNeedsLibcBuiltin, sizeof kNeedsLibcBuiltin); + ok = ok && libc_absent; + std::printf("libc-builtin gated out: %s\n", libc_absent ? "yes" : "NO"); + + wasm_runtime_destroy(); + return ok ? 0 : 1; +} + +#else +int main() { return 0; } +#endif