| Crates.io | shakespeare |
| lib.rs | shakespeare |
| version | 0.1.0-rc2 |
| created_at | 2024-02-26 23:54:22.844925+00 |
| updated_at | 2025-12-23 22:16:44.546286+00 |
| description | An actor framework |
| homepage | |
| repository | https://github.com/ejmount/shakespeare |
| max_upload_size | |
| id | 1154317 |
| size | 108,022 |
Shakespeare is a pure Rust framework that focuses on ergonomics and extensibility in its implementation of the actor model for creating highly parallel yet safe and robust systems. Actors are run as tasks directly on top of tokio (with no "world" or "system" boundary) and run fully in parallel.
Why do I want an actor system? - The actor model describes a system in terms of interacting "actors," each of which has some sort of associated state, and which interact with other actors by asynchronously sending messages. Importantly, only the actor which owns any given data can directly read or write it, with other parts of the system needing to post a message to the owning actor to request the value change.
This has many of the same architectural benefits as micro-services bring to complex systems, but on a smaller scale and with less overhead. (Namely, that no serialization is needed for actors on the same host and heap-allocated data need not be moved at all) More specifically:
Applications that can benefit from the actor model include network servers of almost all kinds, messaging protocols like IRC and Matrix, distributed services like Kubernetes and Docker Swarm (and anything else using Raft consensus), and even some styles of video games.
Why do I want Shakespeare? - Shakespeare focuses on generality and allowing the programmar maximum control over the results while minimizing runtime overhead. Its most significant features include:
dyn Trait was a primary use case. This enables not only polymorphism within application code, but also substituting mock actors in integration testing and the like.
'static type. This includes methods that return values, which can be returned to the caller - however, the choice of whether or not to wait for the response remains with the caller and can be made on a call-by-call basis.
awaiting the response means the message works like an ordinary function call.Future, and actors can treat Future and Stream objects as incoming messages with no indirection.
async and can await arbitrary Futures, with actor messages simply being a special case.Future to the caller (that originally created the actor) that will become ready when the spawned actor stops.
Future indicate whether the shutdown was graceful (i.e. all references to the actor were dropped) or was a result of a panic within a message handler, so that error handling is more easily separated from the "happy path."Future can simply be dropped with no ill effect. (Note in this case, the panic from the actor will not propagate up)For a full explanation of how all this works, see the crate documentation.
It is worth noting that Shakespeare currently does not offer any built-in support for:
Stream of packets as normal if one is constructed out of a network socket such as with a tokio codec.If neeeded, these capabilities are expected to be relatively straightforward to build in application code - one of the original imagined use cases involved "proxy" actors that implement a Role by forwarding the received messages to an actor on a remote host. If more support for these use cases is important to you, please raise an issue and leave your feedback.
Additionally, Shakespeare currently runs exclusively on tokio but this may change in the future. It also currently uses only unbounded channels, but improving this is planned future work.
The following is a demonstration of some of Shakespeare's capabilities. There is also a chat server example showing these features in a more realistic context.
use shakespeare::{actor, performance, role, ActorHandles, Envelope};
use std::sync::Arc;
use tokio::sync::mpsc;
#[role]
trait BasicRole { // Defining a role is largely a normal trait definition, just with the attribute macro
fn inform(&self, val: usize); // Role methods can take any number of arguments of any reasonable type
fn get(&self) -> usize; // They can similarly return any reasonable type
}
// ("Reasonable" here means Sized, Send and with 'static lifetime)
#[actor]
mod AnActor { // The name of an actor type is defined by the module name here
struct StateA(mpsc::Sender<usize>); // An actor's internal state type can be any struct, enum or union
// with any name, any number of fields/variants and any size.
// Any syntax for these is accepted - this example does not
// name the struct field, but it could do
// (Anonymous tuples are not allowed as state types)
#[performance]
impl BasicRole for StateA {
async fn inform(&mut self, val: usize) {
// Even though the role was defined as taking &self and as non-async, the implementation can be either or both
// The macros will handle the glue
// In role implementations, `self` is the state type, i.e. `StateA` here
self.0.send(val).await;
// this method doesn't do anything interesting, just passes the value back through the channel
// that the actor was given at startup
}
fn get(&self) -> usize {
4 // Chosen by fair dice roll, guaranteed to be random
// ...not that it needs to be random, this is just providing a return value we can retrieve later
}
}
}
#[tokio::main]
async fn main() {
// We will give the actor a channel to send values back to us
// This is to demonstrate that actor methods can be async.
// There's no ordinary need to do this because we can also get hold of return values directly
let (tx, mut rx) = mpsc::channel(1);
let actor_state = StateA(tx); // Nothing unusual is happening here, just initializing a value
// This calls the generated constructor, which starts the actor as a new task
// with the provided value as its state, i.e. with the sender we assigned.
let actor_handles = AnActor::start(actor_state);
// Starting the actor produces two handles
// - the message handle for sending messages/calling functions
// - the join handle to await the actor stopping and indicating successful shutdown or panic
let ActorHandles { message_handle, join_handle: _, .. } = actor_handles;
// In many cases, you may not care about the join_handle and can just ignore it
// the message handle can be upcast to a Role trait object
// Code working with actors via trait objects is more generic
let actor: Arc<dyn BasicRole> = message_handle;
// We send the actor messages by calling methods
// These calls return an "Envelope" to let you decide what to do with the return value
{
let _: Envelope<dyn BasicRole, _> = actor.inform(100);
// can let the Envelope drop to fire and forget, ignoring any return value from the actor
}
// The message was sent to the actor after the closing brace, but since it arrived on a separate
// task, we must wait until that task is scheduled and sends us a value back on the sender we gave it.
// NB: Putting the following line inside the braces above would cause a deadlock
// because we would await the response before the Envelope dropped and sent the message
let chan_response = rx.recv().await;
assert_eq!(chan_response, Some(100));
// We can also directly await the Envelope to get a syncronous, strongly-typed return value
let ret_value = actor.get().await;
assert_eq!(ret_value, Ok(4));
// Its also possible in general the actor shuts down while we were waiting for the message,
// which would give us an Err when we awaited.
}
If you directly browse this crate's source code, you will come across functions marked pub but also #[doc(hidden)]. This is because the macros used in Shakespeare generate output code that then calls into these library functions. These calls technically originate from the user's crate, which means the called functions need to be pub to resolve. However, they are nonetheless not intended for direct client use, so they are suppressed from the documentation and are exempt from SemVer - code calling these functions by bypassing documented interfaces may break even in patch releases.
Licensed under either of Apache Licence, Version 2.0 or MIT licence at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 licence, shall be dual licensed as above, without any additional terms or conditions.