Skip to content

Add emscripten-wasm32 support with rmw_zenoh_pico - #46

Open
Tobias-Fischer wants to merge 7 commits into
RoboStack:codex/cross-distro-syncfrom
Tobias-Fischer:feature/emscripten-wasm32-zenoh-pico
Open

Add emscripten-wasm32 support with rmw_zenoh_pico#46
Tobias-Fischer wants to merge 7 commits into
RoboStack:codex/cross-distro-syncfrom
Tobias-Fischer:feature/emscripten-wasm32-zenoh-pico

Conversation

@Tobias-Fischer

@Tobias-FischerTobias-Fischer commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds emscripten-wasm32 as an opt-in target platform for rolling, using
rmw_zenoh_pico as the
RMW 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_cpp RMW used there for
zenoh-pico, which has real, actively-maintained upstream WASM/pthreads
support — significantly less custom code to maintain.

All changes are gated by if: wasm32 / if: not wasm32 selectors (or
per-platform robostack.yaml dicts) and are additive — no behavior change
for existing platforms.

Verified end-to-end, both a C and a Python talker:

  • A WebAssembly rclc talker node running in a real browser publishes
    std_msgs/String via rmw_zenoh_pico over a real WebSocket to a native
    zenohd router, received and correctly decoded by an independent native
    process.
  • A real rclpy talker (CPython built with --enable-wasm-pthreads,
    including a real, non-stubbed numpy) running the same way in the same
    browser, 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 the emscripten-wasm32 platform through
    the 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 the vinca pypi dependency at
    RoboStack/vinca#154's branch
    until it merges upstream.
  • patch/ros-rolling-*.patch: per-package emscripten build fixes (rclpy
    narrowing warning + -latomic link 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: build
    rmw_zenoh_pico and its typesupport backend for wasm32. The underlying
    fixes 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 for
    rmw_zenoh_pico's own dependencies, not otherwise packaged for RoboStack.

Upstream PRs

The genuine bugs found along the way (not RoboStack-specific) have been
submitted upstream rather than only patched here:

Getting rclpy running: what it actually took

Earlier revisions of this PR reported rclpy as blocked by CPython for
wasm32 not being pthread-enabled. That's fixed, and it turned out to be
one configure flag: CPython upstream has shipped --enable-wasm-pthreads
since 3.11 (adds exactly -pthread -sUSE_PTHREADS -sPROXY_TO_PTHREAD, the
same flags this whole pipeline already uses) — emscripten-forge's own
python recipe just doesn't turn it on. Building a custom variant with
that flag, plus registering numpy's ~14 eagerly-imported compiled
extension modules ahead of time (this build doesn't enable CPython's
--enable-wasm-dynamic-linking — its own docs flag that combination with
pthreads as known-crashy — so nothing can rely on Python's normal import
dynamically dlopen-ing a .so), got a real rclpy talker with real
numpy running 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 for rmw_zenoh_pico
    builds fine. See the comment on pixi.toml's build-emscripten task.
  • rmw_zenoh_pico doesn't support publisher/subscriber QoS event handlers
    (RCL_PUBLISHER_OFFERED_INCOMPATIBLE_QOS etc.) or TypeDescriptionService
    — the former fails with a plain RCLError instead of the
    UnsupportedEventTypeErrorrclpy already handles gracefully, and the
    latter hangs rather than erroring. Worked around locally for the demo
    (not upstreamed — didn't root-cause the rmw_zenoh_pico side tonight);
    worth a closer look if rclpy support here gets formalized further.

The rclpy demo itself (build script, patched rclpy/numpy, browser
harness) is local-only exploratory tooling, same as the existing rclc
one — not part of this PR's recipe changes.

Test plan

  • Full recipe closure (~230 packages) builds green for
    emscripten-wasm32 + rmw_zenoh_pico.
  • Browser demo: wasm32 rclc talker → real WebSocket → native zenohd
    independent native subscriber, verified receiving correctly-decoded
    messages.
  • Same, with a real rclpy talker (real numpy, not stubbed).
  • Confirmed against a real (non-sandboxed) browser, not just automated
    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 rclcandrclpy browser demo verified end-to-end against a native zenohd router.

🤖 Generated with Claude Code

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>
Tobias-Fischerand others added 6 commits September 9, 2026 14:47
- 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
Sign up for freeto 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.

1 participant

@Tobias-Fischer