Crate meslin

source ·
Expand description

Meslin

Meslin is a Rust library offering ergonomic wrappers for channels like mpmc and broadcast. It’s designed to ease the creation of actor systems by adding user-friendly features, without being tied to any specific runtime. This makes it compatible with various runtimes such as tokio, smol, or async-std.It intentionally steers clear of incorporating supervisory functions or other complex features, focusing instead on simplicity and non-interference.

Meslin is designed with a zero-cost abstraction principle in mind, ensuring that its ease of use and flexibility don’t compromise performance. When not using any dynamic features of the library, Meslin does not add any additional runtime overhead compared to hand-written equivalents.

Concepts

Messages

All messages that are sent through a channel must implement the Message trait. The trait defines two associated types: Message::Input and Message::Output. When sending a message to an actor, you only need to provide the input type and if the message is sent succesfully, the output type is returned.

Message is implemented for a lot of common types, like i32, String, Vec<T>, etc. Furthermore, it is implemented for Msg<M> and Request<A, B>. The first is a simple wrapper that allows sending any type that does not implement Message. The second is a message that requires a response, i.e. the output is actually a oneshot::Receiver.

The Message derive-macro can be used to derive Message for custom types.

Protocols

Protocols define the messages that can be received by an actor. For every message M that can be received, the protocol must implement From<M> and TryInto<M>. These traits can be derived using the From and TryInto derive-macros.

Optionally, the protocol can implement DynFromInto and AsSet using the derive-macro DynFromInto. This allows for conversion of senders into dynamic senders. See DynSender for more information.

Senders

Senders are responsible for defining the delivery mechanism of a protocol. They implement IsSender and can be used to send messages using Sends<M>. Examples of some default senders are mpmc::Sender, priority::Sender and the DynSender.

Most senders have their associated type IsSender::With set to (), meaning that they don’t require any additional data to send a message. However, some senders, like priority::Sender do require additional data.

Send methods

The SendsExt and DynSendsExt traits provide a bunch of methods for sending messages. The following are the modifier keywords and their meaning:

  • send: The base method, that asynchronously sends a message and waits for space to become available.
  • request: After sending the message, the Message::Output is awaited and returned immeadeately.
  • {...}_with: Instead of using the default IsSender::With value, a custom value is given.
  • try_{...}: Sends a message, returning an error if space is not available.
  • {...}_blocking: Sends a message, blocking the current thread until space becomes available.
  • {...}_msg: Instead of giving the Message::Input, the message itself is given.
  • dyn_{...}: Attempts to send a message, when it can not be statically verified that the actor will accept the message.

Dynamic senders

A unique feature of Meslin is the transformation of senders into dynamic senders, converting any sender into a dyn DynSends<W>. This allows for storage of different sender types in the same data structure, like Vec<T>.

DynSender provides an abstraction over a Box<dyn DynSends>, allowing for type-checked dynamic dispatch and conversions. For example, if you have an mpmc::Sender<ProtocolA> and a broadcast::Sender<ProtocolB>, both accepting messages Msg1 and Msg2, they can both be converted into DynSender<Set![Msg1, Msg2]>. This dynamic sender then implements Sends<Msg1> + Sends<Msg2>.

The Set macro can be used to define the accepted messages of a dynamic sender. Some examples of dynamic sender conversions:

  • Set![Msg1, Msg2] == dyn Two<Msg1, Msg2>.
  • DynSender<Set![Msg1, Msg2]> can be converted into DynSender<Set![Msg1]>.
  • mpmc::Sender<ProtocolA> can be converted into DynSender<Set![Msg1, ...]> as long as ProtocolA implements DynFromInto and Contains<Msg1> + Contains<...> + ....

Cargo features

