first commit

This commit is contained in:
sinu
2023-12-09 15:44:44 -08:00
commit 1091d87512
16 changed files with 747 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
**/target
Cargo.lock

2
Cargo.toml Normal file
View File

@@ -0,0 +1,2 @@
[workspace]
members = ["ludi-core", "ludi-macros", "ludi"]

10
ludi-core/Cargo.toml Normal file
View File

@@ -0,0 +1,10 @@
[package]
name = "ludi-core"
version = "0.1.0"
edition = "2021"
[dependencies]
futures = { version = "0.3" }
[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

53
ludi-core/src/envelope.rs Normal file
View File

@@ -0,0 +1,53 @@
use std::marker::PhantomData;
use futures::channel::oneshot::{self, Receiver, Sender};
use crate::{Actor, Context, Mailbox, Message};
pub type ActorEnvelope<A> =
Envelope<<A as Actor>::Message, <<A as Actor>::Message as Message<A>>::Return, A>;
pub struct Envelope<M, R, A> {
msg: M,
send: Option<Sender<R>>,
_pd: PhantomData<A>,
}
impl<M, R, A> Envelope<M, R, A> {
pub fn new(msg: M) -> Self {
Self {
msg,
send: None,
_pd: PhantomData,
}
}
pub fn new_returning(msg: M) -> (Self, Receiver<R>) {
let (send, recv) = oneshot::channel();
(
Self {
msg,
send: Some(send),
_pd: PhantomData,
},
recv,
)
}
}
impl<M, R, A> Envelope<M, R, A>
where
A: Actor,
M: Message<A, Return = R> + Send + 'static,
R: Send + 'static,
{
pub async fn handle<T: Mailbox<A>>(mut self, actor: &mut A, ctx: &mut Context<'_, A, T>) {
self.msg
.handle(actor, ctx, move |ret| {
if let Some(send) = self.send.take() {
let _ = send.send(ret);
}
})
.await;
}
}

115
ludi-core/src/lib.rs Normal file
View File

@@ -0,0 +1,115 @@
#![deny(unsafe_code)]
pub mod envelope;
pub mod mailbox;
use std::marker::PhantomData;
use envelope::ActorEnvelope;
use futures::{Future, Stream, StreamExt};
pub use envelope::Envelope;
pub trait Message<A: Actor>: Send + Sized + 'static {
type Return: Send + 'static;
fn handle<M: Mailbox<A>, R: FnOnce(Self::Return) + Send>(
self,
actor: &mut A,
ctx: &mut Context<A, M>,
ret: R,
) -> impl Future<Output = ()> + Send;
}
pub trait Actor: Send + Sized + 'static {
type Message: Message<Self>;
type Stop;
fn started(
&mut self,
_ctx: &mut Context<'_, Self, impl Mailbox<Self>>,
) -> Result<(), Self::Stop> {
Ok(())
}
fn stopped(&mut self) -> impl Future<Output = Self::Stop> + Send;
fn run(&mut self, mut mailbox: impl Mailbox<Self>) -> impl Future<Output = Self::Stop> + Send {
async move {
let mut ctx = Context::new(&mut mailbox);
if let Err(stop) = self.started(&mut ctx) {
return stop;
}
while let Some(msg) = mailbox.next().await {
let mut ctx = Context::new(&mut mailbox);
msg.handle(self, &mut ctx).await;
if ctx.stopped() {
break;
}
}
self.stopped().await
}
}
}
pub trait Handler<T>: Actor {
type Return: Send + 'static;
fn handle<M: Mailbox<Self>>(
&mut self,
msg: T,
ctx: &mut Context<Self, M>,
) -> impl Future<Output = Self::Return> + Send;
fn after<M: Mailbox<Self>>(
&mut self,
_ctx: &mut Context<Self, M>,
) -> impl Future<Output = ()> + Send {
async {}
}
}
pub trait Mailbox<A: Actor>: Stream<Item = ActorEnvelope<A>> + Send + Unpin + 'static {
type Address: Address<A>;
fn address(&self) -> &Self::Address;
}
pub trait Address<A: Actor>: Clone + Send + 'static {
fn send<M>(&self, msg: M) -> impl Future<Output = <A as Handler<M>>::Return> + Send
where
A: Handler<M>,
<A::Message as Message<A>>::Return: Into<<A as Handler<M>>::Return>,
M: Into<A::Message> + Send;
}
pub struct Context<'a, A: Actor, M: Mailbox<A>> {
running: bool,
mailbox: &'a mut M,
_pd: PhantomData<A>,
}
impl<'a, A: Actor, M: Mailbox<A>> Context<'a, A, M> {
pub fn new(mailbox: &'a mut M) -> Self {
Self {
running: true,
mailbox,
_pd: PhantomData,
}
}
pub fn mailbox(&mut self) -> &mut M {
self.mailbox
}
pub fn stop(&mut self) {
self.running = false;
}
pub fn stopped(&self) -> bool {
!self.running
}
}

