Skip to content

Commit

Permalink
Drop dependency on async_trait, let AsyncTasks take self by value, tr…
Browse files Browse the repository at this point in the history
…eat async closures as AsyncTasks (#161)
  • Loading branch information
Taaitaaiger authored Jan 25, 2025
1 parent dad0b7d commit 236d846
Show file tree
Hide file tree
Showing 15 changed files with 244 additions and 428 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@

- The `julia-1-6`, `julia-1-7`, `julia-1-8`, and `julia-1-9` features have been removed.

- The async runtime supports using async closures when the `async-closure` feature is enabled. This requires using at least Rust 1.85. The `AsyncTask` trait has been implemented for all async closures `AsyncFnMut(AsyncGcFrame) -> T`, `AsyncHandle::closure` additionally accepts any `AsyncFnOnce(AsyncGcFrame) -> T`. Async closures can also be used in combination with nested async scopes by calling `AsyncGcFrame::async_scope_closure`, which doesn't suffer from the same lifetime issues as `AsyncGcFrame::async_scope`. The methods `AsyncGcFrame::async_scope` and `AsyncGcFrame::relaxed_async_scope` and the feature gate itself will be removed in the future when the MSRV is increased.
- The `Executor`, `AsyncTask`, and `PersistentTask` traits no longer use `async_trait`. To migrate existing `PersistentTask`s, remove the `async_trait`-annotation. `AsyncTask`s now take `self` by value, so to migrate them remove the annotation and change `&mut self` in `run` to `self`.

- The async runtime supports using async closures when the `async-closure` feature is enabled, this requires using at least Rust 1.85. The `AsyncTask` trait has been implemented for all async closures `AsyncFnOnce(AsyncGcFrame) -> T`, so async closures can easily be called with `AsyncHandle::task`. Async closures can also be used in combination with nested async scopes by calling `AsyncGcFrame::async_scope_closure`, which doesn't suffer from the same lifetime issues as `AsyncGcFrame::async_scope`. The methods `AsyncGcFrame::async_scope` and `AsyncGcFrame::relaxed_async_scope` and the feature gate itself will be removed in the future when the MSRV is increased.

#### v0.21

Expand Down
1 change: 0 additions & 1 deletion benches/benches/mt_rt_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ impl AsyncExecutor for TokioExecutor {

struct MyTask;

#[async_trait(?Send)]
impl AsyncTask for MyTask {
type Output = ();

Expand Down
4 changes: 1 addition & 3 deletions examples/async_tasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@ struct MyTask {
iters: isize,
}

#[async_trait(?Send)]
impl AsyncTask for MyTask {
// Different tasks can return different results. If successful, this task returns an `f64`.
type Output = JlrsResult<f64>;

// This is the async variation of the closure you provide `Julia::scope` when using the sync
// runtime.
async fn run<'frame>(&mut self, mut frame: AsyncGcFrame<'frame>) -> Self::Output {
async fn run<'frame>(self, mut frame: AsyncGcFrame<'frame>) -> Self::Output {
// Convert the two arguments to values Julia can work with.
let dims = Value::new(&mut frame, self.dims);
let iters = Value::new(&mut frame, self.iters);
Expand All @@ -41,7 +40,6 @@ impl AsyncTask for MyTask {
}
}

#[async_trait(?Send)]
impl Register for MyTask {
// Include the custom code MyTask needs.
async fn register<'frame>(mut frame: AsyncGcFrame<'frame>) -> JlrsResult<()> {
Expand Down
8 changes: 2 additions & 6 deletions examples/nested_async_scopes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,14 @@ struct MyTask {
iters: isize,
}

// `MyTask` is a task we want to be executed, so we need to implement `AsyncTask`. This requires
// `async_trait` because traits with async methods are not yet available in Rust. Because the
// task itself is executed on a single thread, it is marked with `?Send`.
#[async_trait(?Send)]
// `MyTask` is a task we want to be executed, so we need to implement `AsyncTask`.
impl AsyncTask for MyTask {
// Different tasks can return different results. If successful, this task returns an `f64`.
type Output = JlrsResult<f64>;

// This is the async variation of the closure you provide `Julia::scope` when using the sync
// runtime.
async fn run<'base>(&mut self, mut frame: AsyncGcFrame<'base>) -> Self::Output {
async fn run<'base>(self, mut frame: AsyncGcFrame<'base>) -> Self::Output {
// Nesting async frames works like nesting on ordinary scope. The main difference is that
// the closure must return an async block.
let output = frame.output();
Expand Down Expand Up @@ -53,7 +50,6 @@ impl AsyncTask for MyTask {
}
}

#[async_trait(?Send)]
impl Register for MyTask {
// Include the custom code MyTask needs.
async fn register<'base>(mut frame: AsyncGcFrame<'base>) -> JlrsResult<()> {
Expand Down
2 changes: 0 additions & 2 deletions examples/persistent_tasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ struct AccumulatorTask {
init_value: f64,
}

#[async_trait(?Send)]
impl Register for AccumulatorTask {
// Register this task. This method can take care of custom initialization work, in this case
// creating the mutable MutFloat64 type in the Main module.
Expand All @@ -19,7 +18,6 @@ impl Register for AccumulatorTask {
}
}

#[async_trait(?Send)]
impl PersistentTask for AccumulatorTask {
// The capacity of the channel used to communicate with this task
const CHANNEL_CAPACITY: usize = 2;
Expand Down
3 changes: 1 addition & 2 deletions jlrs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ multi-rt = ["jl-sys/fast-tls"]
# Utilities

# Enable task and channel traits used by the async runtime
async = ["async-trait", "async-channel"]
async = ["async-channel"]
# Enable `ccall` module for use from `ccall`ed Rust functions
ccall = ["jlrs-macros/ccall"]
# Enable using `f16` as a layout for `Float16` data
Expand Down Expand Up @@ -104,7 +104,6 @@ lock_api = "0.4"
fnv = "1"
atomic = "0.6"

async-trait = { version = "0.1", optional = true }
async-channel = { version = "2", optional = true }
half = { version = "2.4", optional = true }
ndarray = { version = "0.16", optional = true }
Expand Down
48 changes: 19 additions & 29 deletions jlrs/src/async_util/task.rs
Original file line number Diff line number Diff line change
@@ -1,43 +1,33 @@
use std::time::Duration;

use async_trait::async_trait;
use std::{future::Future, time::Duration};

use crate::{
call::Call,
inline_static_ref,
prelude::{AsyncGcFrame, JlrsResult, Target, Value},
};

#[async_trait(?Send)]
pub trait Register: 'static + Send {
async fn register<'frame>(frame: AsyncGcFrame<'frame>) -> JlrsResult<()>;
fn register<'frame>(frame: AsyncGcFrame<'frame>) -> impl Future<Output = JlrsResult<()>>;
}

/// Async task
///
/// Any type that implements this trait can be sent to the async runtime where its `run` method
/// will be called. Note that [`async_trait`] is used, all implementers must be marked with
/// `#[async_trait(?Send)]`.
///
/// When the `async-closure` feature has been enabled, this trait is implemented for all
/// `A: AsyncFnMut(AsyncGcFrame) -> T`.
///
/// [`async_trait`]: async_trait::async_trait
#[async_trait(?Send)]
/// will be called. If the `async-closure` feature has been enabled, this trait is implemented for
/// all `AsyncFnOnce(AsyncGcFrame) -> T`.
pub trait AsyncTask: 'static + Send {
/// The return type of `run`.
type Output: 'static + Send;

/// Run this task.
async fn run<'frame>(&mut self, frame: AsyncGcFrame<'frame>) -> Self::Output;
fn run<'frame>(self, frame: AsyncGcFrame<'frame>) -> impl Future<Output = Self::Output>;
}

