Add emscripten-wasm32 support with rmw_zenoh_pico - #46
Open
Tobias-Fischer wants to merge 20 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>
… at the source Root-causes and fixes the gap that previously required rebuilding a dozen-plus packages by hand with scoped typesupport overrides after the main build-emscripten pass: rosidl_typesupport_microxrcedds_cpp's srv__type_support_cpp.cpp.em (and its matching header template) never generated typesupport for the auto-generated service/action "_Event" message, unlike the C generator, which already handled it correctly. Any package defining a service or action failed to compile as soon as something needed get_message_type_support_handle<...Event>() for it. Patched onto the same pinned commit this package already builds from (patch/ros-rolling-rosidl-typesupport-microxrcedds-cpp.patch, alongside the existing ament_cmake_ros_core/export-set fixes) rather than worked around here. Upstream PR: micro-ROS/rosidl_typesupport_microxrcedds#83. Verified building the full closure in a single `pixi run build-emscripten` pass with both typesupport overrides set globally (the "single approach") -- action_msgs, rcl_interfaces, type_description_interfaces, test_msgs and the rest of the previously-affected packages all build correctly now, with the correct typesupport backend confirmed wired into their dispatch tables. This removes the whole two-group scoped-rebuild workaround from build-emscripten's own comment -- simplified accordingly, along with the CI script and docs in the demo repo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Tobias-Fischer
added a commit
to Tobias-Fischer/ros2-emscripten-zenoh-demo
that referenced
this pull request
Sep 10, 2026
RoboStack/ros-rolling#46 patches rosidl_typesupport_microxrcedds_cpp's own _Event codegen gap at the source now (upstream PR: micro-ROS/rosidl_typesupport_microxrcedds#83) instead of needing a dozen affected packages rebuilt by hand with different typesupport overrides after the main build. A single `pixi run build-emscripten` pass now builds the entire closure correctly, so build_ros_rolling_closure.sh only needs to handle the (unrelated) native bootstrap mirror requirement -- loops sync+build until a pass adds nothing new, instead of the previous two-group scoped-rebuild machinery. Updates docs/demo_env.md and README.md's Known limitations to match -- the _Event gap is fixed, not worked around, so the reproduction steps and limitations list no longer describe a workaround that doesn't exist anymore. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cp output/emscripten-wasm32/*.tar.bz2 fails with "No such file or directory" when that directory doesn't exist yet (a truly cold build, no prior output/ at all) or is empty -- the shell has no files to expand the glob against, so it's passed to cp as a literal, non-existent filename. Every local test of this task happened to already have some previously- built packages lying around, so this never showed up until the first genuinely-cold CI run (RoboStack/ros-rolling has never had this task before tonight) hit it immediately, on the very first pixi run build-emscripten invocation, before a single package had been built. find ... -exec cp, unlike a bare glob, does nothing (no error) when nothing matches -- also adding mkdir -p for output/emscripten-wasm32 itself so the find has somewhere to look on the very first invocation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
rattler-index's default incremental update can leave repodata.json pointing at a stale hash for a file whose content changed without its name/version/build-string changing (e.g. rebuilding a package after fixing a bug in it, same output filename each time) -- surfaced tonight as a confusing "failed to fetch <package>" on a completely unrelated package that merely depended on the one with the stale hash. --force (full reindex instead of incremental) costs a fraction of a second even on this whole ~230-package channel, so there's no real tradeoff here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
For the browser demo's upcoming teleop (geometry_msgs/Twist), camera (sensor_msgs/Image), and GPS (sensor_msgs/NavSatFix) pages. Neither package is a transitive dependency of anything already in this closure, so both need selecting explicitly in vinca.yaml's wasm32 packages_select_by_deps, plus the same per-package microxrcedds add_host entry every message package in this closure needs (see the new comment in patch/dependencies.yaml -- there's no generic/wildcard rule, this is genuinely per-package). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Tobias-Fischer
added a commit
to Tobias-Fischer/ros2-emscripten-zenoh-demo
that referenced
this pull request
Sep 10, 2026
Publishes Twist on 'cmd_vel' at 10 Hz, driven by W/A/S/D or the arrow keys. Verified end-to-end: keyboard event -> published Twist -> confirmed via the zenohd router's own REST API (linear.x/angular.z match the expected scaled values, zero when no keys are held). The one real wrinkle: this build uses -sPROXY_TO_PTHREAD=1 (same as talker_rclc, needed by rmw_zenoh_pico), so main() runs on a pthread Web Worker, not the page's real main thread -- which has no window/DOM access. Tried MAIN_THREAD_EM_ASM* first (the "proxy JS to the main thread" mechanism this is normally for), but its macro internals turned out too fragile for a multi-branch JS body to get through the C preprocessor's stringification reliably. Landed on a more robust approach instead: the keyboard listener lives entirely in plain JS in index_teleop.html (already on the real main thread, no proxying needed), writing directly into wasm linear memory -- a SharedArrayBuffer in a pthreads build -- at an address teleop_get_state_ptr() exports once. The worker-side timer callback just reads a C global directly on its 10 Hz hot path, no JS call needed there at all. Requires ccall and HEAPF64 explicitly added to -sEXPORTED_RUNTIME_METHODS -- neither is exposed by default, and their absence surfaces as a runtime abort, not a link/build error. Also adds geometry_msgs to demo_env's own dependency list now that RoboStack/ros-rolling#46 builds it (see that repo's own commit for adding it to the wasm32 closure in the first place). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sync-native-bootstrap-mirror mirrored built emscripten-wasm32 packages into a directory literally named output/osx-arm64 -- right for local dev on this project's own Apple Silicon Mac, but rattler-build's solver only ever looks at the channel subdirectory matching build_platform, which is whatever machine is *actually running the build*. On this repo's own CI (ubuntu-latest, i.e. linux-64), the mirror silently wrote into a directory the solver never looked at, so the entire rest of the closure past the handful of "core" packages that don't need the native rosidl_default_generators bootstrap never got past "No candidates were found for ros2-<package>" -- confirmed as the actual first-ever CI failure of the corrected pipeline, 2026-09-10. sync_native_bootstrap_mirror.sh now detects uname -s/-m and mirrors into the matching platform directory (osx-arm64, osx-64, linux-64, or linux-aarch64) instead. Also had to add a .gitignore negation for the new script -- caught myself about to repeat the exact same mistake this repo's own .gitignore already documents (a blanket *.sh rule meant for vinca-generated recipe scripts silently swallowing a genuine, hand-written one). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Tobias-Fischer
added a commit
to Tobias-Fischer/ros2-emscripten-zenoh-demo
that referenced
this pull request
Sep 10, 2026
…oyment Two real gaps this closes, not just visual polish: - None of the new demo pages (teleop, GPS, camera, IMU, robot_view) had any deployment path at all -- deploy.yml's asset-copying step only ever copied non-HTML files from browser_demo/out/ into _pages/assets/ (for the two talkers embedded directly into the main site via demo.js's "Run demo" buttons). The new pages, and the existing standalone index_rclc.html/index_rclpy.html, were never reachable on the live site. Now the whole browser_demo/out/ directory (HTML alongside its .js/.wasm/.so files, self-contained) also ships as its own demos/ subtree. - site/index.html's "Known limitations" section still described the rosidl_typesupport_microxrcedds_cpp _Event codegen gap as a live workaround (introspection-only fallback / scoped C-only rebuilds) -- fixed at the source in RoboStack/ros-rolling#46 earlier tonight, so that bullet no longer describes anything that actually happens anymore. Styling: browser_demo/demo-page.css reuses the same design tokens as site/style.css (dark theme, teal/purple accents) but as its own self-contained file, since these pages deploy into a separate subtree from the main site rather than sharing a directory with it. Mobile-first --- GPS and phone-tilt are meant to be opened on an actual phone -- with a shared nav between all seven demo pages and 44px-minimum tap targets. Verified visually at both desktop and 375px mobile viewport widths. Also adds a "More demos" section to site/index.html linking to all five new pages. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…usercontent.com URLs CI started failing recipe generation outright: vinca -m --platform emscripten-wasm32 crashed fetching ros2cli's pinned release package.xml with a 404, because raw.githubusercontent.com's short <ref>/<path> URL form can't tell where a slash-containing tag name (release/rolling/ros2cli/0.41.1-1) ends and the path begins -- a guess that's cached inconsistently across CDN edges and 404s from some vantage points (GitHub Actions runners included) while resolving fine from others. Cherry-picked the fix (vinca@9e44663, from a separate branch, not a direct ancestor) onto the tip of our feature/emscripten-pthreads branch rather than just re-pointing the pin there directly, since that branch lacks the emscripten pthreads/demangle/typesupport-override fixes this whole pipeline depends on. Fast-forward push, so nothing on the branch was rewritten. Verified locally: the same URL that 404s with the short form resolves (200) with refs/tags/, and a full `vinca -m --platform emscripten-wasm32` run now generates all 233 recipes without error. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sm32 rmw_zenoh_pico_set_unicast() (and set_mode()/get_mode()) aren't part of the standard rmw.h ABI surface, so nothing inside this library or a normal ROS 2 caller ever references them -- unlike the standard rmw_* functions (looked up by rmw_implementation via dlopen+dlsym, so implicitly kept live), wasm-ld's side-module build drops anything with zero references from the dylink export table. Confirmed via wasm-objdump: the built .so exported every standard rmw_* symbol but none of these three. A wasm32 app that wants to override the compiled-in zenoh connect address at runtime (127.0.0.1:7447 -- see ros2-emscripten-zenoh-demo's browser_demo/, which now does exactly this) needs rmw_zenoh_pico_set_unicast() actually callable. Explicit -Wl,--export= for the three extension functions, scoped to this one target and gated on EMSCRIPTEN, fixes it -- verified with a scoped local rebuild (wasm-objdump now shows all three exported) and an end-to-end browser_demo test overriding the connect address via a URL param. 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, and (the cpppatch) fix a real codegen gap in that typesupport backend — see
"Upstream PRs" below. The underlying fixes have been submitted upstream
separately; 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.rosidl_typesupport_microxrcedds_cpp'scodegen didn't handle ROS 2's auto-generated service/action "_Event"
message, which used to mean rebuilding a dozen-plus affected packages by
hand with different typesupport overrides after the main build (see the
old revision of
pixi.toml'sbuild-emscriptencomment in this PR'shistory for exactly how involved that was). Fixed at the source instead
— see "Upstream PRs" below — so a single
pixi run build-emscriptenpasswith both typesupport overrides set globally now builds the entire
closure correctly. No more scoped rebuilds, and no more risk of silently
routing a package through the wrong one (confirmed happened once, to
type_description_interfaces, before this fix).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_INTERFACEfixes, plus the missing Event-messagetypesupport codegen (see below)
rcl_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.One more narrow gap surfaced along the way, documented in this repo rather
than silently patched around:
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),in a single
pixi run build-emscriptenpass with both typesupportoverrides set globally — no scoped per-package rebuilds.
action_msgs,rcl_interfaces,type_description_interfaces,test_msgs, ...)confirmed building with the correct typesupport backend actually
wired into its dispatch table (checked via
stringson the built.so).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