Attribute Macro crossmist::func

source ·
#[func]
Expand description

Enable a function to be used as an entrypoint of a child process, and turn it into an Object.

This macro applies to fn functions, including generic ones. It adds various methods for spawning a child process from this function.

For a function declared as

#[func]
fn example(arg1: Type1, ...) -> Output;

…the methods are:

pub fn spawn(&self, arg1: Type1, ...) -> std::io::Result<crossmist::Child<Output>>;
pub fn run(&self, arg1: Type1, ...) -> std::io::Result<Output>;

spawn runs the function in a subprocess and returns a Child instance which can be used to monitor the process and retrieve its return value when it finishes via Child::join. run combines the two operations into one, which may be useful if a new process is needed for a reason other than parallel execution.

For example:

use crossmist::{func, main};

#[func]
fn example(a: i32, b: i32) -> i32 {
    a + b
}

#[main]
fn main() {
    assert_eq!(example.spawn(5, 7).unwrap().join().unwrap(), 12);
    assert_eq!(example.run(5, 7).unwrap(), 12);
}

The function can also be invoked in the same process via the FnOnceObject, FnMutObject, and FnObject traits, which are similar to std::ops::FnOnce, std::ops::FnMut, and std::ops::Fn, respectively:

use crossmist::{FnObject, func, main};

#[func]
fn example(a: i32, b: i32) -> i32 {
    a + b
}

#[main]
fn main() {
    assert_eq!(example.call_object((5, 7)), 12);
}

If the nightly feature is enabled, the function can also directly be called, providing the same behavior as if #[func] was not used:

use crossmist::{FnObject, func, main};

#[func]
fn example(a: i32, b: i32) -> i32 {
    a + b
}

#[main]
fn main() {
    assert_eq!(example(5, 7), 12);
}

spawn and run return an error if spawning the child process failed (e.g. the process limit is exceeded or the system lacks memory). run also returns an error if the process panics, calls std::process::exit or alike instead of returning a value, or is terminated (as does Child::join).

The child process relays its return value to the parent via an implicit channel. Therefore, it is important to keep the Child instance around until the child process terminates and never drop it before joining, or the child process will panic.

Do:

#[crossmist::main]
fn main() {
    let child = long_running_task.spawn().expect("Failed to spawn child");
    // ...
    let need_child_result = false;  // assume this is computed from some external data
    // ...
    let return_value = child.join().expect("Child died");
    if need_child_result {
        eprintln!("{return_value}");
    }
}

#[crossmist::func]
fn long_running_task() -> u32 {
    std::thread::sleep(std::time::Duration::from_secs(1));
    123
}

Don’t:

#[crossmist::main]
fn main() {
    let child = long_running_task.spawn().expect("Failed to spawn child");
    // ...
    let need_child_result = false;  // assume this is computed from some external data
    // ...
    if need_child_result {
        eprintln!("{}", child.join().expect("Child died"));
    }
}

#[crossmist::func]
fn long_running_task() -> u32 {
    std::thread::sleep(std::time::Duration::from_secs(1));
    123
}

The void return type (()) is an exception to this rule: such return values are not delivered, and thus Child may be safely dropped at any point, and the child process is allowed to use std::process::exit instead of explicitly returning ().

Do:

#[crossmist::main]
fn main() {
    long_running_task.spawn().expect("Failed to spawn child");
}

#[crossmist::func]
fn long_running_task() {
    std::thread::sleep(std::time::Duration::from_secs(1));
}

Do:

#[crossmist::main]
fn main() {
    let child = long_running_task.spawn().expect("Failed to spawn child");
    // ...
    child.join().expect("Child died");
}

#[crossmist::func]
fn long_running_task() {
    std::thread::sleep(std::time::Duration::from_secs(1));
    std::process::exit(0);
}

§Asynchronous case

If the tokio feature is enabled, the following methods are also made available:

pub async fn spawn_tokio(&self, arg1: Type1, ...) ->
    std::io::Result<crossmist::tokio::Child<Output>>;
pub async fn run_tokio(&self, arg1: Type1, ...) -> std::io::Result<Output>;

If smol is enabled, the functions spawn_smol and run_smol with matching signatures are generated.

Additionally, the function may be async. In this case, you have to indicate which runtime to use as follows:

#[crossmist::func(tokio)]
async fn example_tokio() {}

#[crossmist::func(smol)]
async fn example_smol() {}

You may pass operands to forward to tokio::main like this:

#[crossmist::func(tokio(flavor = "current_thread"))]
async fn example() {}

Notice that the use of spawn vs spawn_tokio/spawn_smol is orthogonal to whether the function is async: you can start a synchronous function in a child process asynchronously, or vice versa:

use crossmist::{func, main};

#[func]
fn example(a: i32, b: i32) -> i32 {
    a + b
}

#[main]
#[tokio::main(flavor = "current_thread")]
async fn main() {
    assert_eq!(example.run_tokio(5, 7).await.unwrap(), 12);
}
use crossmist::{func, main};

#[func(tokio(flavor = "current_thread"))]
async fn example(a: i32, b: i32) -> i32 {
    a + b
}

#[main]
fn main() {
    assert_eq!(example.run(5, 7).unwrap(), 12);
}