use alloc::boxed::Box;
use core::future::Future;
use core::pin::Pin;
#[cfg(feature = "tokio")]
use alloc::string::ToString;
pub struct JoinManubrium<T> {
inner: Pin<Box<dyn Future<Output = Result<T, JoinError>> + Send>>,
}
impl<T> JoinManubrium<T> {
#[inline]
pub fn new<F>(fut: F) -> Self
where
F: Future<Output = Result<T, JoinError>> + Send + 'static,
{
JoinManubrium {
inner: Box::pin(fut),
}
}
#[inline]
pub fn ready(value: T) -> Self
where
T: Send + 'static,
{
JoinManubrium {
inner: Box::pin(async move { Ok(value) }),
}
}
#[inline]
pub fn error(err: JoinError) -> Self
where
T: Send + 'static,
{
JoinManubrium {
inner: Box::pin(async move { Err(err) }),
}
}
}
impl<T> Future for JoinManubrium<T> {
type Output = Result<T, JoinError>;
fn poll(
mut self: Pin<&mut Self>,
cx: &mut core::task::Context<'_>,
) -> core::task::Poll<Self::Output> {
self.inner.as_mut().poll(cx)
}
}
#[derive(Debug, Clone)]
pub enum JoinError {
Panic(alloc::string::String),
Cancelled,
Other(alloc::string::String),
}
impl core::fmt::Display for JoinError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
JoinError::Panic(msg) => write!(f, "task panicked: {msg}"),
JoinError::Cancelled => write!(f, "task was cancelled"),
JoinError::Other(msg) => write!(f, "join error: {msg}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for JoinError {}
pub trait RuntimeGenerare {
fn spawn<F, T>(future: F) -> JoinManubrium<T>
where
F: Future<Output = T> + Send + 'static,
T: Send + 'static;
fn spawn_blocking<F, T>(f: F) -> JoinManubrium<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static;
fn yield_now() -> impl Future<Output = ()> + Send;
#[cfg(feature = "std")]
fn sleep(duration: core::time::Duration) -> impl Future<Output = ()> + Send;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NullRuntime;
impl RuntimeGenerare for NullRuntime {
fn spawn<F, T>(_future: F) -> JoinManubrium<T>
where
F: Future<Output = T> + Send + 'static,
T: Send + 'static,
{
panic!(
"NullRuntime::spawn: no async runtime is enabled — enable the `tokio` or `smol` feature"
);
}
fn spawn_blocking<F, T>(f: F) -> JoinManubrium<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
let result = f();
JoinManubrium::ready(result)
}
async fn yield_now() {}
#[cfg(feature = "std")]
async fn sleep(duration: core::time::Duration) {
std::thread::sleep(duration);
}
}
#[cfg(feature = "tokio")]
#[derive(Debug, Clone, Copy, Default)]
pub struct TokioRuntime;
#[cfg(feature = "tokio")]
impl RuntimeGenerare for TokioRuntime {
fn spawn<F, T>(future: F) -> JoinManubrium<T>
where
F: Future<Output = T> + Send + 'static,
T: Send + 'static,
{
let handle = tokio::spawn(future);
JoinManubrium::new(async move { handle.await.map_err(|e| JoinError::Panic(e.to_string())) })
}
fn spawn_blocking<F, T>(f: F) -> JoinManubrium<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
let handle = tokio::task::spawn_blocking(f);
JoinManubrium::new(async move { handle.await.map_err(|e| JoinError::Panic(e.to_string())) })
}
fn yield_now() -> impl Future<Output = ()> + Send {
tokio::task::yield_now()
}
#[cfg(feature = "std")]
fn sleep(duration: core::time::Duration) -> impl Future<Output = ()> + Send {
tokio::time::sleep(duration)
}
}
#[cfg(feature = "smol")]
fn smol_panic_message(payload: &(dyn core::any::Any + Send)) -> alloc::string::String {
if let Some(s) = payload.downcast_ref::<&str>() {
alloc::string::String::from(*s)
} else if let Some(s) = payload.downcast_ref::<alloc::string::String>() {
s.clone()
} else {
alloc::string::String::from("smol task panicked")
}
}
#[cfg(feature = "smol")]
#[derive(Debug, Clone, Copy, Default)]
pub struct SmolRuntime;
#[cfg(feature = "smol")]
impl RuntimeGenerare for SmolRuntime {
fn spawn<F, T>(future: F) -> JoinManubrium<T>
where
F: Future<Output = T> + Send + 'static,
T: Send + 'static,
{
struct DetachOnDrop<T: Send + 'static>(Option<smol::Task<T>>);
impl<T: Send + 'static> Drop for DetachOnDrop<T> {
fn drop(&mut self) {
if let Some(t) = self.0.take() {
t.detach();
}
}
}
let mut guard = DetachOnDrop(Some(smol::spawn(future)));
JoinManubrium::new(core::future::poll_fn(move |cx| {
let task = guard
.0
.as_mut()
.expect("task present until this future resolves");
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| Pin::new(task).poll(cx)))
{
Ok(core::task::Poll::Ready(v)) => {
guard.0 = None; core::task::Poll::Ready(Ok(v))
}
Ok(core::task::Poll::Pending) => core::task::Poll::Pending,
Err(payload) => {
guard.0 = None; core::task::Poll::Ready(Err(JoinError::Panic(smol_panic_message(&*payload))))
}
}
}))
}
fn spawn_blocking<F, T>(f: F) -> JoinManubrium<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
let handle = smol::unblock(f);
JoinManubrium::new(async move { Ok(handle.await) })
}
fn yield_now() -> impl Future<Output = ()> + Send {
smol::future::yield_now()
}
#[cfg(feature = "std")]
async fn sleep(duration: core::time::Duration) {
smol::Timer::after(duration).await;
}
}
#[cfg(feature = "tokio")]
pub type CurrentRuntime = TokioRuntime;
#[cfg(all(feature = "smol", not(feature = "tokio")))]
pub type CurrentRuntime = SmolRuntime;
#[cfg(not(any(feature = "tokio", feature = "smol")))]
pub type CurrentRuntime = NullRuntime;
pub trait FutureRuntimeExt: Future + Sized {
#[inline]
fn spawn_on<R: RuntimeGenerare>(self) -> JoinManubrium<Self::Output>
where
Self: Send + 'static,
Self::Output: Send + 'static,
{
R::spawn(self)
}
#[inline]
fn spawn_current(self) -> JoinManubrium<Self::Output>
where
Self: Send + 'static,
Self::Output: Send + 'static,
{
CurrentRuntime::spawn(self)
}
}
impl<F: Future> FutureRuntimeExt for F {}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
#[test]
fn test_null_runtime_spawn_blocking() {
let _handle = NullRuntime::spawn_blocking(|| 42);
}
#[test]
fn test_join_error_display() {
let panic_err = JoinError::Panic("test panic".to_string());
assert!(panic_err.to_string().contains("panic"));
let cancelled_err = JoinError::Cancelled;
assert!(cancelled_err.to_string().contains("cancel"));
let other_err = JoinError::Other("other".to_string());
assert!(other_err.to_string().contains("other"));
}
#[test]
fn test_join_manubrium_ready() {
let _handle = JoinManubrium::ready(42);
}
#[test]
#[should_panic(expected = "no async runtime is enabled")]
fn null_runtime_spawn_panics_instead_of_discarding() {
let _handle = NullRuntime::spawn(async { 42 });
}
#[cfg(feature = "smol")]
#[test]
fn smol_spawn_detaches_on_drop_instead_of_cancelling() {
use alloc::sync::Arc;
use core::sync::atomic::{AtomicUsize, Ordering};
let counter = Arc::new(AtomicUsize::new(0));
let counter_task = counter.clone();
let handle = SmolRuntime::spawn(async move {
smol::Timer::after(core::time::Duration::from_millis(20)).await;
counter_task.fetch_add(1, Ordering::SeqCst);
});
drop(handle);
smol::block_on(async {
smol::Timer::after(core::time::Duration::from_millis(200)).await;
});
assert_eq!(
counter.load(Ordering::SeqCst),
1,
"detached task must keep running after the JoinManubrium is dropped"
);
}
#[cfg(feature = "smol")]
#[test]
fn smol_spawn_panic_becomes_join_error_panic() {
let handle = SmolRuntime::spawn(async { panic!("boom") });
let result = smol::block_on(handle);
assert!(
matches!(result, Err(JoinError::Panic(_))),
"a panicking smol task must surface as JoinError::Panic, not resume the unwind \
into the awaiting context"
);
}
}