71
ludi-core/src/mailbox.rs Normal file
View File

@@ -0,0 +1,71 @@
use futures::{channel::mpsc, SinkExt, Stream};
use crate::{Actor, Address, Envelope, Handler, Mailbox, Message};
pub struct FuturesMailbox<A: Actor> {
addr: FuturesAddress<A>,
recv: mpsc::Receiver<Envelope<A::Message, <A::Message as Message<A>>::Return, A>>,
}
impl<A: Actor> FuturesMailbox<A> {
pub fn new() -> Self {
let (send, recv) = mpsc::channel(100);
Self {
addr: FuturesAddress { send },
recv,
}
}
}
impl<A: Actor> Mailbox<A> for FuturesMailbox<A> {
type Address = FuturesAddress<A>;
fn address(&self) -> &Self::Address {
&self.addr
}
}
impl<A: Actor> Stream for FuturesMailbox<A> {
type Item = Envelope<A::Message, <A::Message as Message<A>>::Return, A>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
std::pin::Pin::new(&mut self.recv).poll_next(cx)
}
}
pub struct FuturesAddress<A: Actor> {
send: mpsc::Sender<Envelope<A::Message, <A::Message as Message<A>>::Return, A>>,
}
impl<A: Actor> Clone for FuturesAddress<A> {
fn clone(&self) -> Self {
Self {
send: self.send.clone(),
}
}
}
impl<A: Actor> Address<A> for FuturesAddress<A> {
async fn send<M>(&self, msg: M) -> <A as Handler<M>>::Return
where
A: Handler<M>,
<A::Message as Message<A>>::Return: Into<<A as Handler<M>>::Return>,
M: Into<A::Message> + Send,
{
let msg: A::Message = msg.into();
let (env, ret) = Envelope::new_returning(msg.into());
let mut send = self.send.clone();
send.send(env).await.unwrap();
let ret: <A::Message as Message<A>>::Return = ret.await.unwrap();
ret.into()
}
}

119
ludi-core/tests/api.rs Normal file
View File

