From db03733d3c3bae431eb6a533e25b3ecbf9ab5d94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Wed, 5 Nov 2025 10:32:52 +0100 Subject: [PATCH 1/4] Adapt internals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- crates/tytix-core/src/lib.rs | 147 ++++++++++++++++++++++++-------- crates/tytix-core/src/macros.rs | 21 +++++ crates/tytix/src/lib.rs | 126 ++++++++++++++++++++++----- 3 files changed, 239 insertions(+), 55 deletions(-) create mode 100644 crates/tytix-core/src/macros.rs diff --git a/crates/tytix-core/src/lib.rs b/crates/tytix-core/src/lib.rs index 7651dee..a02b34a 100644 --- a/crates/tytix-core/src/lib.rs +++ b/crates/tytix-core/src/lib.rs @@ -1,8 +1,13 @@ #![feature(const_cmp)] #![feature(const_trait_impl)] +#[macro_use] +mod macros; + use std::any::Any; use std::any::TypeId; +use std::marker::PhantomData; +use std::pin::Pin; pub use anyhow; @@ -10,14 +15,6 @@ pub trait Message: Send + Any { type Reply: Send + Any; } -pub trait MessageIdentifier { - const IDENT: BundleChain; -} - -impl MessageIdentifier for M { - const IDENT: BundleChain = BundleChain::of::(); -} - pub struct InternalMessage { value: Box, name: &'static str, @@ -58,6 +55,46 @@ pub trait Address { fn send(&mut self, message: M) -> impl Future>; } +pub struct BoxedAddress { + addr: Box, + #[allow(clippy::type_complexity)] + send: for<'a> fn( + &'a mut dyn Any, + InternalMessage, + ) -> Pin> + 'a>>, + _pd: PhantomData, +} + +impl BoxedAddress { + pub fn new(addr: IMH) -> Self + where + IMH: InternalMessageHandler + 'static, + { + BoxedAddress { + addr: Box::new(addr), + send: |addr, msg| { + let addr: &mut IMH = addr.downcast_mut().unwrap(); + + Box::pin(addr.handle_message(msg)) + }, + _pd: PhantomData, + } + } +} + +impl InternalMessageHandler for BoxedAddress +where + MB: MessageBundle, +{ + type HandledMessages = MB; + fn handle_message( + &mut self, + msg: InternalMessage, + ) -> impl Future> { + (self.send)(self.addr.as_mut(), msg) + } +} + pub trait InternalMessageHandler { type HandledMessages: MessageBundle; fn handle_message( @@ -93,22 +130,27 @@ where } } -pub trait MessageReceiver {} - pub trait MessageBundle { - const IDS: BundleChain; + const CHAIN: BundleChain; } impl MessageBundle for () { - const IDS: BundleChain = BundleChain { - next: None, - op: BundleOp::Remove(TypeId::of::<()>()), + const CHAIN: BundleChain = BundleChain::empty(); +} + +macro_rules! impl_message_bundles { + ( [ $($ty:ident),* ] , $last:ident ) => { + impl<$($ty,)* $last> MessageBundle for ($($ty,)* $last,) + where + $( $ty: Message, )* + $last: Message, + { + const CHAIN: BundleChain = <($($ty,)*) as MessageBundle>::CHAIN.with::<$last>(); + } }; } -impl MessageBundle for (M,) { - const IDS: BundleChain = BundleChain::of::(); -} +all_the_tuples_special_first!(impl_message_bundles); #[derive(Debug, Clone, Copy)] pub struct BundleChain { @@ -121,6 +163,7 @@ pub enum BundleOp { Add(TypeId), Remove(TypeId), Chain(&'static BundleChain), + None, } impl BundleChain { @@ -135,6 +178,40 @@ impl BundleChain { check_is_contained(self, id) } + pub const fn is_subset_of(&self, ids: &BundleChain) -> bool { + let mut current = self; + + loop { + match current.op { + BundleOp::Add(type_id) => { + if !ids.contains(type_id) { + return false; + } + } + BundleOp::Remove(..) => (), + BundleOp::Chain(bundle_chain) => { + let chain_result = bundle_chain.is_subset_of(ids); + let own_result = if let Some(bc) = self.next { + bc.is_subset_of(ids) + } else { + true + }; + + return chain_result && own_result; + } + BundleOp::None => (), + } + + if let Some(next) = current.next { + current = next; + } else { + break; + }; + } + + true + } + pub const fn with(&'static self) -> BundleChain { let to_add = TypeId::of::(); BundleChain { @@ -157,24 +234,13 @@ impl BundleChain { next: Some(ids), } } -} -impl MessageBundle for (A, B) -where - A: Message, - B: Message, -{ - const IDS: BundleChain = BundleChain::of::().with::(); -} - -impl MessageBundle for (A, B, C, D) -where - A: Message, - B: Message, - C: Message, - D: Message, -{ - const IDS: BundleChain = BundleChain::of::().with::().with::().with::(); + pub const fn empty() -> BundleChain { + BundleChain { + op: BundleOp::None, + next: None, + } + } } pub trait IsContainedInBundle { @@ -186,7 +252,7 @@ where M: Message, MB: MessageBundle, { - const IS_CONTAINED: bool = check_is_contained(&MB::IDS, TypeId::of::()); + const IS_CONTAINED: bool = check_is_contained(&MB::CHAIN, TypeId::of::()); } const fn check_is_contained(ids: &BundleChain, id: TypeId) -> bool { @@ -206,6 +272,7 @@ const fn check_is_contained(ids: &BundleChain, id: TypeId) -> bool { return true; } } + BundleOp::None => (), } if let Some(next) = ids.next { @@ -269,6 +336,16 @@ mod tests { )); } + #[test] + fn check_subset() { + const CHAIN: BundleChain = BundleChain::of::().join(&BundleChain::of::()); + const SUB: BundleChain = BundleChain::of::().with::(); + + assert!(SUB.is_subset_of(&CHAIN)); + + assert!(!BundleChain::of::().is_subset_of(&CHAIN)); + } + #[apply(test!)] async fn check_sending_messages() { struct Sender; diff --git a/crates/tytix-core/src/macros.rs b/crates/tytix-core/src/macros.rs new file mode 100644 index 0000000..bcb7bc2 --- /dev/null +++ b/crates/tytix-core/src/macros.rs @@ -0,0 +1,21 @@ +#[rustfmt::skip] +macro_rules! all_the_tuples_special_first { + ($name:ident) => { + $name!([], M1); + $name!([M1], M2); + $name!([M1, M2], M3); + $name!([M1, M2, M3], M4); + $name!([M1, M2, M3, M4], M5); + $name!([M1, M2, M3, M4, M5], M6); + $name!([M1, M2, M3, M4, M5, M6], M7); + $name!([M1, M2, M3, M4, M5, M6, M7], M8); + $name!([M1, M2, M3, M4, M5, M6, M7, M8], M9); + $name!([M1, M2, M3, M4, M5, M6, M7, M8, M9], M10); + $name!([M1, M2, M3, M4, M5, M6, M7, M8, M9, M10], M11); + $name!([M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11], M12); + $name!([M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12], M13); + $name!([M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12, M13], M14); + $name!([M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12, M13, M14], M15); + $name!([M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12, M13, M14, M15], M16); + }; +} diff --git a/crates/tytix/src/lib.rs b/crates/tytix/src/lib.rs index f1222ef..523c5df 100644 --- a/crates/tytix/src/lib.rs +++ b/crates/tytix/src/lib.rs @@ -1,6 +1,7 @@ use std::marker::PhantomData; use futures::FutureExt; +use tytix_core::BoxedAddress; use tytix_core::InternalMessage; use tytix_core::InternalMessageHandler; use tytix_core::IsContainedInBundle; @@ -18,17 +19,29 @@ pub trait AddressExt { fn inspect(self, f: F) -> Inspect where - F: Fn(&M) -> U, - M: Message + IsContainedInBundle, + Inspect: InternalMessageHandler, Self: Sized; fn join(self, o: Other) -> Joined where Joined: InternalMessageHandler, Self: Sized; + + fn simplify(self) -> SimpleAddress + where + SimpleAddress: InternalMessageHandler, + Self: Sized; + + fn boxed(self) -> BoxedAddress + where + BoxedAddress: InternalMessageHandler; } -impl> AddressExt for A { +impl AddressExt for A +where + MB: MessageBundle, + A: InternalMessageHandler + 'static, +{ fn map(self, f: F, r: RF) -> MappedMessage where MappedMessage: InternalMessageHandler, @@ -44,16 +57,9 @@ impl> Address fn inspect(self, f: F) -> Inspect where - F: Fn(&M) -> U, + Inspect: InternalMessageHandler, Self: Sized, - M: Message + IsContainedInBundle, { - const { - let true = >::IS_CONTAINED else { - panic!("Message is not contained in MessageBundle",); - }; - } - Inspect { address: self, func: f, @@ -71,6 +77,47 @@ impl> Address right: o, } } + + fn simplify(self) -> SimpleAddress + where + SimpleAddress: InternalMessageHandler, + Self: Sized, + { + SimpleAddress { + inner: self, + _pd: PhantomData, + } + } + + fn boxed(self) -> BoxedAddress { + BoxedAddress::new(self) + } +} + +pub struct SimpleAddress { + inner: A, + _pd: PhantomData, +} + +impl InternalMessageHandler for SimpleAddress +where + SMB: MessageBundle, + MB: MessageBundle, + A: InternalMessageHandler, +{ + type HandledMessages = SMB; + + fn handle_message( + &mut self, + msg: InternalMessage, + ) -> impl Future> { + const { + let true = SMB::CHAIN.is_subset_of(&MB::CHAIN) else { + panic!("Message is not contained in MessageBundle",); + }; + } + self.inner.handle_message(msg) + } } pub struct Joined { @@ -87,7 +134,8 @@ where L: MessageBundle, R: MessageBundle, { - const IDS: tytix_core::BundleChain = ::IDS.join(&::IDS); + const CHAIN: tytix_core::BundleChain = + ::CHAIN.join(&::CHAIN); } impl InternalMessageHandler for Joined @@ -100,7 +148,7 @@ where async fn handle_message(&mut self, msg: InternalMessage) -> anyhow::Result { let message_id = msg.type_id(); - if L::HandledMessages::IDS.contains(message_id) { + if L::HandledMessages::CHAIN.contains(message_id) { self.left.handle_message(msg).await } else { self.right.handle_message(msg).await @@ -148,7 +196,7 @@ where M: Message, R: Message + IsContainedInBundle, { - const IDS: tytix_core::BundleChain = MB::IDS.without::().with::(); + const CHAIN: tytix_core::BundleChain = MB::CHAIN.without::().with::(); } impl InternalMessageHandler for MappedMessage @@ -219,9 +267,9 @@ mod tests { type Reply = usize; } - struct SimpleAddress; + struct FooBarAddress; - impl InternalMessageHandler for SimpleAddress { + impl InternalMessageHandler for FooBarAddress { type HandledMessages = (Foo, Bar); async fn handle_message( @@ -253,7 +301,7 @@ mod tests { async fn check_mapping() { static MSG: OnceLock = OnceLock::new(); - let mut sa = SimpleAddress.map( + let mut sa = FooBarAddress.map( |_b: Bar| { let _ = MSG.set(true); async { Foo } @@ -270,7 +318,7 @@ mod tests { async fn check_inspect() { static MSG: OnceLock = OnceLock::new(); - let mut sa = SimpleAddress.inspect(|_b: &Bar| { + let mut sa = FooBarAddress.inspect(|_b: &Bar| { let _ = MSG.set(true); async {} }); @@ -285,11 +333,11 @@ mod tests { } #[apply(test!)] - async fn check_join() { + async fn check_simplify() { static MSG_SA: OnceLock = OnceLock::new(); static MSG_ZAP: OnceLock = OnceLock::new(); - let sa = SimpleAddress.inspect(|_b: &Bar| { + let sa = FooBarAddress.inspect(|_b: &Bar| { MSG_SA.set(true).unwrap(); async {} }); @@ -308,4 +356,42 @@ mod tests { MSG_ZAP.get().expect("The message was NOT inspected!"); } + + #[apply(test!)] + async fn check_join() { + static MSG_SA: OnceLock = OnceLock::new(); + + let sa = FooBarAddress.inspect(|_b: &Bar| { + MSG_SA.set(true).unwrap(); + async {} + }); + + let zap = ZapAddress.join(ZapAddress).join(ZapAddress); + + let joined = sa.join(zap); + + let mut simple = joined.simplify::<(Foo, Bar, Zap)>(); + + simple.send(Bar).await.unwrap(); + + MSG_SA.get().expect("The message was not :CC inspected!"); + } + + #[apply(test!)] + async fn check_boxed() { + static MSG_SA: OnceLock = OnceLock::new(); + + let sa = FooBarAddress.inspect(|_b: &Bar| { + MSG_SA.set(true).unwrap(); + async {} + }); + + let zap = ZapAddress.join(ZapAddress).join(ZapAddress); + + let mut boxed = zap.join(sa).simplify::<(Bar, Zap, Foo)>().boxed(); + + boxed.send(Bar).await.unwrap(); + + MSG_SA.get().expect("The message was not :CC inspected!"); + } } From bf3e65a6e2521d4539fcda5a6a78f6b7a5bb2e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Wed, 5 Nov 2025 10:59:42 +0100 Subject: [PATCH 2/4] Re-order internals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- crates/tytix-core/src/address.rs | 87 ++++++++++++++++ crates/tytix-core/src/lib.rs | 168 +++---------------------------- crates/tytix-core/src/message.rs | 66 ++++++++++++ 3 files changed, 169 insertions(+), 152 deletions(-) create mode 100644 crates/tytix-core/src/address.rs create mode 100644 crates/tytix-core/src/message.rs diff --git a/crates/tytix-core/src/address.rs b/crates/tytix-core/src/address.rs new file mode 100644 index 0000000..c80671b --- /dev/null +++ b/crates/tytix-core/src/address.rs @@ -0,0 +1,87 @@ +use std::any::Any; +use std::marker::PhantomData; +use std::pin::Pin; + +use crate::InternalMessage; +use crate::IsContainedInBundle; +use crate::Message; +use crate::MessageBundle; + +pub trait Address { + fn send(&mut self, message: M) -> impl Future>; +} + +pub struct BoxedAddress { + addr: Box, + #[allow(clippy::type_complexity)] + send: for<'a> fn( + &'a mut dyn Any, + InternalMessage, + ) -> Pin> + 'a>>, + _pd: PhantomData, +} + +impl BoxedAddress { + pub fn new(addr: IMH) -> Self + where + IMH: InternalMessageHandler + 'static, + { + BoxedAddress { + addr: Box::new(addr), + send: |addr, msg| { + let addr: &mut IMH = addr.downcast_mut().unwrap(); + + Box::pin(addr.handle_message(msg)) + }, + _pd: PhantomData, + } + } +} + +impl InternalMessageHandler for BoxedAddress +where + MB: MessageBundle, +{ + type HandledMessages = MB; + fn handle_message( + &mut self, + msg: InternalMessage, + ) -> impl Future> { + (self.send)(self.addr.as_mut(), msg) + } +} + +pub trait InternalMessageHandler { + type HandledMessages: MessageBundle; + fn handle_message( + &mut self, + msg: InternalMessage, + ) -> impl Future>; +} + +impl Address for IMH +where + IMH: InternalMessageHandler, +{ + fn send(&mut self, message: M) -> impl Future> { + const { + let true = >::IS_CONTAINED else { + panic!("Message is not contained in MessageBundle",); + }; + } + + let message = self.handle_message(InternalMessage::new(message)); + + async { + message.await.and_then(|msg| { + msg.into_inner::().map_err(|e| { + anyhow::anyhow!( + "Expected a {}, but got a {}", + std::any::type_name::(), + e.type_name() + ) + }) + }) + } + } +} diff --git a/crates/tytix-core/src/lib.rs b/crates/tytix-core/src/lib.rs index a02b34a..213fe7d 100644 --- a/crates/tytix-core/src/lib.rs +++ b/crates/tytix-core/src/lib.rs @@ -3,154 +3,18 @@ #[macro_use] mod macros; +pub mod address; +pub mod message; -use std::any::Any; use std::any::TypeId; -use std::marker::PhantomData; -use std::pin::Pin; +pub use address::Address; +pub use address::BoxedAddress; +pub use address::InternalMessageHandler; pub use anyhow; - -pub trait Message: Send + Any { - type Reply: Send + Any; -} - -pub struct InternalMessage { - value: Box, - name: &'static str, -} - -impl InternalMessage { - pub fn new(message: M) -> InternalMessage { - InternalMessage { - value: Box::new(message), - name: std::any::type_name::(), - } - } - - pub fn into_inner(self) -> Result { - self.value - .downcast() - .map(|v| *v) - .map_err(|value| InternalMessage { - value, - name: self.name, - }) - } - - pub fn as_ref(&self) -> Option<&M> { - self.value.downcast_ref() - } - - pub fn type_name(&self) -> &'static str { - self.name - } - - pub fn type_id(&self) -> TypeId { - self.value.as_ref().type_id() - } -} - -pub trait Address { - fn send(&mut self, message: M) -> impl Future>; -} - -pub struct BoxedAddress { - addr: Box, - #[allow(clippy::type_complexity)] - send: for<'a> fn( - &'a mut dyn Any, - InternalMessage, - ) -> Pin> + 'a>>, - _pd: PhantomData, -} - -impl BoxedAddress { - pub fn new(addr: IMH) -> Self - where - IMH: InternalMessageHandler + 'static, - { - BoxedAddress { - addr: Box::new(addr), - send: |addr, msg| { - let addr: &mut IMH = addr.downcast_mut().unwrap(); - - Box::pin(addr.handle_message(msg)) - }, - _pd: PhantomData, - } - } -} - -impl InternalMessageHandler for BoxedAddress -where - MB: MessageBundle, -{ - type HandledMessages = MB; - fn handle_message( - &mut self, - msg: InternalMessage, - ) -> impl Future> { - (self.send)(self.addr.as_mut(), msg) - } -} - -pub trait InternalMessageHandler { - type HandledMessages: MessageBundle; - fn handle_message( - &mut self, - msg: InternalMessage, - ) -> impl Future>; -} - -impl Address for IMH -where - IMH: InternalMessageHandler, -{ - fn send(&mut self, message: M) -> impl Future> { - const { - let true = >::IS_CONTAINED else { - panic!("Message is not contained in MessageBundle",); - }; - } - - let message = self.handle_message(InternalMessage::new(message)); - - async { - message.await.and_then(|msg| { - msg.into_inner::().map_err(|e| { - anyhow::anyhow!( - "Expected a {}, but got a {}", - std::any::type_name::(), - e.type_name() - ) - }) - }) - } - } -} - -pub trait MessageBundle { - const CHAIN: BundleChain; -} - -impl MessageBundle for () { - const CHAIN: BundleChain = BundleChain::empty(); -} - -macro_rules! impl_message_bundles { - ( [ $($ty:ident),* ] , $last:ident ) => { - impl<$($ty,)* $last> MessageBundle for ($($ty,)* $last,) - where - $( $ty: Message, )* - $last: Message, - { - const CHAIN: BundleChain = <($($ty,)*) as MessageBundle>::CHAIN.with::<$last>(); - } - }; -} - -all_the_tuples_special_first!(impl_message_bundles); +pub use message::InternalMessage; +pub use message::Message; +pub use message::MessageBundle; #[derive(Debug, Clone, Copy)] pub struct BundleChain { @@ -159,7 +23,7 @@ pub struct BundleChain { } #[derive(Debug, Clone, Copy)] -pub enum BundleOp { +enum BundleOp { Add(TypeId), Remove(TypeId), Chain(&'static BundleChain), @@ -167,6 +31,13 @@ pub enum BundleOp { } impl BundleChain { + pub const fn empty() -> BundleChain { + BundleChain { + op: BundleOp::None, + next: None, + } + } + pub const fn of() -> BundleChain { BundleChain { op: BundleOp::Add(TypeId::of::()), @@ -234,13 +105,6 @@ impl BundleChain { next: Some(ids), } } - - pub const fn empty() -> BundleChain { - BundleChain { - op: BundleOp::None, - next: None, - } - } } pub trait IsContainedInBundle { diff --git a/crates/tytix-core/src/message.rs b/crates/tytix-core/src/message.rs new file mode 100644 index 0000000..e91d29f --- /dev/null +++ b/crates/tytix-core/src/message.rs @@ -0,0 +1,66 @@ +use std::any::Any; +use std::any::TypeId; + +use crate::BundleChain; + +pub trait Message: Send + Any { + type Reply: Send + Any; +} + +pub struct InternalMessage { + value: Box, + name: &'static str, +} + +impl InternalMessage { + pub fn new(message: M) -> InternalMessage { + InternalMessage { + value: Box::new(message), + name: std::any::type_name::(), + } + } + + pub fn into_inner(self) -> Result { + self.value + .downcast() + .map(|v| *v) + .map_err(|value| InternalMessage { + value, + name: self.name, + }) + } + + pub fn as_ref(&self) -> Option<&M> { + self.value.downcast_ref() + } + + pub fn type_name(&self) -> &'static str { + self.name + } + + pub fn type_id(&self) -> TypeId { + self.value.as_ref().type_id() + } +} + +pub trait MessageBundle { + const CHAIN: BundleChain; +} + +impl MessageBundle for () { + const CHAIN: BundleChain = BundleChain::empty(); +} + +macro_rules! impl_message_bundles { + ( [ $($ty:ident),* ] , $last:ident ) => { + impl<$($ty,)* $last> MessageBundle for ($($ty,)* $last,) + where + $( $ty: Message, )* + $last: Message, + { + const CHAIN: BundleChain = <($($ty,)*) as MessageBundle>::CHAIN.with::<$last>(); + } + }; +} + +all_the_tuples_special_first!(impl_message_bundles); From d38b04396c2596adf81b02c3be18176f48b45483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Wed, 5 Nov 2025 14:21:38 +0100 Subject: [PATCH 3/4] Add Actor interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the early idea. The error message is still not great if the HandledMessages don't fit the Actor. (Aka forgot to impl Handle> Signed-off-by: Marcel Müller --- crates/tytix-core/src/actor.rs | 139 ++++++++++++++++++++++++++++++++ crates/tytix-core/src/lib.rs | 1 + crates/tytix-core/src/macros.rs | 22 +++++ 3 files changed, 162 insertions(+) create mode 100644 crates/tytix-core/src/actor.rs diff --git a/crates/tytix-core/src/actor.rs b/crates/tytix-core/src/actor.rs new file mode 100644 index 0000000..ddf679b --- /dev/null +++ b/crates/tytix-core/src/actor.rs @@ -0,0 +1,139 @@ +use std::any::Any; +use std::marker::PhantomData; +use std::pin::Pin; + +use crate::InternalMessage; +use crate::IsContainedInBundle; +use crate::Message; +use crate::MessageBundle; + +pub trait Actor: Any { + type HandledMessages: MessageBundle; +} + +pub trait Handle { + fn handle(&mut self, message: M) -> impl Future>; +} + +pub trait ActorHandler: Any { + fn handle_message( + &mut self, + message: InternalMessage, + ) -> impl Future>; +} + +macro_rules! impl_actor_handle { + ( $($ty:ident),* ) => { + impl ActorHandler<($($ty,)*)> for ACTOR + where + ACTOR: Actor, + $( ACTOR: Handle<$ty>, )* + $( $ty: Message, )* + { + fn handle_message( + &mut self, + msg: InternalMessage, + ) -> impl Future> { + + #[allow(unused_variables)] + async { + $( + let msg = match msg.into_inner::<$ty>() { + Ok(msg) => { + return self.handle(msg).await.map(InternalMessage::new); + } + Err(msg) => msg, + }; + )* + + Err(anyhow::anyhow!("Could not handle message")) + + } + } + } + }; +} + +all_the_tuples!(impl_actor_handle); + +pub struct ActorHandle { + actor: Box, + #[allow(clippy::type_complexity)] + handle: for<'a> fn( + &'a mut dyn Any, + msg: InternalMessage, + ) -> Pin> + 'a>>, + _pd: PhantomData, +} + +impl ActorHandle { + pub fn new>(actor: A) -> ActorHandle { + ActorHandle { + actor: Box::new(actor), + handle: |actor, msg| { + let actor: &mut A = actor.downcast_mut().unwrap(); + + Box::pin(actor.handle_message(msg)) + }, + + _pd: PhantomData, + } + } + + pub async fn handle>( + &mut self, + message: M, + ) -> anyhow::Result { + const { + let true = >::IS_CONTAINED else { + panic!("Message is not contained in MessageBundle",); + }; + } + + (self.handle)(self.actor.as_mut(), InternalMessage::new(message)) + .await + .map(|msg| match msg.into_inner::() { + Ok(t) => t, + Err(msg) => panic!("Could not process reply of type: {}", msg.type_name()), + }) + } +} + +#[cfg(test)] +mod tests { + use macro_rules_attribute::apply; + use smol_macros::test; + + use super::*; + + struct Foo; + + impl Message for Foo { + type Reply = (); + } + + struct Zap; + + impl Message for Zap { + type Reply = (); + } + + struct FActor; + + impl Actor for FActor { + type HandledMessages = (Foo,); + } + + impl Handle for FActor { + async fn handle(&mut self, _message: Foo) -> anyhow::Result<::Reply> { + todo!() + } + } + + #[apply(test!)] + async fn test_name() { + let mut actor = ActorHandle::new(FActor); + + actor.handle(Zap).await.unwrap(); + } +} diff --git a/crates/tytix-core/src/lib.rs b/crates/tytix-core/src/lib.rs index 213fe7d..3a8bd5f 100644 --- a/crates/tytix-core/src/lib.rs +++ b/crates/tytix-core/src/lib.rs @@ -5,6 +5,7 @@ mod macros; pub mod address; pub mod message; +pub mod actor; use std::any::TypeId; diff --git a/crates/tytix-core/src/macros.rs b/crates/tytix-core/src/macros.rs index bcb7bc2..914ce3d 100644 --- a/crates/tytix-core/src/macros.rs +++ b/crates/tytix-core/src/macros.rs @@ -1,3 +1,25 @@ +#[rustfmt::skip] +macro_rules! all_the_tuples { + ($name:ident) => { + $name!(M1); + $name!(M1, M2); + $name!(M1, M2, M3); + $name!(M1, M2, M3, M4); + $name!(M1, M2, M3, M4, M5); + $name!(M1, M2, M3, M4, M5, M6); + $name!(M1, M2, M3, M4, M5, M6, M7); + $name!(M1, M2, M3, M4, M5, M6, M7, M8); + $name!(M1, M2, M3, M4, M5, M6, M7, M8, M9); + $name!(M1, M2, M3, M4, M5, M6, M7, M8, M9, M10); + $name!(M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11); + $name!(M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12); + $name!(M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12, M13); + $name!(M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12, M13, M14); + $name!(M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12, M13, M14, M15); + $name!(M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12, M13, M14, M15, M16); + }; +} + #[rustfmt::skip] macro_rules! all_the_tuples_special_first { ($name:ident) => { From 023a2a17fc407dd34657b873d7edaea529f78483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Wed, 5 Nov 2025 14:48:51 +0100 Subject: [PATCH 4/4] Add more ergonomic constructor of ActorHandles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- crates/tytix-core/src/actor.rs | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/crates/tytix-core/src/actor.rs b/crates/tytix-core/src/actor.rs index ddf679b..1b9391f 100644 --- a/crates/tytix-core/src/actor.rs +++ b/crates/tytix-core/src/actor.rs @@ -7,10 +7,23 @@ use crate::IsContainedInBundle; use crate::Message; use crate::MessageBundle; -pub trait Actor: Any { +pub trait Actor: Any +where + Self: ActorHandler, +{ type HandledMessages: MessageBundle; } +pub trait IntoActorHandle { + fn into_actor_handle(self) -> ActorHandle; +} + +impl IntoActorHandle for A { + fn into_actor_handle(self) -> ActorHandle { + ActorHandle::new(self) + } +} + pub trait Handle { fn handle(&mut self, message: M) -> impl Future>; } @@ -26,7 +39,7 @@ macro_rules! impl_actor_handle { ( $($ty:ident),* ) => { impl ActorHandler<($($ty,)*)> for ACTOR where - ACTOR: Actor, + ACTOR: Actor, $( ACTOR: Handle<$ty>, )* $( $ty: Message, )* { @@ -40,7 +53,7 @@ macro_rules! impl_actor_handle { $( let msg = match msg.into_inner::<$ty>() { Ok(msg) => { - return self.handle(msg).await.map(InternalMessage::new); + return >::handle(self, msg).await.map(InternalMessage::new); } Err(msg) => msg, }; @@ -50,6 +63,7 @@ macro_rules! impl_actor_handle { } } + } }; } @@ -112,12 +126,6 @@ mod tests { type Reply = (); } - struct Zap; - - impl Message for Zap { - type Reply = (); - } - struct FActor; impl Actor for FActor { @@ -126,14 +134,14 @@ mod tests { impl Handle for FActor { async fn handle(&mut self, _message: Foo) -> anyhow::Result<::Reply> { - todo!() + Ok(()) } } #[apply(test!)] async fn test_name() { - let mut actor = ActorHandle::new(FActor); + let mut actor = FActor.into_actor_handle(); - actor.handle(Zap).await.unwrap(); + actor.handle(Foo).await.unwrap(); } }