Skeletal actor framework. Somewhat unweildy, needs a message box, a better runtime, and different channels. However, hello world example works
This commit is contained in:
Zachery Aaron Shores-Chmielewski 2025-11-24 19:39:17 -05:00
commit 120ba0ec71
7 changed files with 260 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

104
Cargo.lock generated Normal file
View file

@ -0,0 +1,104 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bytes"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "swactor"
version = "0.1.0"
dependencies = [
"tokio",
"tokio-util",
]
[[package]]
name = "syn"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tokio"
version = "1.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
dependencies = [
"pin-project-lite",
"tokio-macros",
]
[[package]]
name = "tokio-macros"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio-util"
version = "0.7.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"

11
Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "swactor"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
tokio = { version = "1.48.0", features = ["rt", "macros"] }
tokio-util = "0.7.17"

2
README.md Normal file
View file

@ -0,0 +1,2 @@
# about
Small wasm-compatible actor library

46
examples/hello.rs Normal file
View file

@ -0,0 +1,46 @@
use swactor::Actor;
use tokio::sync::oneshot;
pub enum GreeterMessage {
Name(String),
}
pub enum GreeterResponse {
Hello(String),
}
pub struct Greeter;
impl Actor for Greeter {
type Message = GreeterMessage;
type Response = GreeterResponse;
fn handle_message(&self, msg: Self::Message, tx: oneshot::Sender<Self::Response>) {
let rep = match msg {
GreeterMessage::Name(name) => GreeterResponse::Hello(format!("Hello, {name}!")),
};
if let Err(_) = tx.send(rep) {
// Greeter is not responsible for a dropped Receiver
}
}
}
fn main() {
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.expect("failed to build runtime");
let greeter = Greeter::spawn(Greeter, &rt);
let response = rt
.block_on(async move {
greeter
.send(GreeterMessage::Name("world".to_string()))
.await
})
.expect("failed to get respose");
match response {
GreeterResponse::Hello(hello) => println!("{hello}"),
}
}

5
src/error.rs Normal file
View file

@ -0,0 +1,5 @@
pub type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
pub type Result<T> = std::result::Result<T, Error>;
pub fn convert_err<E: std::fmt::Debug>(e: E) -> Error {
format!("{e:?}").into()
}

91
src/lib.rs Normal file
View file

@ -0,0 +1,91 @@
pub mod error;
/// Public export as the oneshot channel is in the `Actor` trait signature
pub use tokio::sync::oneshot;
use tokio::{
sync::{mpsc},
task::JoinHandle,
};
use tokio_util::sync::CancellationToken;
use crate::error::{Result, convert_err};
const DEFAULT_CHANNEL_SIZE: usize = 100;
/// Combined 'JoinHandle' to await the actor process and 'Sender' for communication
pub struct Handle<A>
where
A: Actor,
{
cancel_token: CancellationToken,
tx: mpsc::Sender<(A::Message, oneshot::Sender<A::Response>)>,
_handle: JoinHandle<Result<()>>,
// to prevent accidental swaps, strongly type the handle
_type: std::marker::PhantomData<A>,
}
impl<A: Actor> Handle<A> {
/// Send a message to the spawned `Actor` task and get a response corresponding to the `Actor::Response` type
pub async fn send(&self, msg: A::Message) -> Result<A::Response> {
let (tx, rx) = oneshot::channel::<A::Response>();
self.tx.send((msg, tx)).await.map_err(convert_err)?;
rx.await.map_err(convert_err)
}
}
impl<A: Actor> Drop for Handle<A> {
fn drop(&mut self) {
self.cancel_token.cancel();
}
}
/// Primary trait defining an `Actor` capable of receiving, processing, and transmitting messages
pub trait Actor: Send + Sized + Unpin + 'static {
/// The type for messages received by this `Actor`
type Message: Send;
/// The type for responses given by this actor when called from `Handle::send(..)`
type Response: Send;
/// Inner method that defines actor behavior
fn handle_message(&self, msg: Self::Message, tx: oneshot::Sender<Self::Response>);
/// Spawns the `Actor` utilizing the given runtime context
/// Only `tokio` runtime is accepted for now
fn spawn(self, ctx: &tokio::runtime::Runtime) -> Handle<Self> {
let cancel_token = CancellationToken::new();
let cancel = cancel_token.clone();
let (tx, mut rx) =
mpsc::channel::<(Self::Message, oneshot::Sender<Self::Response>)>(DEFAULT_CHANNEL_SIZE);
let handle = ctx.spawn(async move {
let mut res = Ok(());
loop {
tokio::select! {
_ = cancel.cancelled() => {
break;
},
msg = rx.recv() => {
match msg {
Some(m) => { self.handle_message(m.0, m.1); },
None => {res = Err(format!("Sender handle was dropped without calling cancel!").into()); break; },
}
}
};
}
res
});
Handle {
cancel_token,
_handle: handle,
tx,
_type: std::marker::PhantomData::<Self>,
}
}
}