@@ -0,0 +1,119 @@
use ludi_core::{mailbox::FuturesMailbox, *};
struct PingActor;
impl Actor for PingActor {
type Message = PingMessage;
type Stop = ();
async fn stopped(&mut self) -> Self::Stop {}
}
impl Handler<Ping> for PingActor {
type Return = String;
async fn handle<M: Mailbox<Self>>(
&mut self,
_msg: Ping,
_ctx: &mut Context<'_, Self, M>,
) -> Self::Return {
println!("ping");
"pong".to_string()
}
}
impl Handler<Pong> for PingActor {
type Return = ();
async fn handle<M: Mailbox<Self>>(
&mut self,
_msg: Pong,
_ctx: &mut Context<'_, Self, M>,
) -> Self::Return {
println!("pong");
}
async fn after<M: Mailbox<Self>>(&mut self, _ctx: &mut Context<'_, Self, M>) {
println!("sent pong");
}
}
enum PingMessage {
Ping(Ping),
Pong(Pong),
}
enum PingReturn {
Ping(String),
Pong(()),
}
impl Into<()> for PingReturn {
fn into(self) -> () {
match self {
PingReturn::Pong(()) => (),
_ => unreachable!("handler returned unexpected type, this indicates the `Message` implementation is incorrect"),
}
}
}
impl Into<String> for PingReturn {
fn into(self) -> String {
match self {
PingReturn::Ping(s) => s,
_ => unreachable!("handler returned unexpected type, this indicates the `Message` implementation is incorrect"),
}
}
}
impl Message<PingActor> for PingMessage {
type Return = PingReturn;
async fn handle<M: Mailbox<PingActor>, R: FnOnce(PingReturn)>(
self,
actor: &mut PingActor,
ctx: &mut Context<'_, PingActor, M>,
ret: R,
) {
match self {
PingMessage::Ping(ping) => {
let value = PingReturn::Ping(Handler::<Ping>::handle(actor, ping, ctx).await);
ret(value);
Handler::<Ping>::after(actor, ctx).await;
}
PingMessage::Pong(pong) => {
let value = PingReturn::Pong(Handler::<Pong>::handle(actor, pong, ctx).await);
ret(value);
Handler::<Pong>::after(actor, ctx).await;
}
};
}
}
struct Ping;
impl From<Ping> for PingMessage {
fn from(value: Ping) -> Self {
PingMessage::Ping(value)
}
}
struct Pong;
impl From<Pong> for PingMessage {
fn from(value: Pong) -> Self {
PingMessage::Pong(value)
}
}
#[tokio::test]
async fn test_api() {
let mailbox = FuturesMailbox::new();
let addr = mailbox.address().clone();
let mut actor = PingActor;
tokio::spawn(async move { actor.run(mailbox).await });
addr.send(Pong).await;
}

13
ludi-macros/Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "ludi-macros"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
syn = { version = "1.0", features = ["full", "extra-traits", "visit"] }
quote = "1.0"
proc-macro2 = "1.0"
heck = "0.4.1"

View File