/// Persistent task
///
/// Unlike an [`AsyncTask`], which is executed once, a persistent task is initialized and then
/// provides a handle to call `run`. A persistent task has a state, which is returned by `init`,
/// which is provided every time `run` is called in addition to the input data.
#[async_trait(?Send)]
pub trait PersistentTask: 'static + Send {
/// The type of the result which is returned if `init` completes successfully.
///
Expand All @@ -58,32 +48,33 @@ pub trait PersistentTask: 'static + Send {
/// You can interact with Julia inside this method, the frame is not dropped until the task
/// itself is dropped. This means that `State` can contain arbitrary Julia data rooted in this
/// frame. This data is provided to every call to `run`.
async fn init<'frame>(
fn init<'task>(
&mut self,
frame: AsyncGcFrame<'frame>,
) -> JlrsResult<Self::State<'frame>>;
frame: AsyncGcFrame<'task>,
) -> impl Future<Output = JlrsResult<Self::State<'task>>>;

/// Run the task.
///
/// This method takes an `AsyncGcFrame`, which lets you interact with Julia.
/// It's also provided with a mutable reference to its `state` and the `input` provided by the
/// caller. While the state is mutable, it's not possible to allocate a new Julia value in
/// `run` and assign it to the state because the frame doesn't live long enough.
async fn run<'frame, 'state: 'frame>(
fn run<'frame, 'task: 'frame>(
&mut self,
frame: AsyncGcFrame<'frame>,
state: &mut Self::State<'state>,
state: &mut Self::State<'task>,
input: Self::Input,
) -> Self::Output;
) -> impl Future<Output = Self::Output>;

/// Method that is called when all handles to the task have been dropped.
///
/// This method is called with the same frame as `init`.
async fn exit<'frame>(
fn exit<'task>(
&mut self,
_frame: AsyncGcFrame<'frame>,
_state: &mut Self::State<'frame>,
) {
_frame: AsyncGcFrame<'task>,
_state: &mut Self::State<'task>,
) -> impl Future<Output = ()> {
async {}
}
}

Expand Down Expand Up @@ -111,15 +102,14 @@ pub fn sleep<'scope, 'data, Tgt: Target<'scope>>(target: &Tgt, duration: Duratio
}

#[cfg(feature = "async-closure")]
#[async_trait(?Send)]
impl<A, U> AsyncTask for A
where
for<'scope> A: AsyncFnMut(AsyncGcFrame<'scope>) -> U + Send + 'static,
for<'scope> A: AsyncFnOnce(AsyncGcFrame<'scope>) -> U + Send + 'static,
U: Send + 'static,
{
type Output = U;

async fn run<'frame>(&mut self, frame: AsyncGcFrame<'frame>) -> Self::Output {
self(frame).await
fn run<'frame>(self, frame: AsyncGcFrame<'frame>) -> impl Future<Output = Self::Output> {
self(frame)
}
}
Loading

0 comments on commit 236d846

Please sign in to comment.