The following features are available:

  • Default features: ["derive", "request", "mpmc", "broadcast", "priority"]
  • Additional features: `[“watch”]“”

Basic example

use meslin::{mpmc, From, Message, Request, SendsExt, TryInto};

// Create a simple, custom message type
#[derive(Debug, From, Message)]
#[from(forward)]
struct MyMessage(String);

// Create the protocol used by the actor
// It defines the messages that can be sent
#[derive(Debug, From, TryInto)]
enum MyProtocol {
    Number(i32),
    Message(MyMessage),
    Request(Request<i32, String>),
}

#[tokio::main]
async fn main() {
    // Create the channel and spawn a task that receives messages
    let (sender, receiver) = mpmc::unbounded::<MyProtocol>();
    tokio::task::spawn(receive_messages(receiver));

    // Send a number
    sender.send::<i32>(42).await.unwrap();

    // Send a message
    sender.send::<MyMessage>("Hello").await.unwrap();

    // Send a request and then wait for the reply (oneshot channel)
    let rx = sender.send::<Request<i32, String>>(42).await.unwrap();
    let reply = rx.await.unwrap();
    assert_eq!(reply, "The number is 42");

    // Send a request and receive the reply immeadiately
    let reply = sender.request::<Request<i32, String>>(42).await.unwrap();
    assert_eq!(reply, "The number is 42");
}

// This is completely standard: `mpmc::Receiver` == `flume::Receiver`
async fn receive_messages(receiver: mpmc::Receiver<MyProtocol>) {
    while let Ok(msg) = receiver.recv_async().await {
        match msg {
            MyProtocol::Number(msg) => {
                println!("Received number: {msg:?}");
            }
            MyProtocol::Message(msg) => {
                println!("Received message: {msg:?}");
            }
            MyProtocol::Request(Request { msg, tx }) => {
                println!("Received request: {msg:?}");
                tx.send(format!("The number is {}", msg)).ok();
            }
        }
    }
}

Advanced example

use meslin::{
    mpmc, priority, DynFromInto, DynSender, DynSendsExt, From, MappedWithSender, SendsExt, TryInto,
    WithValueSender,
};

#[derive(Debug, From, TryInto, DynFromInto)]
enum P1 {
    A(i32),
    B(i64),
    C(i128),
}

#[derive(Debug, From, TryInto, DynFromInto)]
enum P2 {
    A(i16),
    B(i32),
    C(i64),
}

#[tokio::main]
async fn main() {
    // Create two different senders, sending different protocols
    let (sender1, _receiver) = mpmc::unbounded::<P1>(); // Sends `P1` with `()`
    let (sender2, _receiver) = priority::unbounded::<P2, u32>(); // Sends `P2` with `u32` as priority

    // Sending messages to the senders:
    sender1.send::<i32>(8).await.unwrap(); // Normal
    sender2.send::<i32>(8).await.unwrap(); // Uses `u32::default()` as priority
    sender2.send_with::<i32>(8, 15).await.unwrap(); // Uses `15` as priority

    // Create a vector of dynamic senders: (Checked at compile time)
    let senders: Vec<DynSender![i32, i64]> = vec![
        // For sender1, use `into_dyn` to transform it into a DynSender
        sender1.into_dyn(),
        // For sender2, use `with` / `map_with` and then `into_dyn` to transform it into a DynSender
        // This sender will always send `15` as the priority
        sender2.clone().with(15).into_dyn(),
        sender2.map_with(|_| 15, |_| ()).into_dyn(),
    ];

    // Send a `i32` or `i64` to the senders
    senders[0].send::<i32>(8).await.unwrap();
    senders[1].send::<i64>(8).await.unwrap();
    senders[2].send::<i32>(8).await.unwrap();

    // Downcast the senders back to their original types
    let _sender1 = senders[0].downcast_ref::<mpmc::Sender<P1>>().unwrap();
    let _sender2 = senders[1]
        .downcast_ref::<WithValueSender<priority::Sender<P2, u32>>>()
        .unwrap();
    let _sender3 = senders[2]
        .downcast_ref::<MappedWithSender<priority::Sender<P2, u32>, ()>>()
        .unwrap();
}

Modules

Macros

Structs

  • A boxed message with a with value, used for dynamic dispatch.
  • A wrapper around a Box<dyn DynSends> that allows for type-checked conversions.
  • A wrapper around a sender, which provides a mapping between the with-value of the sender and a custom with-value.
  • A simple wrapper for any type that does not implement Message.
  • A Message with input A, returning a response B.
  • Error that is returned when a channel is closed.
  • Marker struct to act as a set of elements.
  • A wrapper around a sender, which provides a default with-value.

Enums

  • Error that is returned when a channel is closed, or the message was not accepted.
  • Error that is returned when a channel is closed, full, or the message was not accepted.
  • Error that is returned when a channel is full, or the request did nor receive a reply
  • Error that is returned when a channel is closed or full.

Traits

Derive Macros