@@ -0,0 +1,71 @@
use heck::{ToSnakeCase, ToUpperCamelCase};
use quote::quote;
use syn::{parse_str, Ident, ItemImpl, Path};
use crate::types::MethodSig;
pub(crate) fn impl_implement(item: ItemImpl) -> proc_macro::TokenStream {
let self_ty = *item.self_ty;
let trait_path = item.trait_.expect("expected trait implementation").1;
let msgs_module: Path = parse_str(&format!(
"{}_msgs",
quote!(#trait_path).to_string().to_snake_case()
))
.unwrap();
let (method_blocks, impl_blocks): (Vec<_>, Vec<_>) = item
.items
.into_iter()
.filter_map(|item| match item {
syn::ImplItem::Method(method) => Some(method),
_ => None,
})
.map(|method| {
let full_sig = method.sig.clone();
let sig = MethodSig::from(method.sig);
let msg_ident: Ident = parse_str(&sig.ident.to_string().to_upper_camel_case()).unwrap();
let block = method.block;
let ret = sig.ret;
let structure = if sig.args.is_empty() {
quote!(#msgs_module :: #msg_ident)
} else {
let arg_idents = sig.args.iter().map(|(ident, _)| ident);
quote!(#msgs_module :: #msg_ident { #( #arg_idents ),* })
};
let method_block = quote!(
#full_sig {
self.send(#structure).await
}
);
let impl_block = quote!(
impl ::ludi::Handler<#msgs_module :: #msg_ident> for #self_ty {
type Return = #ret;
async fn handle<M: ::ludi::Mailbox<Self>>(
&mut self,
msg: #msgs_module :: #msg_ident,
ctx: &mut ::ludi::Context<'_, Self, M>,
) -> Self::Return {
let #structure = msg;
#block
}
}
);
(method_block, impl_block)
})
.unzip();
quote!(
impl<A: ::ludi::Address<#self_ty>> #trait_path for A {
#( #method_blocks )*
}
#( #impl_blocks )*
)
.into()
}

View File

@@ -0,0 +1,133 @@
use std::collections::HashMap;
use heck::{ToSnakeCase, ToUpperCamelCase};
use proc_macro2::Span;
use quote::quote;
use syn::{parse_str, Ident, ItemTrait, Path, Type};
use crate::types::MethodSig;
pub(crate) fn impl_interface(item: ItemTrait) -> proc_macro::TokenStream {
let ident = &item.ident;
let msgs_module: Path =
parse_str(&format!("{}_msgs", ident.to_string().to_snake_case())).unwrap();
let sigs = item.items.into_iter().filter_map(|item| match item {
syn::TraitItem::Method(method) => Some(MethodSig::from(method.sig)),
_ => None,
});
let msg_enum_name = Ident::new(&format!("{}Message", ident), Span::call_site());
let msg_return_enum_name = Ident::new(&format!("{}MessageReturn", ident), Span::call_site());
let mut msg_idents = Vec::new();
let mut msg_arg_idents = Vec::new();
let mut msg_arg_types = Vec::new();
let mut msg_rets = Vec::new();
let mut msgs = Vec::new();
let mut ret_map: HashMap<Type, Vec<Ident>> = HashMap::new();
for sig in sigs {
let MethodSig { ident, args, ret } = sig;
let ident: Ident = parse_str(&ident.to_string().to_upper_camel_case()).unwrap();
let msg = if args.is_empty() {
quote!(
pub struct #ident;
)
} else {
let arg_idents = args.iter().map(|(ident, _)| ident);
let arg_types = args.iter().map(|(_, ty)| ty);
quote!(
pub struct #ident {
#( pub #arg_idents: #arg_types ),*
}
)
};
msgs.push(msg);
msg_idents.push(ident.clone());
for arg in args {
msg_arg_idents.push(arg.0);
msg_arg_types.push(arg.1);
}
if let Some(variants) = ret_map.get_mut(&ret) {
variants.push(ident);
} else {
ret_map.insert(ret.clone(), vec![ident]);
}
msg_rets.push(ret);
}
let ret_into = ret_map
.iter()
.map(|(ty, variants)| {
quote! {
impl Into<#ty> for #msg_return_enum_name {
fn into(self) -> #ty {
match self {
#( #msg_return_enum_name :: #variants (value) => value, )*
_ => unreachable!("handler returned unexpected type, this indicates the `Message` implementation is incorrect"),
}
}
}
}
});
quote! {
use #msgs_module :: #msg_enum_name;
pub mod #msgs_module {
pub enum #msg_enum_name {
#( #msg_idents ( #msg_idents ) ),*
}
pub enum #msg_return_enum_name {
#( #msg_idents ( #msg_rets ) ),*
}
#(
#msgs
)*
#(
impl From<#msg_idents> for #msg_enum_name {
fn from(value: #msg_idents) -> Self {
#msg_enum_name :: #msg_idents (value)
}
}
)*
#(
#ret_into
)*
impl<A> ::ludi::Message<A> for #msg_enum_name where
A: ::ludi::Actor,
#( A: ::ludi::Handler<#msg_idents, Return = #msg_rets>, )*
{
type Return = #msg_return_enum_name;
async fn handle<M: ::ludi::Mailbox<A>, R: FnOnce(Self::Return)>(
self,
actor: &mut A,
ctx: &mut ::ludi::Context<'_, A, M>,
ret: R,
) {
match self {
#(
#msg_enum_name :: #msg_idents (msg) => {
let value = #msg_return_enum_name :: #msg_idents (::ludi::Handler::<#msg_idents>::handle(actor, msg, ctx).await);
ret(value);
::ludi::Handler::<#msg_idents>::after(actor, ctx).await;
}
),*
};
}
}
}
}.into()
}

22
ludi-macros/src/lib.rs Normal file
View File

