Add emscripten-wasm32 support with rmw_zenoh_pico - #46
Open
Tobias-Fischer wants to merge 13 commits into
Open
Conversation
Ports the emscripten-wasm32 build target (proven on ros-humble via Tobias-Fischer/ros-humble, see RoboStack/vinca#154) to rolling, using rmw_zenoh_pico instead of a custom RMW, since zenoh-pico has real upstream WASM/pthreads support. Verified end-to-end: a wasm32 rclc talker running in a browser publishes std_msgs/String over a real WebSocket to a native zenohd router, received by an independent native subscriber. - vinca.yaml/robostack.yaml/patch/dependencies.yaml/pkg_additional_info.yaml: gate the emscripten-wasm32 platform throughout, same pattern as the humble port (if: wasm32 / not wasm32 selectors, per-platform robostack.yaml dicts). - conda_build_config.yaml/vinca_pinning.yaml: emscripten toolchain + pin overrides (compiler, python 3.13, graphviz/libffi pins that only exist on emscripten-forge). - pixi.toml: point vinca at the fork branch backing vinca#154 until merged. - patch/ros-rolling-*.patch: per-package emscripten build fixes (rclpy narrowing/-latomic, rcutils, rosidl-generator-py, spdlog/yaml-cpp vendor, osrf-testing-tools-cpp, rmw_test_fixture_implementation). - patch/ros-rolling-rmw-zenoh-pico.patch, patch/ros-rolling-rosidl-typesupport- microxrcedds-{c,cpp}.patch: build rmw_zenoh_pico and its typesupport backend for wasm32 (upstream PRs open, see below). - extra_recipes/zenoh-pico, extra_recipes/microcdr: new recipes for rmw_zenoh_pico's dependencies (not otherwise packaged for RoboStack). Upstream bug fixes have been submitted separately: - eclipse-zenoh/zenoh-pico#1314 - esol-community/rmw_zenoh_pico#7 - micro-ROS/rosidl_typesupport_microxrcedds#83 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This was referenced Sep 9, 2026
- Bump the vinca pin to 325d0af, which also sets CMAKE_SHARED_MODULE_CREATE_C/CXX_FLAGS (not just the SHARED-library variants) -- CMake's MODULE library type, used by pybind11_add_module() for every Python C extension (rclpy's own _rclpy_pybind11, and each message package's rosidl_generator_py typesupport accessor), was linking without USE_PTHREADS=1 even though its objects compiled with atomics fine, producing a load-time "mismatch in shared state of memory" next to the rest of a pthreads build. Invisible until something used pybind11_add_module() -- found getting a live rclpy demo running. - Add two Emscripten guards to patch/ros-rolling-rcutils.emscripten.patch (upstreamed as ros2/rcutils#591) so rcl_logging_implementation's dlopen-by-name backend selection works: rcutils_get_platform_library_name() had no emscripten case (always "failed to format library name"), and the post-dlopen path resolution took a dlinfo(RTLD_DI_LINKMAP) branch that Emscripten's JS-backed dlopen doesn't support, treating a successful dlopen() as a failure. - Document a known gap (see pixi.toml comment): rosidl_typesupport_ microxrcedds_cpp's codegen doesn't handle service "_Event" messages, so action_msgs/lifecycle_msgs/rcl_interfaces/rosgraph_msgs/statistics_msgs/ type_description_interfaces/micro_ros_msgs need a second, C-only rebuild pass rather than the global STATIC_ROSIDL_TYPESUPPORT_CPP override this task sets -- a real rclpy talker (not just rclc) verified end-to-end through this same rmw_zenoh_pico pipeline surfaced this. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…shot vinca's PR (RoboStack/vinca#154) had a merge conflict against current master -- vinca/main.py has since been split into several modules (configuration.py, pipeline.py, recipes.py, sources.py, etc.) by an unrelated upstream refactor. Merged and verified: none of this branch's actual changes touch main.py in a way the refactor didn't already independently resolve (the one main.py hunk this branch touched -- dropping an unsatisfiable build-time rosidl_default_generators requirement for emscripten cross builds -- turned out to already be gone from the refactored file), and `pixi run generate-recipes-emscripten` against the merged vinca commit still produces the same 229-recipe closure with all of this repo's template customizations (real pthreads, configurable RMW_IMPLEMENTATION/typesupport backend) intact. The refactored vinca requires a newer rosdistro_snapshot.yaml schema (a per-package `dependencies:` list) that the snapshot committed here predates -- regenerated via the existing `create_snapshot` task. As a side effect this also re-syncs every package's pinned tag to current rolling (the prior snapshot was from 2026-08-24); spot-checked several packages and the version bumps are real upstream rolling releases, not the migration itself introducing drift. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ature/emscripten-wasm32-zenoh-pico # Conflicts: # patch/dependencies.yaml # pkg_additional_info.yaml
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…river patch The previous commit's full vinca-snapshot regeneration (needed for the new vinca's dependencies: schema) also silently bumped every package's pinned git tag to whatever's currently on rolling. That's far more blast radius than intended and broke at least one unrelated, already-passing patch (ros-rolling-mujoco-3d-lidar.patch, whose context assumed the older pinned mjtnum.h-vs-mjtype.h header layout). Replaced it with a merge: keep every already-tracked package's original tag/version/url exactly as they were, and only backfill the new dependencies: field from a fresh vinca-snapshot run. Packages genuinely new to rosdistro since the original snapshot (no old entry to preserve) still get their current tag -- one of those, sbg_driver, needed its own patch regenerated against upstream's newer release, which turned out to have already picked up 3 of our patch's 4 hunks itself (ament_target_dependencies -> target_link_libraries modernization, usleep -> sleep_for); only the maybe_unused parameter fix still applied. `pixi run check-patches` (the full non-emscripten closure) now passes clean, and `pixi run generate-recipes-emscripten` still produces the same 229-recipe closure as before. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ature/emscripten-wasm32-zenoh-pico # Conflicts: # conda_build_config.yaml # vinca_pinning.yaml
get_used_typesupports.cmake finds candidate typesupports via a plain find_package(rosidl_typesupport_microxrcedds_c QUIET), not a real package.xml/recipe dependency -- same class of ordering issue already documented and fixed for rmw_implementation just above. With VINCA_EMSCRIPTEN_STATIC_TYPESUPPORT_C/_CPP naming that implementation, a genuinely from-scratch build of the full closure is order-dependent: rattler-build's own topological sort has no way to know rosidl_typesupport_microxrcedds_c/cpp need to exist before rosidl_typesupport_c/cpp without a declared dependency saying so. Caught by a real CI run doing an atomic from-scratch build of the whole closure for the first time (the local build this was developed against accreted incrementally over many separate runs, which happened to build these in a working order by chance). Verified with an isolated rebuild of just these recipes plus their now-declared dependents, starting from none of them existing: builds cleanly in one rattler-build pass, no retry/continue-on-failure needed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A blanket *.sh in .gitignore (meant for vinca-generated build scripts under recipes/, already separately ignored) also silently swallowed these two hand-written build.sh files, which are genuine recipe source, not generated output. Neither ever actually made it into a commit -- a real CI run doing a genuinely fresh checkout hit "build.sh: command not found" building microcdr, since generate-recipes-emscripten's `cp -r extra_recipes/. recipes/` only had recipe.yaml to copy. Add a negation (!extra_recipes/**/*.sh) rather than removing the blanket rule outright, since it's presumably still doing its job for whatever vinca-generated scripts prompted it in the first place. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…vent codegen gap globally Same class of fix as the previous commit's rosidl_typesupport_c/cpp ordering one, for two packages that were missed: test_msgs and example_interfaces use rosidl_generate_interfaces() like the other 11 core message packages, but weren't in patch/dependencies.yaml's add_host list for ros2-rosidl-typesupport-microxrcedds-c/-cpp -- their own build environment never had those typesupport backends installed, so STATIC_ROSIDL_TYPESUPPORT_C silently fell back to rosidl_typesupport_introspection_c and get_used_typesupports.cmake rejected it outright once something else (rcl, via a real build_depend/test_depend) needed them to actually exist. Also adds --continue-on-failure to build-emscripten itself, matching build_continue_on_failure's existing rationale for the native build: rosidl_typesupport_microxrcedds_cpp's codegen gap for auto-generated service "_Event" messages is a real, permanent upstream limitation for action_msgs/lifecycle_msgs/rosgraph_msgs/statistics_msgs/ micro_ros_msgs/test_msgs/example_interfaces (confirmed generating uncompilable C++), not something a dependency declaration can fix -- those packages need a second pass without the CPP override (see README's Known limitations), so the first pass has to tolerate their failure instead of aborting the whole ~230-package closure over seven packages with a known, accepted, understood gap. Verified against a genuinely from-scratch build (cleared local caches, rebuilt the closure with no pre-existing packages): all ~230 recipes build cleanly across a first pass (tolerating the seven known failures) plus the established second-pass fixups, and the resulting rclpy talker publishes real std_msgs/String messages through a native zenoh router. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Tobias-Fischer
added a commit
to Tobias-Fischer/vinca
that referenced
this pull request
Sep 10, 2026
A message package's *Config.cmake only re-exports find_dependency() calls for what its own package.xml/CMakeLists.txt declares. It has no idea VINCA_EMSCRIPTEN_STATIC_TYPESUPPORT_C/_CPP named an extra typesupport backend, so it never propagates *that* as an exported dependency to its own downstream consumers -- a package that find_package()s only one message package at a time never notices (it already found the backend itself while configuring its own rosidl_generate_interfaces() call), but one that find_package()s several message packages together hits "the target was not found ... A find_package call is missing for an IMPORTED target" the first time a downstream *Export.cmake references rosidl_typesupport_microxrcedds_c(pp)::rosidl_typesupport_microxrcedds_c(pp) without anyone upstream having found it first. Pre-finding it in the same CMAKE_PROJECT_INCLUDE file already used for the pthreads/shared-lib flags (included right after every project() call, so it's in every target's CMake namespace before that project's own find_package() calls run) covers every consumer uniformly instead of needing a patch per affected package. Found via RoboStack/ros-rolling#46's test_msgs, the first package in that closure to find_package() enough message packages together to expose the gap. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Tobias-Fischer/vinca@b1960a5 -- see that commit and the previous ros-rolling commit's message for the actual fix and why it was needed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pins to Tobias-Fischer/vinca@c974b23, which stops pre-finding VINCA_EMSCRIPTEN_STATIC_TYPESUPPORT_C/_CPP for packages that call rosidl_generate_interfaces() themselves. Re-verifying the pipeline from a clean rebuild surfaced a real bug: pre-finding the override backend for a message-generating package makes it "already a target" before that package's own typesupport discovery runs, which -- confirmed by inspecting the built package's own ament_cmake_export_targets-extras.cmake -- bumps its typesupport entry ahead of the generator target its own Export.cmake requires. Every downstream find_package(<that package>) then failed with "referenced, but are missing: <pkg>::<pkg>__rosidl_generator_c(pp)", surfaced here on builtin_interfaces (breaking service_msgs, which consumes it). Also adds a sync-native-bootstrap-mirror task (wired into build-emscripten's depends-on) instead of the same mirroring having to be done by hand each time -- see its own comment for why the mirror is needed at all (a build:-time dependency on a native rosidl_default_generators that no channel actually publishes). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…omment The two documented workaround groups had gotten blurred together: the "no override" group (falls back to introspection, fine since nothing downstream needs their C backend specifically) and the "C-only" group (rcl_interfaces, type_description_interfaces, service_msgs -- rmw_zenoh_pico needs the C backend from these three specifically) are disjoint. Routing a C-only package through the no-override rebuild "succeeds" (no build error) but silently leaves its rosidl_typesupport_c dispatch table pointing at introspection instead of microxrcedds -- caught this on type_description_interfaces via `strings ... | grep get_message_type_support_handle` showing introspection_c, not microxrcedds_c, wired into the dispatch table. Also clarifies the C-only group needs no separate no-override pass first: leaving CPP unset already falls back to introspection_cpp on its own, sidestepping the same _Event gap. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
emscripten-wasm32as an opt-in target platform for rolling, usingrmw_zenoh_picoas theRMW implementation. This follows the same emscripten-wasm32 port already
proven on humble (Tobias-Fischer/ros-humble, backed by
RoboStack/vinca#154), but
swaps the custom hand-rolled
wasm_cpp/rmw_wasm_cppRMW used there forzenoh-pico, which has real, actively-maintained upstream WASM/pthreadssupport — significantly less custom code to maintain.
All changes are gated by
if: wasm32/if: not wasm32selectors (orper-platform
robostack.yamldicts) and are additive — no behavior changefor existing platforms.
Verified end-to-end, both a C and a Python talker:
rclctalker node running in a real browser publishesstd_msgs/Stringviarmw_zenoh_picoover a real WebSocket to a nativezenohdrouter, received and correctly decoded by an independent nativeprocess.
rclpytalker (CPython built with--enable-wasm-pthreads,including a real, non-stubbed
numpy) running the same way in the samebrowser, also verified against a native subscriber.
Both are genuine cross-process interop, not just "doesn't crash."
What's included
vinca.yaml/robostack.yaml/patch/dependencies.yaml/pkg_additional_info.yaml: thread theemscripten-wasm32platform throughthe recipe pipeline (selectors, per-platform dependency-name mappings,
rmw_implementation/typesupport wiring for the ~230-recipe closure).conda_build_config.yaml/vinca_pinning.yaml: emscripten toolchain pin(emscripten-forge's compiler/SDK) plus targeted pin overrides where
emscripten-forge's package set diverges from conda-forge (python 3.13 vs
3.14, graphviz/libffi versions) — base pins are preserved for every other
platform.
pixi.toml: points thevincapypi dependency atRoboStack/vinca#154's branch
until it merges upstream.
patch/ros-rolling-*.patch: per-package emscripten build fixes (rclpynarrowing warning +
-latomiclink condition, rcutils, rosidl-generator-py,spdlog/yaml-cpp vendor policy, osrf-testing-tools-cpp,
rmw_test_fixture_implementation).
patch/ros-rolling-rmw-zenoh-pico.patch,patch/ros-rolling-rosidl-typesupport-microxrcedds-{c,cpp}.patch: buildrmw_zenoh_picoand its typesupport backend for wasm32. The underlyingfixes have been submitted upstream separately (see below); these patches
apply them to the exact pinned commits until they merge.
extra_recipes/zenoh-pico,extra_recipes/microcdr: new recipes forrmw_zenoh_pico's own dependencies, not otherwise packaged for RoboStack.Making this genuinely buildable from a clean checkout
Earlier revisions of this PR had only been exercised incrementally, on a
local
output/directory that had accumulated a lot of by-hand state. Sincethen it's been rebuilt from a genuinely empty
output/directory (twice, toalso re-verify the vinca ordering fix below), which surfaced several real
bugs a from-scratch build hits that an incremental one never does:
patch/dependencies.yaml:rosidl_typesupport_c/rosidl_typesupport_cppnow declare a real
add_hostdependency onros2-rosidl-typesupport-microxrcedds-{c,cpp}(and likewisetest_msgs/example_interfaces, needed for the same reason as the packages alreadylisted on
pixi.toml'sbuild-emscriptencomment below). Without this,rattler-build's solver has no declared reason to build the microxrcedds
typesupport packages before the packages that
find_package()them, so afrom-scratch build's ordering came down to luck — sometimes it built in
the right order, sometimes a plain
find_package()failed outright. Thisreplaced an earlier, hacky retry-loop workaround with the actual missing
dependency declaration, so rattler-build now builds the correct order
itself, every time.
extra_recipes/{microcdr,zenoh-pico}/build.sh: these existed only on alocal machine, never actually committed — silently swallowed by a blanket
*.sh.gitignorerule (now negated forextra_recipes/**/*.sh). Anygenuinely fresh checkout failed with "build.sh: command not found".
pixi.toml's newsync-native-bootstrap-mirrortask (wired intobuild-emscripten'sdepends-on) scripts a workaround that used to bemanual, undocumented tribal knowledge: several message packages' generated
recipes declare a
build:-time (build-platform, i.e. native osx-arm64)dependency on
rosidl_default_generators, but no channel publishes anative
ros2-*-prefixed build of anything — it's permanentlyunsatisfiable as declared. CMake's own
find_package()resolution onlyever needed the host-prefix (
emscripten-wasm32) copy (alreadycross-compilation-safe via
-DCMAKE_FIND_ROOT_PATH=$PREFIX); rattler-build'ssolver is what insists on a real build-platform channel entry. Mirroring
already-built
emscripten-wasm32packages into a same-namedosx-arm64channel directory and reindexing satisfies the solver without any actual
native rebuilding.
pixi.toml'svincapin is now onTobias-Fischer/vinca@c974b23
(part of RoboStack/vinca#154),
which fixes a genuine CMake target-registration-ordering bug found while
re-verifying this PR from scratch — see that PR's description for the
full root-cause writeup. In short: pre-finding the static typesupport
override for a message-generating package (rather than only for
consumers, which is what it's actually for) made that package's own
typesupport target jump ahead of the generator target its own
Export.cmakerequires, breaking every downstreamfind_package()of it.First surfaced here on
builtin_interfaces, breakingservice_msgs.Upstream PRs
The genuine bugs found along the way (not RoboStack-specific) have been
submitted upstream rather than only patched here:
ament_cmake_ros_core+export-set/
BUILD_INTERFACEfixesrcl_logging_implementation'sdlopen-by-name backend selection (found getting
rclpy's logging initworking)
CMAKE_SHARED_MODULE_CREATE_*_FLAGS, not just theSHARED-libraryvariant. CMake's
MODULElibrary type — whatpybind11_add_module()usesfor every Python C extension, including
rclpy's own_rclpy_pybind11and each message package's
rosidl_generator_pytypesupport accessor —is distinct from
SHAREDand was linking withoutUSE_PTHREADS=1. Neverhit by the
rclc/C-only path; only surfaced once a realrclpydemolinked a pybind11 module into the same pthreads build.
Getting
rclpyrunning: what it actually tookEarlier revisions of this PR reported
rclpyas blocked by CPython forwasm32 not being pthread-enabled. That's fixed, and it turned out to be
one configure flag: CPython upstream has shipped
--enable-wasm-pthreadssince 3.11 (adds exactly
-pthread -sUSE_PTHREADS -sPROXY_TO_PTHREAD, thesame flags this whole pipeline already uses) —
emscripten-forge's ownpythonrecipe just doesn't turn it on. Building a custom variant withthat flag, plus registering
numpy's ~14 eagerly-imported compiledextension modules ahead of time (this build doesn't enable CPython's
--enable-wasm-dynamic-linking— its own docs flag that combination withpthreads as known-crashy — so nothing can rely on Python's normal
importdynamically
dlopen-ing a.so), got a realrclpytalker with realnumpyrunning end-to-end.Two more narrow gaps surfaced along the way, both documented in this repo
rather than silently patched around:
rosidl_typesupport_microxrcedds_cpp's codegen doesn't handle service"_Event" messages — specific to the C++ typesupport variant; the
C variant those same packages (
rcl_interfaces,type_description_interfaces,service_msgs) need forrmw_zenoh_picobuilds fine. See the comment on
pixi.toml'sbuild-emscriptentask.rmw_zenoh_picodoesn't support publisher/subscriber QoS event handlers(
RCL_PUBLISHER_OFFERED_INCOMPATIBLE_QOSetc.) orTypeDescriptionService— the former fails with a plain
RCLErrorinstead of theUnsupportedEventTypeErrorrclpyalready handles gracefully, and thelatter hangs rather than erroring. Worked around locally for the demo
(not upstreamed — didn't root-cause the
rmw_zenoh_picoside tonight);worth a closer look if
rclpysupport here gets formalized further.The
rclpydemo itself (build script, patchedrclpy/numpy, browserharness) is local-only exploratory tooling, same as the existing
rclcone — not part of this PR's recipe changes.
Test plan
emscripten-wasm32+ rmw_zenoh_pico, from a genuinely emptyoutput/directory (not incrementally on top of prior local state).zenohd→independent native subscriber, verified receiving correctly-decoded
messages via the router's own REST API (not just the browser's own
console).
rclpytalker (realnumpy, not stubbed), alsoindependently confirmed via the router's REST API.
tooling.
Full write-up
All the changes this required, across every repo, are documented together in Tobias-Fischer/ros2-emscripten-zenoh-demo — including a working
rclcandrclpybrowser demo verified end-to-end against a nativezenohdrouter.🤖 Generated with Claude Code