commit 88121780ce5e805a7a9b1831dd8e8a93ca35acbe Author: Zachery Aaron Shores-Chmielewski Date: Mon Nov 24 19:39:17 2025 -0500 init Skeletal actor framework. Somewhat unweildy, needs a message box, a better runtime, and different channels. However, hello world example works Signed-off-by: Zachery Aaron Shores-Chmielewski diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fd2fd7a --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ba24c26 --- /dev/null +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md new file mode 100644 index 0000000..0e5a150 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# about +Small wasm-compatible actor library diff --git a/examples/hello.rs b/examples/hello.rs new file mode 100644 index 0000000..57f7611 --- /dev/null +++ b/examples/hello.rs @@ -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) { + 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}"), + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..750effe --- /dev/null +++ b/src/error.rs @@ -0,0 +1,5 @@ +pub type Error = Box; +pub type Result = std::result::Result; +pub fn convert_err(e: E) -> Error { + format!("{e:?}").into() +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8bbc7ec --- /dev/null +++ b/src/lib.rs @@ -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 +where + A: Actor, +{ + cancel_token: CancellationToken, + tx: mpsc::Sender<(A::Message, oneshot::Sender)>, + _handle: JoinHandle>, + // to prevent accidental swaps, strongly type the handle + _type: std::marker::PhantomData, +} + +impl Handle { + + /// 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 { + let (tx, rx) = oneshot::channel::(); + self.tx.send((msg, tx)).await.map_err(convert_err)?; + + rx.await.map_err(convert_err) + } +} + +impl Drop for Handle { + 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); + + /// Spawns the `Actor` utilizing the given runtime context + /// Only `tokio` runtime is accepted for now + fn spawn(self, ctx: &tokio::runtime::Runtime) -> Handle { + let cancel_token = CancellationToken::new(); + let cancel = cancel_token.clone(); + + let (tx, mut rx) = + mpsc::channel::<(Self::Message, oneshot::Sender)>(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::, + } + } +}