@@ -0,0 +1,22 @@
mod implement;
mod interface;
pub(crate) mod types;
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn interface(_attr: TokenStream, item: TokenStream) -> TokenStream {
let mut tokens = item.clone();
let item_trait = syn::parse_macro_input!(item as syn::ItemTrait);
tokens.extend(interface::impl_interface(item_trait));
tokens
}
#[proc_macro_attribute]
pub fn implement(_attr: TokenStream, item: TokenStream) -> TokenStream {
let item_impl = syn::parse_macro_input!(item as syn::ItemImpl);
implement::impl_implement(item_impl)
}

48
ludi-macros/src/types.rs Normal file
View File

@@ -0,0 +1,48 @@
use syn::{parse_quote, FnArg, Ident, Pat, ReturnType, Signature, Type};
#[derive(Clone)]
pub(crate) struct MethodSig {
pub ident: Ident,
pub args: Vec<(Ident, Type)>,
pub ret: Type,
}
impl From<Signature> for MethodSig {
fn from(sig: Signature) -> Self {
let Signature {
ident,
generics,
inputs,
output,
..
} = sig;
let ret = match output {
ReturnType::Default => parse_quote!(()),
ReturnType::Type(_, ty) => *ty,
};
if !generics.params.is_empty() {
panic!("generic methods are not supported");
}
let args = inputs
.into_iter()
.filter_map(|arg| {
let FnArg::Typed(pat) = arg else {
return None;
};
let ty = *pat.ty;
let Pat::Ident(pat) = *pat.pat else {
panic!("only support named arguments");
};
Some((pat.ident, ty))
})
.collect();
Self { ident, args, ret }
}
}

19
ludi/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
[package]
name = "ludi"
version = "0.1.0"
edition = "2021"
[features]
default = ["macros"]
macros = ["dep:ludi-macros"]
[dependencies]
ludi-core = { path = "../ludi-core" }
ludi-macros = { path = "../ludi-macros", optional = true }
[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
[[example]]
name = "simple"
required-features = ["macros"]

59
ludi/examples/simple.rs Normal file
View File

@@ -0,0 +1,59 @@
use ludi::{implement, interface, mailbox::FuturesMailbox, prelude::*};
// Slap `#[interface]` on a trait to generate a message types for it.
//
// This also generates a blanket implementation for all addresses of actors which implement
// all the `Handler` impls for the messages.
//
// The original trait is not modified in any way.
#[interface]
trait Counter {
/// Increment the counter by `increment` and return the new value.
async fn increment(&self, increment: usize) -> usize;
}
#[derive(Default)]
struct CounterBoi {
count: usize,
}
impl Actor for CounterBoi {
type Message = CounterMessage;
type Stop = ();
async fn stopped(&mut self) -> Self::Stop {}
}
// Implement a trait for an actor as if it were a normal implementation block.
//
// Code navigation works as expected, at least in VSCode. As in, you can jump to this
// implementation from the trait definition.
#[implement]
impl Counter for CounterBoi {
async fn increment(&self, increment: usize) -> usize {
self.count += increment;
self.count
}
}
#[tokio::main]
async fn main() {
let mailbox = FuturesMailbox::new();
let addr = mailbox.address().clone();
let mut actor = CounterBoi::default();
tokio::spawn(async move { actor.run(mailbox).await });
// Because of the blanket implementation, this actor's address implements
// the trait directly and can be used as normal.
//
// Also because it implements the actual trait documentation, highlighting,
// etc. works as expected.
let _count: usize = addr.increment(1).await;
// And can of course use the address as normal as well.
let _count: usize = addr.send(counter_msgs::Increment { increment: 1 }).await;
println!("adding: {}, result: {}", 2, addr.increment(2).await);
println!("adding: {}, result: {}", 3, addr.increment(3).await);
}

7
ludi/src/lib.rs Normal file
View File

@@ -0,0 +1,7 @@
pub use ludi_core::*;
#[cfg(feature = "macros")]
pub use ludi_macros::*;
pub mod prelude {
pub use ludi_core::{Actor, Address, Context, Handler, Mailbox};
}

2
rust-toolchain.toml Normal file
View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"