subsecond: simple test does not work when async
Problem
Minimum test to understand how to use subsecond. Works without tokio and async and does nothing (no error) when async is involved
Steps To Reproduce
Steps to reproduce the behavior:
- run dx serve --hot-patch
- See the line "Hello" being print over and over again
- change the string to "Hello World!" in the get_response function
- See the hot patching log from the dx cli
- Nothing change the line is still just "Hello"
Code :
// Import the dioxus_devtools crate for hot-reload and devtools integration.
use dioxus_devtools::connect_subsecond;
// Entry point of the application.
#[tokio::main]
async fn main() {
// Initialize the dioxus_devtools for hot-reloading.
connect_subsecond();
// Start the main application loop.
run_application().await;
}
// Main application loop that runs indefinitely.
async fn run_application() {
loop {
// Call the get_response function through subsecond::call to enable hot-reloading.
let greeting = subsecond::call(get_response).await;
// Print the result to the console.
println!("{greeting}");
// Sleep for 10 seconds before repeating.
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
}
}
// Function that returns a greeting string.
// This function can be hot-reloaded at runtime.
async fn get_response() -> String {
"Hello World!".to_string()
}
[dependencies]
dioxus-devtools = "0.7.0-alpha.1"
subsecond = "0.7.0-alpha.1"
tokio = { version = "1.45.1", features = ["full"] }
Same code without tokio and async works
Expected behavior
- run dx serve --hot-patch
- See the line "Hello" being print over and over again
- change the string to "Hello World!" in the get response method
- See the hot patching log from the dx cli
- The line now prints "Hello World!"
Screenshots
Environment:
- Dioxus version: dioxus 0.7.0-alpha.1 (662cf0d)
- Rust version: 1.87.0
- OS info: Windows 11
- App platform: None, just trying out subsecond
Questionnaire I'm interested in fixing this myself but don't know where to start.
This is because subsecond::current uses the typeid to line up types across patches. Functions that return futures therefore change in signature between patches and won't be picked up by the engine.
To work around this you want to use a concrete type, ideally a Box<dyn Future> somewhere to prevent the generic from cascading out of the current call.
oh I see, thanks to your comment I got it working with:
use dioxus_devtools::{connect_subsecond};
use futures::FutureExt;
use std::future::Future;
use std::pin::Pin;
type AsyncResult<T> = Pin<Box<dyn Future<Output = T> + Send>>;
#[tokio::main]
async fn main() {
connect_subsecond();
run_application().await;
}
async fn run_application() {
loop {
let future = subsecond::call(get_response_future);
let greeting = future.await;
println!("{}", greeting);
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
}
}
fn get_response_future() -> AsyncResult<String> {
async {
"Hello World!".to_string()
}.boxed()
}
I wonder, if exposing a subsecond function call_async with the Pin<Box<dyn Future>> done under the hood is a good idea 🤔 I played a little and this seems to work for a simple case:
pub fn call_async<F, Fut, O>(mut f: F) -> Pin<Box<dyn Future<Output = O> + Send>>
where
F: FnMut() -> Fut + 'static,
Fut: Future<Output = O> + Send + 'static,
O: 'static,
{
// Only run in debug mode - the rest of this function will dissolve away
if !cfg!(debug_assertions) {
return Box::pin(f());
}
// Create a wrapper function that boxes the future
// This converts our async function into a sync function that returns a boxed future
let mut wrapper = move || -> Pin<Box<dyn Future<Output = O> + Send>> { Box::pin(f()) };
let mut hotfn = HotFn::current(wrapper);
loop {
let res = std::panic::catch_unwind(AssertUnwindSafe(|| hotfn.call(())));
// If the call succeeds just return the result, otherwise we try to handle the panic if its our own.
let err = match res {
Ok(res) => return res,
Err(err) => err,
};
// If this is our panic then let's handle it, otherwise we just resume unwinding
let Some(_hot_payload) = err.downcast_ref::<HotFnPanic>() else {
std::panic::resume_unwind(err);
};
}
}
We might be able to add something like that, but for in-place cases, even just casting the result should work:
fn main() {
let res = subsecond::current(|| Box::new(do_thing()) as Box<dyn Future<Output = String>>).await;
}
async fn do_thing() -> String {
"hi".to_string()
}
Based on your example, we can make @JT117's code work like this:
// Entry point of the application.
#[tokio::main]
async fn main() {
// Initialize the dioxus_devtools for hot-reloading.
dioxus_devtools::connect_subsecond();
// Start the main application loop.
run_application().await;
}
// Main application loop that runs indefinitely.
async fn run_application() {
loop {
let greeting = subsecond::call(|| {
Box::pin(get_response()) as std::pin::Pin<Box<dyn Future<Output = String> + Send>>
})
.await;
// Print the result to the console.
println!("{greeting}");
// Sleep for 10 seconds before repeating.
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
}
}
// Function that returns a greeting string.
// This function can be hot-reloaded at runtime.
async fn get_response() -> String {
"Hello Subsecond!".to_string()
}
Thanks for your feedback.
For helping others on the same boat, after fighting with using this in axum for a while, I ended up with the following setup which seems to be working nicely! The idea is to wrap async fn -> Result<T, E> into fn -> Box<Pin<dyn Future<Output = Result<T, E>>>.
I'm sure there is a better way to do this, and it would be amazing to have this built-into subsecond, but this is working fine for my purposes so far:
macros/lib.rs:
use proc_macro::TokenStream;
use quote::quote;
use syn::{ItemFn, parse_macro_input};
#[proc_macro_attribute]
pub fn hot(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(item as ItemFn);
// Ensure this is an async function
if input_fn.sig.asyncness.is_none() {
return syn::Error::new_spanned(
input_fn.sig.fn_token,
"The #[hot] attribute can only be applied to async functions",
)
.to_compile_error()
.into();
}
// Extract the function components
let fn_vis = &input_fn.vis;
let fn_sig = &input_fn.sig;
let fn_body = &input_fn.block;
let expanded = quote! {
#fn_vis #fn_sig {
call_async(move || async move #fn_body).await
}
};
TokenStream::from(expanded)
}
helpers.rs:
use std::panic::AssertUnwindSafe;
use std::sync::OnceLock;
use std::{future::Future, pin::Pin};
use subsecond::{self, HotFn, HotFnPanic};
pub fn call_async<F, Fut, O>(f: F) -> Pin<Box<dyn Future<Output = O> + Send>>
where
F: FnOnce() -> Fut + 'static,
Fut: Future<Output = O> + Send + 'static,
O: 'static,
{
// For FnOnce, we need to handle this differently since we can only call it once
// We'll store the closure in an Option and take it when needed
let mut f_option = Some(f);
// Create a wrapper function that boxes the future
let wrapper = move || -> Pin<Box<dyn Future<Output = O> + Send>> {
if let Some(closure) = f_option.take() {
Box::pin(closure())
} else {
// This shouldn't happen in normal hot reload scenarios since each
// hot reload creates a new call_async invocation
panic!(
"Hot reload closure already consumed - this indicates a problem with the hot reload system"
)
}
};
let mut hotfn = HotFn::current(wrapper);
loop {
let res = std::panic::catch_unwind(AssertUnwindSafe(|| hotfn.call(())));
// If the call succeeds just return the result, otherwise we try to handle the panic if its our own.
let err = match res {
Ok(res) => return res,
Err(err) => err,
};
// If this is our panic then let's handle it, otherwise we just resume unwinding
let Some(_hot_payload) = err.downcast_ref::<HotFnPanic>() else {
std::panic::resume_unwind(err);
};
// For hot reload with FnOnce, we can't retry with the same closure
// The hot reload system should create a new function call entirely
panic!(
"Hot reload detected but cannot retry with FnOnce closure - hot reload should create new function instance"
);
}
}
main.rs:
#[hot]
async fn test_route() -> Html<&'static str> {
Html("Hello, world!")
}
#[tokio::main]
async fn main() {
dioxus_devtools::connect_subsecond();
tokio::spawn(async {
let app = Router::new()
.route("/", get(test_route));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Server running on http://localhost:3000");
axum::serve(listener, app.clone()).await.unwrap()
})
.await
.unwrap();
loop {
std::thread::sleep(std::time::Duration::from_millis(1000));
}
}
Layering tower-livereload on top gives a pretty solid dev experience!
To help out the usecase here, I wrote a little subsecond_serve that turns a future into a hot-patchable context.
https://github.com/DioxusLabs/dioxus/pull/4588
You can edit your axum router if you want since it's wrapped in the outer future.
#[tokio::main]
async fn main() {
dioxus_devtools::serve_subsecond(router_main).await;
}
async fn router_main() {
use axum::{Router, routing::get};
let app = Router::new().route("/", get(test_route));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Server running on http://localhost:3000");
axum::serve(listener, app.clone()).await.unwrap()
}
async fn test_route() -> axum::response::Html<&'static str> {
"axum works!!!!!".into()
}
I've created a simple project to test out serve_subsecond with dioxus to be able to create a custom axum router setup (with different layers and so on) in here
but it won't work, and it keeps giving me this error
21:58:27 [web] panicked at /home/salman/coding/dioxus/packages/subsecond/subsecond/src/lib.rs:647:10:
called `Result::unwrap()` on an `Err` value: JsValue(CompileError: wasm validation error: at offset 4: failed to match magic number
)
Stack:
__wbg_get_imports/imports.wbg.__wbg_new_e159a89571c5975f/<@http://localhost:8080/wasm/something.js:8482:21
logError@http://localhost:8080/wasm/something.js:17:18
__wbg_get_imports/imports.wbg.__wbg_new_e159a89571c5975f@http://localhost:8080/wasm/something.js:8481:66
something.wasm.__wbg_new_e159a89571c5975f externref shim@http://localhost:8080/wasm/something_bg.wasm:wasm-function[50543]:0xd32bef
something.wasm._ZN10dioxus_web8devtools9hook_impl5Error3new17hc594ef3660fc1ac4E@http://localhost:8080/wasm/something_bg.wasm:wasm-function[30293]:0xbf86aa
something.wasm._ZN10dioxus_web8devtools9hook_impl17h53e579be0d3a4982E@http://localhost:8080/wasm/something_bg.wasm:wasm-function[8212]:0x742d00
something.wasm._ZN10dioxus_web8devtools4init28_$u7b$$u7b$closure$u7d$$u7d$17h6f7c0fcb380597a0E@http://localhost:8080/wasm/something_bg.wasm:wasm-function[40718]:0xcc511a
something.wasm._ZN3std9panicking20rust_panic_with_hook17h4fa3daead9c4e0a7E@http://localhost:8080/wasm/something_bg.wasm:wasm-function[17176]:0xa13e65
something.wasm._ZN3std9panicking19begin_panic_handler28_$u7b$$u7b$closure$u7d$$u7d$17hd6c1b2d9392231acE@http://localhost:8080/wasm/something_bg.wasm:wasm-function[22545]:0xb08817
something.wasm._ZN3std3sys9backtrace26__rust_end_short_backtrace17h59d6c402e40a1685E@http://localhost:8080/wasm/something_bg.wasm:wasm-function[53168]:0xd3bc37
something.wasm._RNvCs73fAdSrgOJL_7___rustc17rust_begin_unwind@http://localhost:8080/wasm/something_bg.wasm:wasm-function[46996]:0xd175b8
something.wasm._ZN4core9panicking9panic_fmt17hd3052a3662d50c95E@http://localhost:8080/wasm/something_bg.wasm:wasm-function[46997]:0xd175e5
something.wasm._ZN4core6result13unwrap_failed17hc9f0cd5f2cb06185E@http://localhost:8080/wasm/something_bg.wasm:wasm-function[25948]:0xb7e8da
something.wasm._ZN9subsecond11apply_patch28_$u7b$$u7b$closure$u7d$$u7d$17h4b801d0718bac115E@http://localhost:8080/wasm/something_bg.wasm:wasm-function[3410]:0x11aaeb
something.wasm._ZN20wasm_bindgen_futures4task12singlethread4Task3run17h8856913431d44221E@http://localhost:8080/wasm/something_bg.wasm:wasm-function[7736]:0x703a1e
something.wasm._ZN20wasm_bindgen_futures5queue10QueueState7run_all17h01e38a2ddb723695E@http://localhost:8080/wasm/something_bg.wasm:wasm-function[7248]:0x6bd128
something.wasm._ZN20wasm_bindgen_futures5queue5Queue3new28_$u7b$$u7b$closure$u7d$$u7d$17h2aa5e003e7b66414E@http://localhost:8080/wasm/something_bg.wasm:wasm-function[29464]:0xbe3100
something.wasm._ZN136_$LT$dyn$u20$core..ops..function..FnMut$LT$$LP$A$C$$RP$$GT$$u2b$Output$u20$$u3d$$u20$R$u20$as$u20$wasm_bindgen..closure..WasmClosure$GT$8describe6invoke17hc702e1bfa46a63f9E@http://localhost:8080/wasm/something_bg.wasm:wasm-function[19788]:0xa941c4
something.wasm.closure2168 externref shim@http://localhost:8080/wasm/something_bg.wasm:wasm-function[50434]:0xd32373
__wbg_adapter_132@http://localhost:8080/wasm/something.js:507:10
real@http://localhost:8080/wasm/something.js:247:20
dx --version
dioxus 0.7.0-rc.0 (ed568be)
Hey @BKSalman maybe it's not a very good explanation, but you did this?
https://github.com/spring-rs/spring-rs/pull/163/files#diff-aa411e05653f1d75cd69c4b83aa89f375160af8ab6bc7ca2fdfa10311d557f92R21-R65
I leave a little guide in this PR about how it could work, I'm using SpringRS for this explanation, but it's not different in the case of axum:
This must be in your Cargo.toml:
subsecond = { git = "https://github.com/DioxusLabs/dioxus.git"}
dioxus-devtools = { git = "https://github.com/DioxusLabs/dioxus.git", features = ["serve"]}
The feature server must be activated.
You need to install this version of the cli or upper
cargo install [email protected]
Wrap your main function into this:
dioxus_devtools::serve_subsecond
Exactly like is shown below in the example of Jonathan
Then just execute this: dx serve --hot-patch, you don't need the Dioxus.toml or other things for this basic example