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, theMessage::Output
is awaited and returned immeadeately.{...}_with
: Instead of using the defaultIsSender::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 theMessage::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 intoDynSender<Set![Msg1]>
.mpmc::Sender<ProtocolA>
can be converted intoDynSender<Set![Msg1, ...]>
as long asProtocolA
implementsDynFromInto
andContains<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
- A broadcast channel using
async_broadcast
. - An mpmc channel using
flume
. - A oneshot channel using
oneshot
. - A priority channel using
async_priority_channel
. - Re-export of
type_sets
. - A watch channel using
tokio::sync::watch
.
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 customwith
-value. - A simple wrapper for any type that does not implement
Message
. - 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
- Trait that allows usage of dynamic senders for a protocol
- Automatically implemented when
SendsProtocol
is implemented for a protocol that implementsDynFromInto
. - Extension trait for
DynSends
, providing methods for dynamic dispatch. - Trait that must be implemented by all senders.
- Trait that defines how a message is created and canceled.
- A future that resolves to a
Result
. - Defines when a message
M
can be sent to the sender. - A supertrait of
IsSender
, that defines how a protocol can be sent to the sender.
Derive Macros
- Macro to derive
DynFromInto
andAsSet
for an enum. - Re-export of
derive_more::From
. - Macro to derive
Message
for a type. - Re-export of
derive_more::TryInto
.