Skip to content

Add wasm32-wasip1 target support for ruby-rbs-sys (cross-platform bindgen issues) #2943

@rubys

Description

@rubys

Summary

ruby-rbs-sys 0.3.0's build.rs has no wasm32-wasi* target awareness — there's no way to point cc at a wasi-targeting compiler or propagate the same flags to bindgen. I drafted a build refactor mirroring ruby-prism-sys's pattern (split build.rs into build/main.rs + build/vendored.rs, detect the wasm target, route everything through wasi-sdk). The C compile of vendored librbs to wasm32-wasip1 then succeeds cleanly. However, the bindgen step fails differently across three different libclang implementations, and the failure modes suggest a deeper bindgen-on-wasm issue that may also affect ruby-prism-sys — but isn't surfacing there because its wasm CI doesn't actually run.

This issue documents what I found in case maintainers (or anyone with deeper bindgen-on-wasm experience) can spot the underlying cause. I'm happy to send a PR for the build-refactor portion regardless — that part is mechanical and parallels prism cleanly. The bindgen question is what I'd like guidance on before proceeding.

Goal

Build ruby-rbs (and consumer crates) for wasm32-wasip1 so it can run in browser environments via wasi-libc shims. Used in a downstream project that ingests RBS signatures alongside Ruby source for a transpile-time type-resolution pipeline.

What works after the build refactor

  • Vendored librbs C compiles to wasm32-wasip1 cleanly with WASI_SDK_PATH set, using -D_WASI_EMULATED_MMAN=1 (defensive <sys/mman.h> includes in rbs_allocator.c; no actual mmap calls, so no link against libwasi-emulated-mman needed).
  • Native build path is unaffected; the wasm code is gated on target.contains("wasm32") && target.contains("wasi").
  • Bindgen is invoked with the same -I, -D, and --sysroot flags as cc via BINDGEN_EXTRA_CLANG_ARGS_<target> propagation — same convention ruby-prism-sys uses.

Three libclangs, three failure modes

Platform / libclang Failure
macOS, Apple CommandLineTools libclang (default) All 18 FFI function declarations dropped from generated bindings. bindings.rs contains struct/typedef/enum definitions but no extern "C" blocks for functions. Downstream ruby-rbs then fails with 90 "cannot find function" errors.
macOS, brew llvm 22 libclang (LIBCLANG_PATH=/opt/homebrew/opt/llvm/lib) Functions emit. But 5 typedef-after-forward-declared structs (rbs_ast_symbol, rbs_namespace, rbs_type_name, rbs_types_block, rbs_ast_declarations_class_super) emit as opaque (pub _address: u8) despite the full typedef struct foo { ... } foo_t; definition appearing later in vendor/rbs/include/rbs/ast.h. Bindgen's own layout-test assertions then fail at rustc time (size_of::<rbs_ast_symbol>() - 24usize overflows because actual size is 1, expected size is 24).
Ubuntu 24.04, distribution libclang (apt install clang libclang-dev) Different missing types — at minimum rbs_parser_t is absent, breaking ruby-rbs's codegen-from-config.yml output (pub struct AnnotationNode<'a> { parser: NonNull<rbs_parser_t>, ... }).

The pattern looks like bindgen handles forward-decl-then-typedef structs differently across libclang versions when running with --target=wasm32-wasip1 --sysroot=<wasi-sysroot>. The native build (no wasm target, no sysroot override) handles them correctly across all three libclangs.

Why prism's pattern alone may not be a sufficient precedent

I initially assumed ruby-prism-sys had wasm support figured out and copying its build pattern would suffice. The build-script pattern transferred cleanly, but the bindgen issues surfaced anyway. On checking: ruby-prism's wasm CI doesn't appear to actually run. The repo has rust/ruby-prism/examples/wasm/{main.rs,run.sh} and the rake cargo:examples task that invokes them, but I don't see a GHA workflow job that exercises the wasm path end-to-end — rust-bindings.yml adds targets: wasm32-wasip1 to the rustup-action but I couldn't find a job that actually runs cargo build --target=wasm32-wasip1 on prism. Maintainers can correct me if I'm reading this wrong.

If that's right, prism may share a similar issue; it just hasn't been forced to surface. If it's wrong, the prism-specific differences (e.g., PRISM_EXPORT_SYMBOLS macro, .size_t_is_usize(true), .layout_tests(true) configuration) are clues for what RBS may need.

What might help

I'm not sure what the right fix is. Plausible directions:

  1. Disable layout_tests for the affected struct names. Workaround for the brew-libclang failure mode; doesn't address the Apple/Ubuntu modes.
  2. Investigate the typedef-after-forward-decl handling — possibly an ordering or __attribute__((visibility)) interaction with wasm targets that bindgen's libclang sees differently across versions. May require a minimal repro filed against rust-bindgen.
  3. Generate bindings on the host platform, vendor them into the crate, skip bindgen entirely for wasm builds. Ugly but unblocks downstream.

If maintainers have a preferred direction, I'd be glad to send a PR. The build-script refactor (the wasm-detection block alone, mirroring prism) is independently useful and could land first.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions