feat(hydro_lang): add embedded runtime helpers and embedded_flow! macro#3011
feat(hydro_lang): add embedded runtime helpers and embedded_flow! macro#3011shadaj wants to merge 1 commit into
embedded_flow! macro#3011Conversation
Deploying hydro with
|
| Latest commit: |
21713df
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://f8fad2aa.hydroflow.pages.dev |
| Branch Preview URL: | https://sandbox-61167f85-ecce-4c35-8.hydroflow.pages.dev |
…acro Upstreams a downstream app's embedded-mode runtime helpers into `hydro_lang` behind the `embedded_runtime` feature, as the new `hydro_lang::embedded` module: - `InputBuffer<T>` / `InputStream<T>`: address-stable queue backing a `Stream` input. - `CallbackSlot<T>`: address-stable slot holding a scoped `FnMut(T)` output callback. - `AliasedBox<T>` / `OwnedErasedBox`: owning allocations without `Box`'s noalias guarantee. - `DfirRunnable`: object-safe erasure for the unnameable `Dfir<impl TickClosure>`. - `embedded_flow!`: declares a safe, self-contained holder struct for a generated flow. Safety/perf fixes relative to the downstream version (validated under Miri/Stacked Borrows): - fix: `Box`-move aliasing UB — holder fields were `Box`es whose moves invalidate the flow's raw pointers into their contents; replaced with new `AliasedBox` (raw-pointer ownership, `Deref` only), and the cluster self-id is now owned via `OwnedErasedBox`. - fix: `invoke_with` left a dangling callback installed on panic, and nested `invoke_with` cleared (instead of restored) the outer callback; a drop guard now saves/restores the previous pointer. - fix: re-entrant slot invocation could alias the running callback's `&mut`; the callback pointer lives in a `Cell` and is taken out while it runs. - fix: `push` held a borrow of the waker cell while running foreign waker code; wakes are now taken-and-consumed (coalescing redundant wakes), `poll_next` skips waker bookkeeping on the ready path and uses `Waker::clone_from` (`will_wake` fast path), with a queue re-check to avoid lost wakeups. - fix: `InputBuffer` was auto-`Send`, letting safe code race pushes across threads; it is now `!Send` (`PhantomData<*mut ()>`). - fix: the holder no longer runs the flow during construction before callbacks can be installed (which silently discarded initialization outputs). - no_std-readiness: `core`-only primitives with `const fn new`, `Pin`-based address stability contracts, and alloc-dependent pieces isolated behind `embedded::__private` re-exports so the macro can later switch `std` -> `alloc` non-breakingly. The macro is redesigned from the downstream two-variant form into a single arm with optional sections (`self_id`, `membership`, `inputs`, `network_in`, `outputs`, `network_out`), covering shapes the old macro could not (e.g. receive-only cluster members, membership streams). It generates accessors, `run()`, and `run_with(...)` (which installs all output callbacks via nested `invoke_with`). test(hydro_lang): unit tests for the primitives (waker coalescing, FIFO order, move stability, nested/panicking/re-entrant callback installs, erased drop). test(hydro_test_embedded): `embedded_flow!` holder tests against the real generated flows (capitalize, singleton input, o2o echo wiring two holders together, o2m broadcast with membership, m2m broadcast), including re-entrant input pushes from output callbacks; these also pass under Miri. build(hydro_lang,hydro_std,hydro_test): post-process `stageleft_tool::gen_final!` output to gate the generated stageleft registration ctor behind `cfg(not(miri))`, since the `ctor` crate's init_array entries trip Miri's ABI checks; marked TODO for removal once fixed upstream (hydro-project/stageleft#84). Co-authored-by: Infinity 🤖 <infinity@hydro.run> PR: #3011
4fc8e91 to
21713df
Compare
|
Marking as draft while waiting on Stageleft release. |
| //! - [`AliasedBox`] / [`OwnedErasedBox`]: owned heap allocations that, unlike [`Box`], may | ||
| //! be moved while raw pointers into their contents exist. |
There was a problem hiding this comment.
Is this different from Pin<Box<T>>?
There was a problem hiding this comment.
Pull request overview
This PR adds a new hydro_lang::embedded module (behind the embedded_runtime feature) that provides safe runtime primitives and the embedded_flow! macro for driving embedded-mode generated DFIR flows, plus test coverage for both the primitives and real generated embedded flows. It also introduces a build-script workaround to gate stageleft-generated ctor registration under cfg(not(miri)) to keep Miri runs working until upstream fixes land.
Changes:
- Add
hydro_lang::embeddedruntime helpers (InputBuffer,CallbackSlot,AliasedBox,OwnedErasedBox,DfirRunnable) and theembedded_flow!macro. - Add embedded holder tests in
hydro_test_embeddedexercising real generated flows and re-entrancy patterns. - Post-process stageleft output in multiple crates’
build.rsto gate generated ctor registration undercfg(not(miri)).
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| hydro_test/build.rs | Post-processes stageleft-generated staged_deps.rs to gate ctor registration under cfg(not(miri)) for Miri compatibility. |
| hydro_test_embedded/src/lib.rs | Adds tests for embedded_flow! holder structs against real generated embedded flows (including network wiring scenarios). |
| hydro_std/build.rs | Same stageleft staged_deps.rs post-processing for Miri compatibility. |
| hydro_lang/src/lib.rs | Exposes the new embedded module behind the embedded_runtime feature flag. |
| hydro_lang/src/embedded.rs | Introduces the embedded runtime primitives, helper macros, and unit tests for the primitives. |
| hydro_lang/build.rs | Same stageleft staged_deps.rs post-processing for Miri compatibility. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| //! # `no_std` | ||
| //! | ||
| //! The core primitives ([`InputBuffer`], [`CallbackSlot`]) only use `core` items, have | ||
| //! `const fn new` constructors, and communicate address stability through [`Pin`], so they | ||
| //! are compatible with future `no_std` targets using statically-allocated (fixed-size) | ||
| //! storage. Only the owning-allocation helpers ([`AliasedBox`], [`OwnedErasedBox`]) and the | ||
| //! [`embedded_flow!`](crate::embedded_flow) macro require `alloc`. |
| let staged_deps = | ||
| std::path::Path::new(&std::env::var("OUT_DIR").unwrap()).join("staged_deps.rs"); | ||
| let contents = std::fs::read_to_string(&staged_deps).unwrap(); | ||
| std::fs::write( | ||
| &staged_deps, |
| // stageleft registration ctor behind `cfg(not(miri))`. | ||
| let staged_deps = | ||
| std::path::Path::new(&std::env::var("OUT_DIR").unwrap()).join("staged_deps.rs"); | ||
| let contents = std::fs::read_to_string(&staged_deps).unwrap(); | ||
| std::fs::write( |
| // stageleft registration ctor behind `cfg(not(miri))`. | ||
| let staged_deps = | ||
| std::path::Path::new(&std::env::var("OUT_DIR").unwrap()).join("staged_deps.rs"); | ||
| let contents = std::fs::read_to_string(&staged_deps).unwrap(); | ||
| std::fs::write( |
Upstreams a downstream app's embedded-mode runtime helpers into
hydro_langbehind theembedded_runtimefeature, as the newhydro_lang::embeddedmodule:InputBuffer<T>/InputStream<T>: address-stable queue backing aStreaminput.CallbackSlot<T>: address-stable slot holding a scopedFnMut(T)output callback.AliasedBox<T>/OwnedErasedBox: owning allocations withoutBox's noalias guarantee.DfirRunnable: object-safe erasure for the unnameableDfir<impl TickClosure>.embedded_flow!: declares a safe, self-contained holder struct for a generated flow.Safety/perf fixes relative to the downstream version (validated under Miri/Stacked Borrows):
Box-move aliasing UB — holder fields wereBoxes whose moves invalidate theflow's raw pointers into their contents; replaced with new
AliasedBox(raw-pointerownership,
Derefonly), and the cluster self-id is now owned viaOwnedErasedBox.invoke_withleft a dangling callback installed on panic, and nestedinvoke_withcleared (instead of restored) the outer callback; a drop guard now saves/restores the
previous pointer.
&mut; the callbackpointer lives in a
Celland is taken out while it runs.pushheld a borrow of the waker cell while running foreign waker code; wakes arenow taken-and-consumed (coalescing redundant wakes),
poll_nextskips waker bookkeepingon the ready path and uses
Waker::clone_from(will_wakefast path), with a queuere-check to avoid lost wakeups.
InputBufferwas auto-Send, letting safe code race pushes across threads; it isnow
!Send(PhantomData<*mut ()>).installed (which silently discarded initialization outputs).
core-only primitives withconst fn new,Pin-based addressstability contracts, and alloc-dependent pieces isolated behind
embedded::__privatere-exports so the macro can later switch
std->allocnon-breakingly.The macro is redesigned from the downstream two-variant form into a single arm with
optional sections (
self_id,membership,inputs,network_in,outputs,network_out), covering shapes the old macro could not (e.g. receive-only clustermembers, membership streams). It generates accessors,
run(), andrun_with(...)(which installs all output callbacks via nested
invoke_with).test(hydro_lang): unit tests for the primitives (waker coalescing, FIFO order, move
stability, nested/panicking/re-entrant callback installs, erased drop).
test(hydro_test_embedded):
embedded_flow!holder tests against the real generated flows(capitalize, singleton input, o2o echo wiring two holders together, o2m broadcast with
membership, m2m broadcast), including re-entrant input pushes from output callbacks;
these also pass under Miri.
build(hydro_lang,hydro_std,hydro_test): post-process
stageleft_tool::gen_final!outputto gate the generated stageleft registration ctor behind
cfg(not(miri)), since thectorcrate's init_array entries trip Miri's ABI checks; marked TODO for removal oncefixed upstream (hydro-project/stageleft#84).