Skip to content

feat(pkg): compat.wamr 2.4.5 (WebAssembly Micro Runtime) - #299

Merged
Sunrisepeak merged 1 commit into
mcpplibs:mainfrom
cloud-teahouse:feat/compat-wamr
Aug 30, 2026
Merged

feat(pkg): compat.wamr 2.4.5 (WebAssembly Micro Runtime)#299
Sunrisepeak merged 1 commit into
mcpplibs:mainfrom
cloud-teahouse:feat/compat-wamr

Conversation

@yspbwx2010

@yspbwx2010 yspbwx2010 commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Adds compat.wamr 2.4.5 — the Bytecode Alliance's WebAssembly Micro Runtime, a small-footprint wasm interpreter. Pure C, Apache-2.0 WITH LLVM-exception.

Shape

C-source compat, the compat.mbedtls template. The base is the 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 keeps AOT's LLVM dependency out of the index and is the configuration an embedder wants when wasm modules are plugins reached only through host-provided imports. Guest libc arrives through two features, libc-builtin and libc-wasi.

The interesting part: per-architecture selection

WAMR needs architecture-specific input in two places, and the descriptor schema has a per-OS hook but not a per-architecture one (archs declares support rather than selecting).

The first is 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_AARCH64), so with none defined the file compiles quietly with no way to call a native function and the failure only shows up at link time on invokeNative. The second is the implementation of that symbol, which is hand-written assembly, one file per architecture.

Both decisions are moved into the preprocessor, which does know the target:

  • generated_files emits a config header mapping __x86_64__/__aarch64__ to the matching BUILD_TARGET_*, delivered to every TU with -include — the mechanism compat.zlib already uses for Z_HAVE_UNISTD_H.
  • generated_files also emits a .S that #includes the chosen arch/invokeNative_*.s as text.

The dispatcher has to be .S. compat.libffi can list upstream's unix64.S and sysv.S directly and let each guard itself, but WAMR's equivalents are lowercase .s, which clang assembles without running the preprocessor, so an #ifdef inside them is never evaluated. Listing all of them is not an option either — each defines invokeNative, and the aarch64 file does not assemble on x86_64. Both branches were checked directly before any of this reached a descriptor:

$ 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 … -o a64.o && nm a64.o | grep -i invokenative
0000000000000000 T invokeNative

Two smaller decisions worth flagging for review

-std=gnu11 through cflags rather than c_standard. platform_internal.h writes the GS base with a bare asm volatile. -std=c11 defines __STRICT_ANSI__, under which asm is not a keyword, and wasm_memory.c and wasm_interp_fast.c both fail with use of undeclared identifier 'asm'. The alternative is -DWASM_DISABLE_WRITE_GS_BASE=1, a real upstream knob that also compiles — I did not take it because it turns off an x86_64 fast path to work around a language-mode choice. Happy to switch if you would rather not have a -std in cflags.

The POSIX platform files are carried unconditionally. Upstream's platform_api_posix.cmake drops posix_file.c, posix_clock.c and posix_socket.c unless WASI is on, and the natural translation is a ! exclusion in the base with the libc-wasi feature adding them back. That does not work — the exclusion is global and beats the feature's own entry for the same file, producing a build that compiles and then fails 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

all referenced from the feature's own sandboxed-system-primitives/src/posix.c. All four files compile cleanly with WASI off, verified individually, so the base just always carries them.

Relatedly, a feature cannot carry include_dirs, and with WASM_ENABLE_LIBC_WASI=1 it is not only the feature's sources that need the WASI header roots — common/wasm_runtime_common.h itself opens #include "posix.h", so wasm_loader.c and wasm_runtime.c stop compiling without them. Those five roots therefore live in the base include_dirs, where they are inert when the features are off.

Features and the ABI question

Both features carry defines, so I checked them against the rule that keeps compat.recastnavigation's DT_POLYREF64 out of the feature table. Neither WASM_ENABLE_LIBC_BUILTIN nor WASM_ENABLE_LIBC_WASI appears in core/iwasm/include/ at all outside comments, so a consumer compiled without them still sees the same wasm_export.h the library was built against.

libc-wasi also needs WASM_ENABLE_MODULE_INST_CONTEXT=1, which is not obvious: build-scripts/runtime_lib.cmake:97-99 turns it on together with WAMR_BUILD_LIBC_WASI, and without it wasm_native.c calls context helpers that wasm_native.h only declares under that macro.

Linux only, on purpose

xpm carries linux and nothing else, and consumers gate with [target.'cfg(linux)'.dependencies] — the compat.libaio shape. WAMR itself is portable, there are platform/darwin and platform/windows trees, and the assembly files carry Darwin guards that this .S dispatcher would honour, so macOS is probably a small delta. But I could not build or run either, and Windows additionally has an unresolved choice between upstream's MASM .asm and the MinGW .s variant. I would rather leave those sections to someone who can verify them than declare platforms because they look symmetric.

One scope limit inside linux, for the same reason: everything built and run here was x86_64. On aarch64 I verified only that the generated dispatcher assembles and defines invokeNative under --target=aarch64-unknown-linux-gnu, and that the config header selects BUILD_TARGET_AARCH64 there. The C half of an aarch64 build is unverified — I have no aarch64 sysroot on this machine, and cross-compiling against the host's headers fails in glibc, not in WAMR. Nothing suggests it is a problem, but I would rather say so than let the linux section imply more testing than happened.

Verification

Everything below used the pinned MCPP_VERSION 2026.8.27.2 with MCPP_INDEX_MIRROR=GLOBAL.

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. mcpp xpkg parse reports parse OK (11 sources, 16 includes, 2 generated files, 2 features).

Two members, run from a cleaned target/, .mcpp/ and a cleared global build cache, so both 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

Both test modules are hand-assembled wasm bytes in the test source, so neither member needs a wasm toolchain. tests/examples/wamr is not a link check: compute(6,7) returns 42 only because the guest called back into a native host_mul through invokeNative, which is exactly what a missing BUILD_TARGET_* or a mis-dispatched assembly file would break.

The feature gate is asserted in both directions — positively in wamr-features, negatively in wamr, where the same env.putchar module must be refused. Worth recording: the negative check has to call, not instantiate. WAMR does not reject an unresolved import at load or instantiation, it logs warning: failed to link import function and defers; the refusal only appears on the call. The first version of that test asserted on instantiation and was green for the wrong reason.

No CN mirror

I do not have mcpp-res write access, so url is the plain-string upstream form and CN users fall back to GitHub. Happy for a maintainer to promote it to the { GLOBAL=…, CN=… } table after mirroring the identical tarball.

One note on the skill

.agents/skills/add-mcpp-index-package step 8 says to add a row to the category table in README.md and README.zh-CN.md. Those tables are gone — the README points at the online index site now and keeps only the short "Reference examples" list. I put the record in docs/descriptor-examples.md and docs/zh/descriptor-examples.md instead, as a new shape row. Say the word if you would rather it went somewhere else, or if the skill should be updated.

Design notes, including the dead ends, are in .agents/docs/2026-08-30-add-wamr-plan.md.

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) <noreply@anthropic.com>
@Sunrisepeak
Sunrisepeak merged commit 7efcc8d into mcpplibs:main Aug 30, 2026
11 checks passed
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