Crate crossmist

source ·
Expand description

Efficient and seamless cross-process communication, providing semantics similar to std::thread::spawn and alike, both synchronously and asynchronously (via tokio or smol).

This crate allows you to easily perform computations in another process without creating a separate executable or parsing command line arguments manually. For example, the simplest example, computing a sum of several numbers in a one-shot subprocess, looks like this:

#[crossmist::main]
fn main() {
    println!("5 + 7 = {}", add.run(vec![5, 7]).unwrap());
}

#[crossmist::func]
fn add(nums: Vec<i32>) -> i32 {
    nums.into_iter().sum()
}

This crate also supports long-lived tasks with constant cross-process communication:

#[crossmist::main]
fn main() {
    let (mut ours, theirs) = crossmist::duplex().unwrap();
    add.spawn(theirs).expect("Failed to spawn child");
    for i in 1..=5 {
        for j in 1..=5 {
            println!("{i} + {j} = {}", ours.request(&vec![i, j]).unwrap());
        }
    }
}

#[crossmist::func]
fn add(mut chan: crossmist::Duplex<i32, Vec<i32>>) {
    while let Some(nums) = chan.recv().unwrap() {
        chan.send(&nums.into_iter().sum());
    }
}

§Passing objects

Almost arbitrary objects can be passed between processes and across channels, including file handles, sockets, and other channels.

For numeric types, strings, vectors, hashmaps, other common containers, and files/sockets, the Object trait is implemented automatically. For user-defined structures and enums, use #[derive(Object)]. You may use generics, but make sure to add : Object constraint to stored types:

use crossmist::Object;

#[derive(Object)]
struct MyPair<T: Object, U: Object> {
    first: T,
    second: U,
}

Occasionally, e.g. for custom hash tables or externally defined types, you might have to implement Object manually. Check out the documentation for Object for more information.

§Channels

As the second example demonstrates, cross-process communication may be achieved not only via arguments and return values, but via long-lived channels. Channels may be unidirectional (one process has a Sender instance and another process has a connected Receiver instance) or bidirectional (both processes have Duplex instances). Channels are typed: you don’t just send byte streams à la TCP, you send objects of a well-defined type implementing the Object trait, making channels type-safe.

Channels implement Object. This means that not only can you pass channels to subprocesses as arguments (they wouldn’t be useful otherwise), but you can pass channels across other channels, just like you can pass files across channels.

Channels are trusted. This means that if one side reads from Receiver and another side writes garbage to the corresponding file descriptor instead of using Sender, the receiver side may crash and burn, potentially leading to arbitrary code execution.

The communication protocol is not fixed and may not only change in minor versions, but be architecture- or build-dependent. This is done to both ensure performance optimizations can be implemented and to let us fix bugs quickly when they arise. As channels may only be used between two processes started from the same executable file, this does not violate semver.

§Aborting computations

If, at any point, you determine that you are no longer interested in the output of a process, you can kill it:

#[crossmist::main]
fn main() {
    let mut child = long_computation.spawn().expect("Failed to spawn child");
    let kill_handle = child.get_kill_handle();
    std::thread::spawn(move || {
        // Wait, I don't need this, actually!
        std::thread::sleep(std::time::Duration::from_millis(500));
        kill_handle.kill();
    });
    // This will fail in 0.5 seconds:
    child.join().unwrap_err();
}

#[crossmist::func]
fn long_computation() {
    loop {}
}

§Features

This crate provides the following features:

  • tokio: enable Tokio async runtime support.
  • smol: enable smol async runtime support.
  • nightly: make use of nightly features. This enables crossmist to be more performant and provide better API, but requires a nightly compiler to be used.

Re-exports§

Modules§

  • Generic asynchronous implementation.
  • Uni- and bidirectional channels between processes.
  • A wrapper for objects that require global state to be configured before deserialization.
  • Utilities for passing function callbacks between processes.
  • Cross-platform alternative to file descriptors.
  • Serialization and deserialization.
  • smolsmol
    Asynchronous implementation using smol runtime.
  • Serializing references to constant objects.
  • tokiotokio
    Asynchronous implementation using tokio runtime.

Macros§

  • A short-cut for turning a (possible capturing) closure into an object function, just like as if #[func] was used.
  • Create a StaticRef safely.

Structs§

  • A handle that allows to kill the process.

Traits§

Functions§

  • Initialize the crossmist runtime.

Attribute Macros§

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

Derive Macros§

  • Make a structure or a enum serializable.