major refactor

This commit is contained in:
sinu
2024-01-08 19:03:36 -08:00
parent 1091d87512
commit 4ab528d7a7
37 changed files with 2543 additions and 584 deletions

View File

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

13
LICENSE-APACHE Normal file
View File

@@ -0,0 +1,13 @@
Copyright 2023 sinu <65924192+sinui0@users.noreply.github.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

19
LICENSE-MIT Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2023 sinu <65924192+sinui0@users.noreply.github.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.

123
README.md Normal file
View File

@@ -0,0 +1,123 @@
# ludi
A minimal async actor-like framework written in Rust.
## Overview
ludi is mostly a collection of traits which compose together to resemble an actor framework. It is not a full-featured actor framework for building massively concurrent applications deployed to horizontally scalable clusters, nor is it intended to be. Instead, **ludi focuses on providing a lightweight library specifically for asynchronously managing shared local state via message channels**. The provided abstractions support writing concurrent programs without directly relying on lock-based primitives and all the trickiness that comes with them.
Check out this blog post on [tokio actors](https://ryhl.io/blog/actors-with-tokio/) which serves as a nice introduction to this paradigm.
A pitfall of message-based synchronization, in this author's view, is the boilerplate that comes with it. To address this, ludi comes with (optional) macros which can be used to generate APIs which encapsulate the implementation details of message passing, and instead provide more ergonomic OOP-style interfaces (traits, methods). This approach was inspired by [`spaad`](https://github.com/Restioson/spaad), a crate built on [`xtra`](https://github.com/Restioson/xtra).
## Features
- Small
- Contains very little implementation code, mostly traits and helpers.
- The "batteries" that are included are feature gated, eg `futures-mailbox`.
- Ergonomic
- Generate APIs which resemble lock-based interfaces.
- Safe
- ludi is `#![deny(unsafe_code)]`
- Flexible
- Traits are public and low-level, extension crates can support more advanced features.
- ludi does not have to appear in your own API
- Executor agnostic
- Not coupled to a runtime such as `tokio`, everything is built on std primitives.
- Caveat: until RTN, async traits will include `Send` bounds
- Macros to kill boilerplate
- No magic, the boilerplate can be written by hand instead if that's your preference.
## Example
```rust
// Define an actor struct.
//
// We use the `Controller` macro to generate a controller for the actor.
#[derive(Default, ludi::Controller)]
struct CounterBoi {
count: usize,
}
impl ludi::Actor for CounterBoi {
type Stop = ();
type Error = ();
async fn stopped(&mut self) -> Result<Self::Stop, Self::Error> {
Ok(())
}
}
// Slap `#[interface]` on a trait to generate messages for it.
//
// The `msg(wrap)` attribute generates a wrapper message for the trait, called `CounterMsg`.
#[ludi::interface(msg(wrap))]
trait Counter {
/// Reset the counter to zero.
async fn reset(&self);
/// Return the current value of the counter.
fn count(&self) -> impl std::future::Future<Output = usize> + Send;
/// Increment the counter by `increment` and return the new value.
async fn increment(&self, increment: usize) -> usize;
}
// Implement the trait for the actor as if it were a normal implementation block.
//
// This generates handlers for all the `Counter` trait messages.
//
// We pass in the `ctrl` attribute so that the trait is implemented for the actor's
// controller.
#[ludi::implement(ctrl)]
impl Counter for CounterBoi {
async fn reset(&self) {
// `self` is mutable, despite the trait signature.
self.count = 0;
}
async fn count(&self) -> usize {
self.count
}
async fn increment(&self, increment: usize) -> usize {
self.count += increment;
self.count
}
}
#[tokio::main]
async fn main() {
// Create a mailbox and address for sending `CounterMsg` messages.
let (mut mailbox, addr) = ludi::FuturesMailbox::<CounterMsg>::new(100);
// Create a new actor.
let mut actor = CounterBoi::default();
// Create a controller for the actor using the address.
// This controller implements the `Counter` trait.
let ctrl = CounterBoi::controller(addr);
// Spawn the actor to run in the background. This works with any executor, not just tokio.
tokio::spawn(async move { ludi::run(&mut actor, &mut mailbox).await });
// Tada! No message passing present in the API!
let count = ctrl.increment(1).await;
assert_eq!(count, 1);
ctrl.reset().await;
assert_eq!(ctrl.count().await, 0);
let count = ctrl.increment(2).await;
assert_eq!(count, 2);
}
```
## License
All ludi crates are licensed under either of
- Apache License, Version 2.0
- MIT license
at your option.

View File

@@ -3,8 +3,11 @@ name = "ludi-core"
version = "0.1.0"
edition = "2021"
[dependencies]
futures = { version = "0.3" }
[features]
default = ["futures-mailbox"]
futures-mailbox = []
[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
[dependencies]
futures-core = { version = "0.3" }
futures-util = { version = "0.3", features = ["sink"] }
futures-channel = { version = "0.3", features = ["sink"] }

130
ludi-core/src/address.rs Normal file
View File

@@ -0,0 +1,130 @@
use std::future::Future;
use futures_util::Sink;
use crate::{Envelope, Message, MessageError, Wrap};
/// An address of a mailbox.
///
/// An address is used to send messages to a mailbox, which can be dispatched to an actor.
pub trait Address:
Sink<Envelope<Self::Message, <Self::Message as Message>::Return>, Error = MessageError>
+ Send
+ 'static
{
/// The type of message that can be sent to this address.
type Message: Message;
/// Sends a message and awaits a response.
fn send_await<T>(&self, msg: T) -> impl Future<Output = Result<T::Return, MessageError>> + Send
where
Self::Message: Wrap<T>,
T: Message;
/// Sends a message.
fn send<T>(&self, msg: T) -> impl Future<Output = Result<(), MessageError>> + Send
where
Self::Message: Wrap<T>,
T: Message;
}
#[cfg(feature = "futures-mailbox")]
pub(crate) mod futures_address {
use super::*;
use futures_channel::mpsc;
use futures_util::SinkExt;
use std::{pin::Pin, task::Poll};
/// A MPSC address implemented using channels from the [`futures_channel`](https://crates.io/crates/futures_channel) crate.
pub struct FuturesAddress<T: Message> {
pub(crate) send: mpsc::Sender<Envelope<T, T::Return>>,
}
impl<T: Message> Clone for FuturesAddress<T> {
fn clone(&self) -> Self {
Self {
send: self.send.clone(),
}
}
}
impl<T> Sink<Envelope<T, T::Return>> for FuturesAddress<T>
where
T: Message,
{
type Error = MessageError;
fn poll_ready(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(), Self::Error>> {
Pin::new(&mut self.send)
.poll_ready(cx)
.map_err(|_| MessageError::Closed)
}
fn start_send(
mut self: Pin<&mut Self>,
item: Envelope<T, T::Return>,
) -> Result<(), Self::Error> {
Pin::new(&mut self.send)
.start_send(item)
.map_err(|_| MessageError::Closed)
}
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(), Self::Error>> {
Pin::new(&mut self.send)
.poll_flush(cx)
.map_err(|_| MessageError::Closed)
}
fn poll_close(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(), Self::Error>> {
Pin::new(&mut self.send)
.poll_close(cx)
.map_err(|_| MessageError::Closed)
}
}
impl<T> Address for FuturesAddress<T>
where
T: Message,
{
type Message = T;
async fn send_await<U>(&self, msg: U) -> Result<U::Return, MessageError>
where
Self::Message: Wrap<U>,
U: Message,
{
let (env, ret) = Envelope::new_returning(msg.into());
self.send
.clone()
.send(env)
.await
.map_err(|_| MessageError::Closed)?;
let ret = ret.await.map_err(|_| MessageError::Interrupted)?;
Self::Message::unwrap_return(ret)
}
async fn send<U>(&self, msg: U) -> Result<(), MessageError>
where
Self::Message: Wrap<U>,
U: Message,
{
self.send
.clone()
.send(Envelope::new(msg.into()))
.await
.map_err(|_| MessageError::Closed)
}
}
}

View File

@@ -1,53 +1,56 @@
use std::marker::PhantomData;
use futures_channel::oneshot::{channel, Receiver, Sender};
use futures::channel::oneshot::{self, Receiver, Sender};
use crate::{Actor, Context, Dispatch, Message};
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> {
/// An envelope containing a message and optionally a channel which can be
/// used to return a value back to the sender.
pub struct Envelope<M, R> {
msg: M,
send: Option<Sender<R>>,
_pd: PhantomData<A>,
}
impl<M, R, A> Envelope<M, R, A> {
impl<M, R> Envelope<M, R> {
/// Create a new envelope.
pub fn new(msg: M) -> Self {
Self {
msg,
send: None,
_pd: PhantomData,
}
Self { msg, send: None }
}
/// Create a new envelope with a channel which can be used to return
/// a response to the sender.
pub fn new_returning(msg: M) -> (Self, Receiver<R>) {
let (send, recv) = oneshot::channel();
let (send, recv) = channel();
(
Self {
msg,
send: Some(send),
_pd: PhantomData,
},
recv,
)
}
}
impl<M, R, A> Envelope<M, R, A>
impl<M, R> Envelope<M, R>
where
A: Actor,
M: Message<A, Return = R> + Send + 'static,
R: Send + 'static,
M: Message<Return = R>,
R: Send,
{
pub async fn handle<T: Mailbox<A>>(mut self, actor: &mut A, ctx: &mut Context<'_, A, T>) {
/// Dispatches the message and return channel to the actor for handling.
///
/// # Arguments
///
/// * `actor` - The actor which will handle the message.
/// * `ctx` - The context of the actor.
pub async fn dispatch<A>(mut self, actor: &mut A, ctx: &mut Context<A>)
where
A: Actor,
M: Dispatch<A>,
{
self.msg
.handle(actor, ctx, move |ret| {
.dispatch(actor, ctx, move |ret| {
if let Some(send) = self.send.take() {
let _ = send.send(ret);
}
})
.await;
.await
}
}

25
ludi-core/src/error.rs Normal file
View File

@@ -0,0 +1,25 @@
use std::fmt::Display;
/// Errors that can occur when sending a message.
#[derive(Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum MessageError {
/// The mailbox has been closed.
Closed,
/// Handling of the message was interrupted.
Interrupted,
/// Error occurred while wrapping a message.
Wrapper,
}
impl Display for MessageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MessageError::Closed => write!(f, "mailbox closed"),
MessageError::Interrupted => write!(f, "message handling interrupted"),
MessageError::Wrapper => write!(f, "wrapper error"),
}
}
}
impl std::error::Error for MessageError {}

View File

@@ -1,115 +1,256 @@
//! Core types and traits for the ludi library.
#![deny(unsafe_code)]
#![deny(unused_must_use)]
#![deny(missing_docs)]
#![deny(unreachable_pub)]
#![deny(clippy::all)]
pub mod envelope;
pub mod mailbox;
mod address;
mod envelope;
mod error;
mod mailbox;
use std::marker::PhantomData;
use envelope::ActorEnvelope;
use futures::{Future, Stream, StreamExt};
use futures_util::StreamExt;
use std::future::Future;
pub use address::Address;
pub use envelope::Envelope;
pub use error::MessageError;
pub use mailbox::{IntoMail, IntoMailbox, Mailbox};
pub trait Message<A: Actor>: Send + Sized + 'static {
#[cfg(feature = "futures-mailbox")]
pub use address::futures_address::FuturesAddress;
#[cfg(feature = "futures-mailbox")]
pub use mailbox::futures_mailbox::{mailbox, FuturesMailbox};
/// A message type.
pub trait Message: Send + 'static {
/// The return value of the message.
type Return: Send + 'static;
}
fn handle<M: Mailbox<A>, R: FnOnce(Self::Return) + Send>(
/// A message which can wrap another type of message.
pub trait Wrap<T: Message>: From<T> + Message {
/// Unwraps the return value of the message.
fn unwrap_return(ret: Self::Return) -> Result<T::Return, MessageError>;
}
impl<T: Message> Wrap<T> for T {
fn unwrap_return(ret: Self::Return) -> Result<T::Return, MessageError> {
Ok(ret)
}
}
/// A message which can be dispatched to an actor.
pub trait Dispatch<A: Actor>: Message {
/// Dispatches the message and return channel to the actor for handling.
///
/// # Arguments
///
/// * `actor` - The actor which will handle the message.
/// * `ctx` - The context of the actor.
/// * `ret` - A channel which returns a value to the caller.
fn dispatch<R: FnOnce(Self::Return) + Send>(
self,
actor: &mut A,
ctx: &mut Context<A, M>,
ctx: &mut Context<A>,
ret: R,
) -> impl Future<Output = ()> + Send;
}
/// An actor.
///
/// # Message Type
///
/// Each actor must specify the type of message it can handle. See the [`Message`] trait for
/// more information and examples.
///
/// # Start
///
/// When an actor is first started, the [`Actor::started`] method will be called. By default this method
/// does nothing, but it can be overridden to perform any initialization required by the actor.
///
/// # Stop
///
/// When an actor receives a stop signal it will stop processing messages and the [`Actor::stopped`] method
/// will be called before returning.
///
/// # Example
///
/// ```ignore
/// # use ludi_core::*;
/// struct PingActor;
///
/// impl Actor for PingActor {
/// type Stop = ();
///
/// async fn stopped(&mut self) -> Self::Stop {
/// println!("actor stopped");
/// }
/// }
/// ```
pub trait Actor: Send + Sized + 'static {
type Message: Message<Self>;
/// The type of value returned when this actor is stopped.
type Stop;
/// The type of error which may occur during handling.
type Error: Send + 'static;
fn started(
&mut self,
_ctx: &mut Context<'_, Self, impl Mailbox<Self>>,
) -> Result<(), Self::Stop> {
/// A method which can be overridden to perform any initialization required by the
/// actor during startup.
fn started(&mut self, _ctx: &mut Context<Self>) -> Result<(), Self::Error> {
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
}
}
/// A method which is called when the actor receives a stop signal.
fn stopped(&mut self) -> impl Future<Output = Result<Self::Stop, Self::Error>> + Send;
}
pub trait Handler<T>: Actor {
type Return: Send + 'static;
/// An actor that can handle a message.
///
/// When an actor receives a message it is passed to its' handler which
/// processes the message and optionally returns a value to the caller.
///
/// For extra control over how a message is handled, see [`Handler::process`].
pub trait Handler<T: Message>: Actor {
/// Handle a message and return a value to the caller.
///
/// # Arguments
///
/// * `msg` - The message to handle.
/// * `ctx` - The actor's execution context.
fn handle(&mut self, msg: T, ctx: &mut Context<Self>)
-> impl Future<Output = T::Return> + Send;
fn handle<M: Mailbox<Self>>(
/// Handle a message and return a value to the caller. This method is similar to [`Handler::handle`]
/// except that it gives more control over how the message is handled.
///
/// By default, this method simply calls [`Handler::handle`] and returns the value back to the caller.
///
/// # Arguments
///
/// * `msg` - The message to handle.
/// * `ctx` - The actor's execution context.
/// * `ret` - A channel which returns a value to the caller.
///
/// # Defer handling
///
/// Ownership of the return channel `ret` is provided to this method. This allows the
/// actor to defer handling of the message until later, or to send the message to another
/// thread for processing without blocking the actor.
///
/// # Post processing
///
/// It may be useful to perform post-processing after a message has been handled. This can be
/// done by overriding this method and performing work after the value has been sent back to
/// the caller.
fn process<R: FnOnce(T::Return) + Send>(
&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>,
ctx: &mut Context<Self>,
ret: R,
) -> impl Future<Output = ()> + Send {
async {}
async move { ret(self.handle(msg, ctx).await) }
}
}
pub trait Mailbox<A: Actor>: Stream<Item = ActorEnvelope<A>> + Send + Unpin + 'static {
type Address: Address<A>;
/// A controller for an actor.
pub trait Controller {
/// The type of actor that this controller controls.
type Actor: Actor;
/// The type of address that this controller uses to send messages to the actor.
type Address: Address<Message = Self::Message>;
/// The type of message that this controller can send to the actor.
type Message: Message + Dispatch<Self::Actor>;
/// Returns the address of the actor.
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;
/// An actor's execution context.
pub struct Context<A: Actor> {
stopped: bool,
err: Option<A::Error>,
}
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 {
impl<A: Actor> Default for Context<A> {
fn default() -> Self {
Self {
running: true,
mailbox,
_pd: PhantomData,
stopped: Default::default(),
err: Default::default(),
}
}
}
impl<A: Actor> Context<A> {
/// Signals to the actor that it should stop processing messages.
pub fn stop(&mut self) {
self.stopped = true;
}
/// Returns `true` if the actor has received a stop signal.
pub fn stopped(&self) -> bool {
self.stopped
}
/// Returns an error if one has occurred.
pub fn error(&self) -> Option<&A::Error> {
self.err.as_ref()
}
/// Propagates an error to the actor context.
pub fn set_error(&mut self, err: A::Error) {
self.err = Some(err);
}
/// Takes an error if one has occurred.
pub fn take_error(&mut self) -> Option<A::Error> {
self.err.take()
}
/// Executes a fallible function and propagates any errors to the context.
pub async fn try_or_stop<
F: FnOnce(&mut Self) -> Fut,
Fut: Future<Output = Result<Ok, A::Error>>,
Ok,
>(
&mut self,
f: F,
) -> Option<Ok> {
match f(self).await {
Ok(ok) => Some(ok),
Err(err) => {
self.err = Some(err);
None
}
}
}
}
/// Runs an actor until it receives a stop signal or an error occurs.
///
/// # Arguments
///
/// * `actor` - The actor to run.
/// * `mailbox` - The mailbox which will be used to receive messages.
pub async fn run<A, M>(actor: &mut A, mailbox: &mut M) -> Result<A::Stop, A::Error>
where
A: Actor,
M: Mailbox,
M::Message: Dispatch<A>,
{
let mut ctx = Context::default();
actor.started(&mut ctx)?;
while let Some(env) = mailbox.next().await {
env.dispatch(actor, &mut ctx).await;
if let Some(err) = ctx.take_error() {
return Err(err);
} else if ctx.stopped() {
break;
}
}
pub fn mailbox(&mut self) -> &mut M {
self.mailbox
}
pub fn stop(&mut self) {
self.running = false;
}
pub fn stopped(&self) -> bool {
!self.running
}
actor.stopped().await
}

View File

@@ -1,71 +1,133 @@
use futures::{channel::mpsc, SinkExt, Stream};
use std::{pin::Pin, task::Poll};
use crate::{Actor, Address, Envelope, Handler, Mailbox, Message};
use futures_core::Stream;
pub struct FuturesMailbox<A: Actor> {
addr: FuturesAddress<A>,
recv: mpsc::Receiver<Envelope<A::Message, <A::Message as Message<A>>::Return, A>>,
use crate::{Envelope, Message};
/// A mailbox.
///
/// A mailbox is an asynchronous stream of messages which can be dispatched to an actor. The counter-part
/// to a mailbox is an [`Address`](crate::Address), which is used to send messages.
pub trait Mailbox:
Stream<Item = Envelope<Self::Message, <Self::Message as Message>::Return>> + Send + Unpin + 'static
{
/// The type of message that can be sent to this mailbox.
type Message: Message;
}
impl<A: Actor> FuturesMailbox<A> {
pub fn new() -> Self {
let (send, recv) = mpsc::channel(100);
impl<T, U> Mailbox for T
where
T: Stream<Item = Envelope<U, <U as Message>::Return>> + Send + Unpin + 'static,
U: Message,
{
type Message = U;
}
Self {
addr: FuturesAddress { send },
recv,
}
/// An extension trait which converts a stream of messages into a mailbox.
pub trait IntoMailbox {
/// The type of message that can be sent to the mailbox.
type Message: Message;
/// The type of mailbox.
type IntoMail: Mailbox<Message = Self::Message>;
/// Convert self into a mailbox.
fn into_mailbox(self) -> Self::IntoMail;
}
impl<T> IntoMailbox for T
where
T: Stream + Send + Unpin + 'static,
T::Item: Message,
{
type Message = T::Item;
type IntoMail = IntoMail<T>;
fn into_mailbox(self) -> Self::IntoMail {
IntoMail(self)
}
}
impl<A: Actor> Mailbox<A> for FuturesMailbox<A> {
type Address = FuturesAddress<A>;
/// Adapter returned from [`IntoMailbox::into_mailbox`].
///
/// Used to convert a stream of messages into a mailbox.
pub struct IntoMail<T>(T);
fn address(&self) -> &Self::Address {
&self.addr
impl<T> IntoMail<T> {
/// Returns the inner stream.
pub fn to_inner(self) -> T {
self.0
}
/// Returns a reference to the inner stream.
pub fn inner_ref(&self) -> &T {
&self.0
}
/// Returns a mutable reference to the inner stream.
pub fn inner_mut(&mut self) -> &mut T {
&mut self.0
}
}
impl<A: Actor> Stream for FuturesMailbox<A> {
type Item = Envelope<A::Message, <A::Message as Message<A>>::Return, A>;
impl<T> Stream for IntoMail<T>
where
T: Stream + Send + Unpin + 'static,
T::Item: Message,
{
type Item = Envelope<T::Item, <T::Item as Message>::Return>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
std::pin::Pin::new(&mut self.recv).poll_next(cx)
) -> Poll<Option<Self::Item>> {
Stream::poll_next(Pin::new(&mut self.get_mut().0), cx).map(|m| m.map(|m| Envelope::new(m)))
}
}
pub struct FuturesAddress<A: Actor> {
send: mpsc::Sender<Envelope<A::Message, <A::Message as Message<A>>::Return, A>>,
}
#[cfg(feature = "futures-mailbox")]
pub(crate) mod futures_mailbox {
use super::*;
use crate::FuturesAddress;
use futures_channel::mpsc;
impl<A: Actor> Clone for FuturesAddress<A> {
fn clone(&self) -> Self {
Self {
send: self.send.clone(),
/// Returns a new mailbox and its' address.
///
/// # Arguments
///
/// * `capacity` - The number of messages that can be buffered in the mailbox.
pub fn mailbox<T>(capacity: usize) -> (FuturesMailbox<T>, FuturesAddress<T>)
where
T: Message,
{
FuturesMailbox::new(capacity)
}
/// A MPSC mailbox implemented using channels from the [`futures_channel`](https://crates.io/crates/futures_channel) crate.
pub struct FuturesMailbox<T: Message> {
recv: mpsc::Receiver<Envelope<T, T::Return>>,
}
impl<T: Message> FuturesMailbox<T> {
/// Create a new mailbox, returning the mailbox and its' address.
///
/// # Arguments
///
/// * `buffer` - The number of messages that can be buffered in the mailbox.
pub fn new(buffer: usize) -> (Self, FuturesAddress<T>) {
let (send, recv) = mpsc::channel(buffer);
(Self { recv }, FuturesAddress { send })
}
}
impl<T: Message> Stream for FuturesMailbox<T> {
type Item = Envelope<T, T::Return>;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Option<Self::Item>> {
Pin::new(&mut self.recv).poll_next(cx)
}
}
}
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()
}
}

View File

@@ -1,119 +0,0 @@
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;
}

View File

@@ -0,0 +1,34 @@
[package]
name = "ludi-macros-test"
version = "0.0.0"
edition = "2021"
[dependencies]
ludi = { path = "../ludi" }
[dev-dependencies]
ludi-macros = { path = "../ludi-macros" }
[[test]]
name = "message_struct"
path = "tests/message_struct.rs"
[[test]]
name = "message_struct_unit"
path = "tests/message_struct_unit.rs"
[[test]]
name = "message_enum"
path = "tests/message_enum.rs"
[[test]]
name = "interface_basic"
path = "tests/interface_basic.rs"
[[test]]
name = "implement_basic"
path = "tests/implement_basic.rs"
[[test]]
name = "implement_trait"
path = "tests/implement_trait.rs"

View File

@@ -0,0 +1,19 @@
pub fn assert_message<T, U>()
where
T: ludi::Message<Return = U>,
{
}
pub fn assert_wrap<T, U>()
where
T: ludi::Wrap<U>,
U: ludi::Message,
{
}
pub fn assert_handler<T, U>()
where
T: ludi::Handler<U>,
U: ludi::Message,
{
}

View File

@@ -0,0 +1,35 @@
#![allow(dead_code)]
use ludi_macros_test::*;
#[derive(Default, ludi::Controller)]
pub struct CounterBoi {
count: u32,
}
impl ludi::Actor for CounterBoi {
type Stop = ();
type Error = ();
async fn stopped(&mut self) -> Result<Self::Stop, Self::Error> {
Ok(())
}
}
#[ludi::implement]
#[ctrl]
#[msg(wrap)]
impl CounterBoi {
pub async fn increment(&mut self) -> u32 {
self.count += 1;
self.count
}
}
#[test]
fn test() {
assert_message::<CounterBoiMsg, CounterBoiMsgReturn>();
assert_message::<CounterBoiMsgIncrement, u32>();
assert_wrap::<CounterBoiMsg, CounterBoiMsgIncrement>();
assert_handler::<CounterBoi, CounterBoiMsgIncrement>();
}

View File

@@ -0,0 +1,59 @@
#![allow(dead_code)]
use ludi_macros_test::*;
use std::future::Future;
#[derive(Default)]
pub struct Foo;
impl ludi::Actor for Foo {
type Stop = ();
type Error = ();
async fn stopped(&mut self) -> Result<Self::Stop, Self::Error> {
Ok(())
}
}
#[ludi::interface(msg(wrap))]
trait Bar {
fn foo(&mut self) -> impl Future<Output = u32>;
async fn bar(&self) -> String;
fn baz(&self) -> impl Future<Output = ()> + Send;
}
#[ludi::implement]
impl Bar for Foo {
#[msg(skip_handler)]
async fn foo(&mut self) -> u32 {
unimplemented!()
}
async fn bar(&self) -> String {
unimplemented!()
}
async fn baz(&self) {}
}
impl ludi::Handler<BarMsgFoo> for Foo {
async fn handle(&mut self, _msg: BarMsgFoo, _ctx: &mut ludi::prelude::Context<Self>) -> u32 {
todo!()
}
}
#[test]
fn test_implement_trait() {
assert_message::<BarMsg, BarMsgReturn>();
assert_message::<BarMsgFoo, u32>();
assert_message::<BarMsgBar, String>();
assert_message::<BarMsgBaz, ()>();
assert_wrap::<BarMsg, BarMsgFoo>();
assert_wrap::<BarMsg, BarMsgBar>();
assert_wrap::<BarMsg, BarMsgBaz>();
assert_handler::<Foo, BarMsgFoo>();
assert_handler::<Foo, BarMsgBar>();
assert_handler::<Foo, BarMsgBaz>();
}

View File

@@ -0,0 +1,21 @@
#![allow(dead_code, unreachable_code)]
use ludi_macros_test::*;
#[ludi::interface(msg(wrap))]
pub trait Foo {
#[msg(name = "FooMethod")]
fn foo(&self, msg: String) -> impl std::future::Future<Output = String>;
#[allow(async_fn_in_trait)]
async fn bar(&self);
}
#[test]
fn test() {
assert_message::<FooMsg, FooMsgReturn>();
assert_message::<FooMethod, String>();
assert_message::<FooMsgBar, ()>();
assert_wrap::<FooMsg, FooMethod>();
assert_wrap::<FooMsg, FooMsgBar>();
}

View File

@@ -0,0 +1,28 @@
#![allow(dead_code)]
use ludi_macros_test::*;
mod msg {
#[derive(ludi::Message)]
#[ludi(return_ty = usize)]
pub struct Foo;
}
#[derive(ludi::Message)]
#[ludi(return_ty = String)]
struct Bar {
bar: String,
}
#[derive(ludi::Wrap)]
enum CounterMessage {
Foo(msg::Foo),
Bar(Bar),
}
#[test]
fn test() {
assert_message::<CounterMessage, CounterMessageReturn>();
assert_wrap::<CounterMessage, msg::Foo>();
assert_wrap::<CounterMessage, Bar>();
}

View File

@@ -0,0 +1,14 @@
#![allow(dead_code)]
use ludi_macros_test::*;
#[derive(ludi::Message)]
#[ludi(return_ty = usize)]
struct Foo {
foo: usize,
}
#[test]
fn test() {
assert_message::<Foo, usize>();
}

View File

@@ -0,0 +1,12 @@
#![allow(dead_code)]
use ludi_macros_test::*;
#[derive(ludi::Message)]
#[ludi(return_ty = usize)]
struct Foo;
#[test]
fn test() {
assert_message::<Foo, usize>();
}

View File

@@ -7,7 +7,9 @@ edition = "2021"
proc-macro = true
[dependencies]
syn = { version = "1.0", features = ["full", "extra-traits", "visit"] }
syn = { version = "2.0", features = ["full", "extra-traits", "visit"] }
quote = "1.0"
proc-macro2 = "1.0"
heck = "0.4.1"
heck = "0.4"
proc-macro-error = "1.0"
darling = "0.20"

View File

@@ -0,0 +1,86 @@
use proc_macro_error::abort;
use quote::quote;
use crate::utils::ctrl_ident;
pub(crate) fn impl_controller(input: syn::DeriveInput) -> proc_macro2::TokenStream {
let actor_ident = &input.ident;
let vis = &input.vis;
let ctrl_ident = ctrl_ident(actor_ident);
if let Some(param) = input
.generics
.type_params()
.find(|param| param.ident == "A")
{
abort!(
param.ident,
"generic type parameter `A` is reserved for the controller's address"
);
}
let (actor_impl_generics, actor_ty_generics, actor_where_clause) =
input.generics.split_for_impl();
let mut generics = input.generics.clone();
generics.params.push(syn::parse_quote!(A));
let where_clause = generics.make_where_clause();
where_clause
.predicates
.push(syn::parse_quote!(A: ::ludi::Address));
where_clause
.predicates
.push(syn::parse_quote!(<A as ::ludi::Address>::Message: ::ludi::Dispatch<#actor_ident #actor_ty_generics>));
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let fields = if input.generics.params.is_empty() {
quote!(addr: A)
} else {
quote!(addr: A, _pd: std::marker::PhantomData #actor_ty_generics)
};
let from_fields = if input.generics.params.is_empty() {
quote!(addr)
} else {
quote!(addr, _pd: std::marker::PhantomData)
};
quote!(
#[derive(Debug, Clone)]
#vis struct #ctrl_ident #ty_generics {
#fields
}
impl #actor_impl_generics #actor_ident #actor_ty_generics #actor_where_clause {
pub fn controller<A>(addr: A) -> #ctrl_ident #ty_generics
where
A: ::ludi::Address,
<A as ::ludi::Address>::Message: ::ludi::Dispatch<Self>,
{
#ctrl_ident ::from(addr)
}
}
impl #impl_generics From<A> for #ctrl_ident #ty_generics #actor_where_clause
{
fn from(addr: A) -> Self {
Self {
#from_fields
}
}
}
impl #ty_generics ::ludi::Controller for #ctrl_ident #ty_generics #where_clause
{
type Actor = #actor_ident #actor_ty_generics;
type Address = A;
type Message = <A as ::ludi::Address>::Message;
fn address(&self) -> &Self::Address {
&self.addr
}
}
)
}

View File

@@ -1,71 +1,35 @@
use heck::{ToSnakeCase, ToUpperCamelCase};
use quote::quote;
use syn::{parse_str, Ident, ItemImpl, Path};
use darling::util::Override;
use darling::{ast::NestedMeta, Error, FromMeta};
use proc_macro::TokenStream;
use crate::types::MethodSig;
use crate::items::ItemImpl;
use crate::options::{CtrlOptions, MsgOptions};
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()
#[derive(FromMeta)]
pub(crate) struct ImplementAttr {
pub msg: Option<Override<MsgOptions>>,
pub ctrl: Option<Override<CtrlOptions>>,
}
pub(crate) fn impl_implement(attr: TokenStream, item: syn::ItemImpl) -> proc_macro2::TokenStream {
let attr_args = match NestedMeta::parse_meta_list(attr.into()) {
Ok(v) => v,
Err(e) => {
return Error::from(e).write_errors();
}
};
let ImplementAttr { msg, ctrl } = match ImplementAttr::from_list(&attr_args) {
Ok(v) => v,
Err(e) => {
return e.write_errors();
}
};
let msg_options = msg.map(|msg_options| msg_options.unwrap_or_default());
let ctrl_options = ctrl.map(|ctrl_options| ctrl_options.unwrap_or_default());
let item = ItemImpl::from_item_impl(&item, msg_options, ctrl_options);
item.expand()
}

View File

@@ -1,133 +1,51 @@
use std::collections::HashMap;
use darling::{ast::NestedMeta, Error, FromMeta};
use heck::{ToSnakeCase, ToUpperCamelCase};
use proc_macro2::Span;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_str, Ident, ItemTrait, Path, Type};
use crate::types::MethodSig;
use crate::items::ItemTrait;
use crate::options::MsgOptions;
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();
#[derive(FromMeta)]
pub(crate) struct InterfaceAttr {
pub msg: Option<MsgOptions>,
}
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);
pub(crate) fn impl_interface(
attr: TokenStream,
mut item: syn::ItemTrait,
) -> proc_macro2::TokenStream {
let attr_args = match NestedMeta::parse_meta_list(attr.into()) {
Ok(v) => v,
Err(e) => {
return Error::from(e).write_errors();
}
};
if let Some(variants) = ret_map.get_mut(&ret) {
variants.push(ident);
} else {
ret_map.insert(ret.clone(), vec![ident]);
let InterfaceAttr { msg } = match InterfaceAttr::from_list(&attr_args) {
Ok(v) => v,
Err(e) => {
return e.write_errors();
}
};
msg_rets.push(ret);
let mut expanded_tokens = proc_macro2::TokenStream::new();
{
let item = ItemTrait::from_item_trait(&item, msg);
expanded_tokens.extend(item.expand());
}
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;
}
),*
};
}
}
item.items.iter_mut().for_each(|item| {
if let syn::TraitItem::Fn(f) = item {
f.attrs.retain(|attr| !attr.path().is_ident("msg"));
}
}.into()
});
quote!(
#item
#expanded_tokens
)
}

View File

@@ -0,0 +1,294 @@
use darling::usage::{GenericsExt, IdentSet};
use proc_macro2::{Span, TokenStream};
use proc_macro_error::{abort, emit_error};
use quote::quote;
use syn::{parse_quote, spanned::Spanned};
use crate::{
items::method::Method,
options::{CtrlOptions, MsgOptions, WrapOptions},
utils::ctrl_ident,
};
pub(crate) struct ItemImpl {
msg_options: Option<MsgOptions>,
ctrl_options: Option<CtrlOptions>,
impl_trait: Option<ImplTrait>,
impl_generics: syn::Generics,
actor_ident: syn::Ident,
actor_path: syn::Path,
actor_generic_args: Option<syn::AngleBracketedGenericArguments>,
methods: Vec<Method>,
}
pub(crate) struct ImplTrait {
trait_ident: syn::Ident,
trait_path: syn::Path,
}
impl ItemImpl {
pub(crate) fn from_item_impl(
item: &syn::ItemImpl,
mut msg_options: Option<MsgOptions>,
mut ctrl_options: Option<CtrlOptions>,
) -> Self {
let item_msg_options = MsgOptions::maybe_from_attributes(&item.attrs);
if let Some(item_msg_options) = item_msg_options {
if let Some(msg_options) = msg_options.as_mut() {
msg_options.merge(&item_msg_options);
} else {
msg_options = Some(item_msg_options);
}
}
let item_ctrl_options = CtrlOptions::maybe_from_attributes(&item.attrs);
if let Some(item_ctrl_options) = item_ctrl_options {
if let Some(ctrl_options) = ctrl_options.as_mut() {
ctrl_options.merge(&item_ctrl_options);
} else {
ctrl_options = Some(item_ctrl_options);
}
}
let syn::Type::Path(syn::TypePath {
path: actor_path, ..
}) = *(item.self_ty).clone()
else {
abort!(item.self_ty, "expected path to actor type");
};
let actor_segment = actor_path.segments.last().expect("actor path is non-empty");
let actor_ident = actor_segment.ident.clone();
let actor_generic_args = match &actor_segment.arguments {
syn::PathArguments::None => None,
syn::PathArguments::AngleBracketed(args) => Some(args.clone()),
syn::PathArguments::Parenthesized(_) => {
abort!(actor_segment.arguments, "unexpected parenthesis arguments")
}
};
let impl_trait = if let Some((_, trait_path, _)) = item.trait_.clone() {
let trait_segment = trait_path.segments.last().expect("trait path is non-empty");
let trait_ident = trait_segment.ident.clone();
Some(ImplTrait {
trait_ident,
trait_path,
})
} else {
None
};
let parent_ident = if let Some(impl_trait) = &impl_trait {
&impl_trait.trait_ident
} else {
&actor_ident
};
let type_params = item.generics.declared_type_params();
let methods = item
.items
.iter()
.filter_map(|item| {
match item {
syn::ImplItem::Fn(f) => return Some(f),
syn::ImplItem::Const(_) => {
// TODO: support associated consts
emit_error!(item, "const items are not supported");
}
syn::ImplItem::Type(_) => {
// TODO: support associated types
emit_error!(item, "associated types are not supported");
}
_ => {
emit_error!(item, "only methods are supported");
}
};
None
})
.map(|method| {
Method::new(
parent_ident,
&type_params,
msg_options.clone(),
ctrl_options.clone(),
method.span(),
method.attrs.clone(),
method.vis.clone(),
method.sig.clone(),
Some(method.block.clone()),
)
})
.collect::<Vec<_>>();
Self {
msg_options,
ctrl_options,
impl_trait,
impl_generics: item.generics.clone(),
actor_ident,
actor_path,
actor_generic_args,
methods,
}
}
fn expand_messages(&self) -> TokenStream {
let mut tokens = TokenStream::new();
for method in &self.methods {
tokens.extend(method.expand_message());
}
tokens
}
fn expand_wrap(&self) -> TokenStream {
let Some(WrapOptions { attrs, name }) = self
.msg_options
.as_ref()
.map(|opts| opts.wrap.clone().map(|opts| opts.unwrap_or_default()))
.flatten()
else {
return TokenStream::new();
};
let attrs = attrs
.as_ref()
.map(|attrs| attrs.clone().to_vec())
.unwrap_or_default();
let wrap_ident = name.clone().unwrap_or_else(|| {
syn::Ident::new(
&format!("{}Msg", self.actor_ident.to_string()),
Span::call_site(),
)
});
let mut wrap_type_params = IdentSet::default();
let mut variants = Vec::with_capacity(self.methods.len());
for method in &self.methods {
wrap_type_params.extend(method.type_params.clone());
let variant_ident = &method.struct_ident;
let struct_ident = &method.struct_ident;
let type_params = method.type_params.iter();
variants.push(quote!(
#variant_ident (#struct_ident<#(#type_params),*>)
));
}
let wrap_type_params = wrap_type_params.iter();
quote!(
#[derive(::ludi::Wrap)]
#(#[#attrs])*
pub enum #wrap_ident<#(#wrap_type_params),*> {
#(#variants),*
}
)
}
fn expand_handlers(&self) -> TokenStream {
let mut tokens = TokenStream::new();
for method in &self.methods {
tokens.extend(method.expand_handler(&self.actor_path, &self.impl_generics));
}
tokens
}
fn expand_ctrl(&self) -> TokenStream {
let Self {
ctrl_options,
actor_path,
..
} = self;
if self
.methods
.iter()
.all(|method| method.ctrl_options.is_none())
{
return TokenStream::new();
}
let mut ctrl_path = ctrl_options
.as_ref()
.map(|opts| opts.path.clone())
.flatten()
.unwrap_or_else(|| syn::Path::from(ctrl_ident(&self.actor_ident)));
if let Some(generic_args) = &self.actor_generic_args {
let mut generic_args = generic_args.clone();
generic_args.args.push(parse_quote!(A));
ctrl_path.segments.last_mut().unwrap().arguments =
syn::PathArguments::AngleBracketed(generic_args);
} else {
ctrl_path.segments.last_mut().unwrap().arguments =
syn::PathArguments::AngleBracketed(parse_quote!(<A,>));
}
let mut generics = self.impl_generics.clone();
generics.params.push(parse_quote!(A));
let where_clause = generics.make_where_clause();
where_clause
.predicates
.push(parse_quote!(A: Send + Sync + 'static));
where_clause
.predicates
.push(parse_quote!(Self: ::ludi::Controller<Actor = #actor_path>));
if let Some(ImplTrait { trait_path, .. }) = &self.impl_trait {
self.methods.iter().for_each(|method| {
let struct_path = &method.struct_path;
where_clause.predicates.push(
parse_quote!(<Self as ::ludi::Controller>::Message: ::ludi::Wrap<#struct_path>),
);
});
let methods = self.methods.iter().map(|method| method.expand_ctrl(true));
let (impl_generics, _, where_clause) = generics.split_for_impl();
quote!(
impl #impl_generics #trait_path for #ctrl_path #where_clause {
#(#methods)*
}
)
} else {
let impl_blocks = self.methods.iter().map(|method| {
let struct_path = &method.struct_path;
let impl_method = method.expand_ctrl(false);
let mut generics = generics.clone();
let where_clause = generics.make_where_clause();
where_clause.predicates.push(
parse_quote!(<Self as ::ludi::Controller>::Message: ::ludi::Wrap<#struct_path>),
);
let (impl_generics, _, where_clause) = generics.split_for_impl();
quote!(
impl #impl_generics #ctrl_path #where_clause {
#impl_method
}
)
});
quote!(
#(#impl_blocks)*
)
}
}
pub(crate) fn expand(&self) -> TokenStream {
let mut tokens = TokenStream::new();
if self.impl_trait.is_none() {
tokens.extend(self.expand_messages());
tokens.extend(self.expand_wrap());
}
tokens.extend(self.expand_handlers());
tokens.extend(self.expand_ctrl());
tokens
}
}

View File

@@ -0,0 +1,126 @@
use darling::usage::{GenericsExt, IdentSet};
use proc_macro2::{Span, TokenStream};
use proc_macro_error::emit_error;
use quote::quote;
use syn::spanned::Spanned;
use crate::items::method::Method;
use crate::options::{MsgOptions, WrapOptions};
pub(crate) struct ItemTrait {
msg_options: Option<MsgOptions>,
ident: syn::Ident,
vis: syn::Visibility,
methods: Vec<Method>,
}
impl ItemTrait {
pub(crate) fn from_item_trait(item: &syn::ItemTrait, msg_options: Option<MsgOptions>) -> Self {
if item.generics.lifetimes().count() > 0 {
emit_error!(item.generics, "trait can not be generic over lifetimes");
}
let type_params = item.generics.declared_type_params();
let methods = item
.items
.iter()
.filter_map(|item| {
match item {
syn::TraitItem::Fn(f) => return Some(f),
syn::TraitItem::Const(_) => {
// TODO: support associated consts
emit_error!(item, "const items are not supported");
}
syn::TraitItem::Type(_) => {
// TODO: support associated types
emit_error!(item, "associated types are not supported");
}
_ => {
emit_error!(item, "only methods are supported");
}
};
None
})
.map(|method| {
Method::new(
&item.ident,
&type_params,
msg_options.clone(),
None,
method.span(),
method.attrs.clone(),
item.vis.clone(),
method.sig.clone(),
None,
)
})
.collect::<Vec<_>>();
Self {
msg_options,
ident: item.ident.clone(),
vis: item.vis.clone(),
methods,
}
}
fn expand_messages(&self) -> TokenStream {
let mut tokens = TokenStream::new();
for method in &self.methods {
tokens.extend(method.expand_message());
}
tokens
}
fn expand_wrap(&self) -> TokenStream {
let Some(WrapOptions { attrs, name }) = self
.msg_options
.as_ref()
.map(|opts| opts.wrap.clone().map(|opts| opts.unwrap_or_default()))
.flatten()
else {
return TokenStream::new();
};
let attrs = attrs
.as_ref()
.map(|attrs| attrs.clone().to_vec())
.unwrap_or_default();
let wrap_ident = name.clone().unwrap_or_else(|| {
syn::Ident::new(&format!("{}Msg", self.ident.to_string()), Span::call_site())
});
let mut wrap_type_params = IdentSet::default();
let mut variants = Vec::with_capacity(self.methods.len());
for method in &self.methods {
wrap_type_params.extend(method.type_params.clone());
let variant_ident = &method.struct_ident;
let struct_ident = &method.struct_ident;
let type_params = method.type_params.iter();
variants.push(quote!(
#variant_ident (#struct_ident<#(#type_params),*>)
));
}
let wrap_type_params = wrap_type_params.iter();
let vis = &self.vis;
quote!(
#[derive(::ludi::Wrap)]
#(#[#attrs])*
#vis enum #wrap_ident<#(#wrap_type_params),*> {
#(#variants),*
}
)
}
pub(crate) fn expand(&self) -> TokenStream {
let mut tokens = TokenStream::new();
tokens.extend(self.expand_messages());
tokens.extend(self.expand_wrap());
tokens
}
}

View File

@@ -0,0 +1,415 @@
use std::collections::HashSet;
use darling::usage::{IdentSet, Purpose, UsesTypeParams};
use heck::ToUpperCamelCase;
use proc_macro2::{Span, TokenStream};
use proc_macro_error::emit_error;
use quote::quote;
use syn::parse_quote;
use crate::{
options::{CtrlOptions, ErrorStrategy, MsgOptions},
utils::{extract_output, is_ludi_attr},
};
/// An item method.
pub(crate) struct Method {
/// Attributes directly on the method. If these are present on an impl block they will
/// be forwarded to the handler impl if applicable.
pub(crate) attrs: Vec<syn::Attribute>,
/// Doc attributes are forwarded to the controller impl if applicable.
pub(crate) doc_attrs: Vec<syn::Attribute>,
/// Message options
pub(crate) msg_options: Option<MsgOptions>,
/// Controller options
pub(crate) ctrl_options: Option<CtrlOptions>,
/// Method visibility
pub(crate) vis: syn::Visibility,
/// Method signature
pub(crate) sig: syn::Signature,
/// Method body, if present.
pub(crate) body: Option<syn::Block>,
/// Method arguments
pub(crate) args: Vec<(syn::Ident, syn::Type)>,
/// Method return type
pub(crate) return_ty: syn::Type,
/// Type params from the parent item which are present in the method signature
pub(crate) type_params: IdentSet,
/// The struct ident, eg. Foo
pub(crate) struct_ident: syn::Ident,
/// The path to the struct, eg. foo::bar::Foo
pub(crate) struct_path: syn::Path,
}
impl Method {
pub(crate) fn new(
parent_ident: &syn::Ident,
parent_type_params: &IdentSet,
mut msg_options: Option<MsgOptions>,
mut ctrl_options: Option<CtrlOptions>,
span: Span,
attrs: Vec<syn::Attribute>,
vis: syn::Visibility,
sig: syn::Signature,
body: Option<syn::Block>,
) -> Self {
Method::check_signature(&span, &sig);
let (args, return_ty) = Self::extract_args(&sig);
let type_params = Self::extract_type_params(
&parent_type_params,
args.iter().map(|(_, ty)| ty),
&return_ty,
);
let method_msg_options = MsgOptions::maybe_from_attributes(&attrs);
if let Some(method_msg_options) = method_msg_options {
if let Some(msg_options) = msg_options.as_mut() {
msg_options.merge(&method_msg_options);
} else {
msg_options = Some(method_msg_options);
}
}
let method_ctrl_options = CtrlOptions::maybe_from_attributes(&attrs);
if let Some(method_ctrl_options) = method_ctrl_options {
if let Some(ctrl_options) = ctrl_options.as_mut() {
ctrl_options.merge(&method_ctrl_options);
} else {
ctrl_options = Some(method_ctrl_options);
}
}
let struct_ident =
if let Some(struct_name) = msg_options.as_ref().and_then(|opts| opts.name.clone()) {
syn::Ident::new(
&struct_name
.replace("{item}", &parent_ident.to_string())
.replace("{name}", &sig.ident.to_string().to_upper_camel_case()),
Span::call_site(),
)
} else {
syn::Ident::new(
&format!(
"{}Msg{}",
parent_ident.to_string(),
sig.ident.to_string().to_upper_camel_case()
),
Span::call_site(),
)
};
if struct_ident.to_string() == parent_ident.to_string() {
emit_error!(
sig.ident,
"message struct name must not be the same as the parent item"
);
}
let struct_path =
if let Some(path) = msg_options.as_ref().and_then(|opts| opts.path.as_ref()) {
parse_quote!(#path :: #struct_ident)
} else {
parse_quote!(#struct_ident)
};
let (mut attrs, doc_attrs): (Vec<_>, Vec<_>) = attrs
.into_iter()
.partition(|attr| !attr.meta.path().is_ident("doc"));
attrs.retain(|attr| !is_ludi_attr(attr));
Self {
attrs,
doc_attrs,
msg_options,
ctrl_options,
vis,
sig,
body,
args,
return_ty,
type_params,
struct_ident,
struct_path,
}
}
fn check_signature(span: &Span, sig: &syn::Signature) {
if !sig.generics.params.is_empty() {
emit_error!(
sig.generics.params,
"type parameters in methods are not supported"
);
}
if !sig.constness.is_none() {
emit_error!(span, "const methods are not supported");
}
}
/// Extracts the method arguments and return type.
fn extract_args(sig: &syn::Signature) -> (Vec<(syn::Ident, syn::Type)>, syn::Type) {
let args = sig
.inputs
.clone()
.into_iter()
.filter_map(|arg| {
let syn::FnArg::Typed(arg_ty) = arg else {
// Skip receiver arg, ie. `&mut self`
return None;
};
let syn::Pat::Ident(pat_ty) = *arg_ty.pat else {
emit_error!(arg_ty, "expected named argument");
return None;
};
let ty = *arg_ty.ty;
// TODO: better enforce that arg type is Sized + Send + 'static
match ty {
syn::Type::Reference(_) | syn::Type::Slice(_) | syn::Type::TraitObject(_) => {
emit_error!(ty, "arguments must be Sized + Send + 'static");
}
_ => {}
}
let mut ident = pat_ty.ident.clone();
ident.set_span(Span::call_site());
Some((ident, ty))
})
.collect::<Vec<_>>();
let return_ty = if let Some(ty) = extract_output(sig) {
ty
} else {
emit_error!(sig, "method must be async or return a future.");
parse_quote!(())
};
// TODO: better enforce that return type is Sized + Send + 'static
match return_ty {
syn::Type::Reference(_) | syn::Type::Slice(_) | syn::Type::TraitObject(_) => {
emit_error!(return_ty, "return type must be Sized + Send + 'static");
}
_ => {}
}
(args, return_ty)
}
/// Extracts the type params from the parent item which are present in the method signature.
fn extract_type_params<'a>(
parent_type_params: &IdentSet,
arg_tys: impl Iterator<Item = &'a syn::Type>,
return_ty: &syn::Type,
) -> IdentSet {
let arg_type_params = arg_tys
.map(|ty| ty.uses_type_params_cloned(&Purpose::Declare.into(), parent_type_params))
.fold(HashSet::default(), |mut acc, set| {
acc.extend(set);
acc
});
let return_type_params =
return_ty.uses_type_params_cloned(&Purpose::Declare.into(), parent_type_params);
// TODO: we could support this, but for now, no. It would require
// storing a PhantomData in the message struct which is kind of
// annoying.
if !return_type_params.is_empty() && !arg_type_params.is_superset(&return_type_params) {
emit_error!(
return_ty,
"generic param present in return type must also be present in argument types"
);
}
arg_type_params
}
pub(crate) fn expand_message(&self) -> TokenStream {
let Self {
msg_options,
vis,
args,
return_ty,
type_params,
struct_ident,
..
} = self;
if msg_options
.as_ref()
.map(|opts| opts.path.is_some() || opts.skip.is_present())
.unwrap_or(false)
{
return TokenStream::new();
}
let type_params = type_params.iter().cloned().collect::<Vec<_>>();
let arg_idents = args.iter().map(|(ident, _)| ident);
let arg_tys = args.iter().map(|(_, ty)| ty);
let vis = msg_options
.as_ref()
.map(|opts| opts.vis.clone())
.flatten()
.unwrap_or_else(|| vis.clone());
let msg_attrs = msg_options
.as_ref()
.map(|opts| opts.attrs.as_ref().map(|attrs| attrs.clone().to_vec()))
.flatten()
.unwrap_or_default();
let struct_body = if args.is_empty() {
quote!(;)
} else {
quote!({ #( pub #arg_idents: #arg_tys ),* })
};
quote!(
#( #[#msg_attrs] )*
#vis struct #struct_ident<#(#type_params),*> #struct_body
impl<#(#type_params),*> ::ludi::Message for #struct_ident<#(#type_params),*>
where
#(#type_params: Send + 'static),*
{
type Return = #return_ty;
}
impl<A, #(#type_params),*> ::ludi::Dispatch<A> for #struct_ident<#(#type_params),*>
where
A: ::ludi::Actor + ::ludi::Handler<#struct_ident<#(#type_params),*>>,
#(#type_params: Send + 'static),*
{
async fn dispatch<R: FnOnce(#return_ty) + Send>(
self,
actor: &mut A,
ctx: &mut ::ludi::Context<A>,
ret: R,
) {
::ludi::Handler::<#struct_ident<#(#type_params),*>>::process(
actor,
self,
ctx,
ret
).await;
}
}
)
}
pub fn expand_handler(&self, actor_path: &syn::Path, generics: &syn::Generics) -> TokenStream {
let Self {
attrs,
msg_options,
args,
type_params,
struct_path,
body,
..
} = self;
if msg_options
.as_ref()
.map(|opts| opts.skip_handler.is_present())
.unwrap_or(false)
{
return TokenStream::new();
}
let Some(body) = body else {
panic!("expected method to have a body");
};
let arg_idents = args.iter().map(|(ident, _)| ident).collect::<Vec<_>>();
let type_params = type_params.iter().cloned().collect::<Vec<_>>();
let destructure = if arg_idents.is_empty() {
quote!()
} else {
quote!(let #struct_path { #(#arg_idents),* } = msg;)
};
let (impl_generics, _, where_clause) = generics.split_for_impl();
quote!(
impl #impl_generics ::ludi::Handler<#struct_path<#(#type_params),*>> for #actor_path #where_clause {
#(#attrs)*
async fn handle(
&mut self,
msg: #struct_path<#(#type_params),*>,
ctx: &mut ::ludi::Context<Self>
) -> <#struct_path<#(#type_params),*> as ::ludi::Message>::Return {
#destructure
#body
}
}
)
}
pub fn expand_ctrl(&self, is_trait: bool) -> TokenStream {
let Self {
doc_attrs,
ctrl_options,
struct_path,
args,
vis,
sig,
..
} = self;
if ctrl_options.is_none() {
return TokenStream::new();
}
let mut ctrl_sig = sig.clone();
if let Some(syn::FnArg::Receiver(receiver)) = ctrl_sig.inputs.first_mut() {
if receiver.reference.is_some() && !is_trait {
*receiver = syn::parse_quote!(&self);
}
}
let arg_idents = args.iter().map(|(ident, _)| ident);
let struct_arg = if args.is_empty() {
quote!(#struct_path)
} else {
quote!(#struct_path { #(#arg_idents),* })
};
let attrs = ctrl_options
.as_ref()
.map(|opts| opts.attrs.clone().map(|attrs| attrs.to_vec()))
.flatten()
.unwrap_or_default();
let err_strategy = ctrl_options
.as_ref()
.map(|opts| opts.error_strategy())
.unwrap_or_default();
let err_handler = match err_strategy {
ErrorStrategy::Panic => quote!(.expect("message should be handled to completion")),
ErrorStrategy::Try => quote!(?),
ErrorStrategy::Map(expr) => quote!(.map_err(#expr)?),
};
quote!(
#(#doc_attrs)*
#(#[#attrs])*
#vis #ctrl_sig {
::ludi::Address::send_await(
::ludi::Controller::address(self),
#struct_arg
).await #err_handler
}
)
}
}

View File

@@ -0,0 +1,6 @@
mod item_impl;
mod item_trait;
mod method;
pub(crate) use item_impl::ItemImpl;
pub(crate) use item_trait::ItemTrait;

View File

@@ -1,22 +1,51 @@
mod controller;
mod implement;
mod interface;
pub(crate) mod types;
pub(crate) mod items;
mod message;
pub(crate) mod options;
pub(crate) mod utils;
pub(crate) mod wrap;
use proc_macro::TokenStream;
use proc_macro_error::proc_macro_error;
use syn::DeriveInput;
#[proc_macro_error]
#[proc_macro_attribute]
pub fn interface(_attr: TokenStream, item: TokenStream) -> TokenStream {
let mut tokens = item.clone();
pub fn interface(attr: TokenStream, item: TokenStream) -> TokenStream {
let item_trait = syn::parse_macro_input!(item as syn::ItemTrait);
tokens.extend(interface::impl_interface(item_trait));
tokens
interface::impl_interface(attr, item_trait).into()
}
#[proc_macro_error]
#[proc_macro_attribute]
pub fn implement(_attr: TokenStream, item: TokenStream) -> TokenStream {
pub fn implement(attr: TokenStream, item: TokenStream) -> TokenStream {
let item_impl = syn::parse_macro_input!(item as syn::ItemImpl);
implement::impl_implement(item_impl)
implement::impl_implement(attr, item_impl).into()
}
#[proc_macro_derive(Message, attributes(ludi))]
pub fn message(input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as DeriveInput);
message::impl_message(input).into()
}
#[proc_macro_error]
#[proc_macro_derive(Wrap, attributes(ludi))]
pub fn wrap(input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as DeriveInput);
wrap::impl_wrap(input).into()
}
#[proc_macro_error]
#[proc_macro_derive(Controller, attributes(ludi))]
pub fn controller(input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as DeriveInput);
controller::impl_controller(input).into()
}

View File

@@ -0,0 +1,70 @@
use darling::FromDeriveInput;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{parse_quote, DeriveInput};
#[derive(Debug, FromDeriveInput)]
#[darling(attributes(ludi))]
struct Message {
ident: syn::Ident,
generics: syn::Generics,
return_ty: Option<syn::Path>,
}
pub(crate) fn impl_message(input: DeriveInput) -> TokenStream {
let Message {
ident,
mut generics,
return_ty,
} = match Message::from_derive_input(&input) {
Ok(msg) => msg,
Err(e) => return e.with_span(&input).write_errors(),
};
let generic_params = generics.params.clone();
let where_clause = generics.make_where_clause();
for param in generic_params {
where_clause
.predicates
.push(parse_quote!(#param: Send + 'static));
}
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let mut dispatch_generics = generics.clone();
dispatch_generics.params.push(parse_quote!(A));
let dispatch_where = dispatch_generics.make_where_clause();
dispatch_where
.predicates
.push(parse_quote!(A: ::ludi::Actor));
dispatch_where
.predicates
.push(parse_quote!(A: ::ludi::Handler<#ident #ty_generics>));
let (dispatch_generics, _, dispatch_where) = dispatch_generics.split_for_impl();
let return_ty = if let Some(path) = return_ty {
quote!(#path)
} else {
quote!(())
};
quote!(
impl #impl_generics ::ludi::Message for #ident #ty_generics #where_clause {
type Return = #return_ty;
}
impl #dispatch_generics ::ludi::Dispatch<A> for #ident #ty_generics #dispatch_where
{
async fn dispatch<R: FnOnce(Self::Return) + Send>(
self,
actor: &mut A,
ctx: &mut ::ludi::Context<A>,
ret: R,
) {
actor.process(self, ctx, ret).await;
}
}
)
}

265
ludi-macros/src/options.rs Normal file
View File

@@ -0,0 +1,265 @@
use darling::{
util::{Flag, Override},
FromMeta,
};
use proc_macro_error::emit_error;
#[derive(Default, Clone, FromMeta)]
pub(crate) struct MsgOptions {
/// Attributes which are passed through to the message struct
pub(crate) attrs: Option<NestedAttrs>,
/// Visibility of the generated message struct
pub(crate) vis: Option<syn::Visibility>,
/// Message struct name
pub(crate) name: Option<String>,
/// Path to the message struct module
pub(crate) path: Option<syn::Path>,
/// Wrap options
pub(crate) wrap: Option<Override<WrapOptions>>,
/// Skip messages
pub(crate) skip: Flag,
/// Skip message handler
pub(crate) skip_handler: Flag,
}
impl MsgOptions {
pub(crate) fn merge(&mut self, other: &Self) {
if let Some(attrs) = &mut self.attrs {
if let Some(other_attrs) = &other.attrs {
attrs.0.extend_from_slice(&other_attrs.0);
}
} else {
self.attrs = other.attrs.clone();
}
if other.vis.is_some() {
self.vis = other.vis.clone();
}
if other.name.is_some() {
self.name = other.name.clone();
}
if other.path.is_some() {
self.path = other.path.clone();
}
if let Some(Override::Explicit(wrap)) = &mut self.wrap {
if let Some(Override::Explicit(other_wrap)) = other.wrap.as_ref() {
wrap.merge(other_wrap);
}
} else {
self.wrap = other.wrap.clone();
}
self.skip = other.skip.clone();
self.skip_handler = other.skip_handler.clone();
}
pub(crate) fn maybe_from_attributes(attrs: &[syn::Attribute]) -> Option<Self> {
let mut any = false;
let mut options = Self::default();
for attr in attrs {
if attr.path().is_ident("msg") {
any = true;
match &attr.meta {
syn::Meta::Path(_) => {
// We use defaults for word
}
_ => match Self::from_meta(&attr.meta) {
Ok(msg_options) => options.merge(&msg_options),
Err(err) => {
emit_error!(attr, "invalid `msg` attribute: {}", err);
return None;
}
},
}
}
}
if any {
Some(options)
} else {
None
}
}
}
#[derive(Default, Clone, FromMeta)]
pub(crate) struct WrapOptions {
/// Attributes which are passed through to the wrapper struct
pub(crate) attrs: Option<NestedAttrs>,
/// Wrapper struct ident
pub(crate) name: Option<syn::Ident>,
}
impl WrapOptions {
pub(crate) fn merge(&mut self, other: &Self) {
if let Some(attrs) = &mut self.attrs {
if let Some(other_attrs) = &other.attrs {
attrs.0.extend_from_slice(&other_attrs.0);
}
} else {
self.attrs = other.attrs.clone();
}
if other.name.is_some() {
self.name = other.name.clone();
}
}
}
#[derive(Default, Clone, FromMeta)]
pub(crate) struct CtrlOptions {
/// Attributes which are passed through to the controller struct
pub(crate) attrs: Option<NestedAttrs>,
/// Controller struct ident
pub(crate) name: Option<syn::Ident>,
/// Path to the controller struct
pub(crate) path: Option<syn::Path>,
/// Error handling
pub(crate) err: Option<Override<syn::Expr>>,
}
impl CtrlOptions {
pub(crate) fn merge(&mut self, other: &Self) {
if let Some(attrs) = &mut self.attrs {
if let Some(other_attrs) = &other.attrs {
attrs.0.extend_from_slice(&other_attrs.0);
}
} else {
self.attrs = other.attrs.clone();
}
if other.name.is_some() {
self.name = other.name.clone();
}
if other.path.is_some() {
self.path = other.path.clone();
}
if other.err.is_some() {
self.err = other.err.clone();
}
}
pub(crate) fn error_strategy(&self) -> ErrorStrategy {
if let Some(err) = &self.err {
match err {
Override::Inherit => ErrorStrategy::Try,
Override::Explicit(expr) => ErrorStrategy::Map(expr.clone()),
}
} else {
ErrorStrategy::Panic
}
}
pub(crate) fn maybe_from_attributes(attrs: &[syn::Attribute]) -> Option<Self> {
let mut any = false;
let mut options = Self::default();
for attr in attrs {
if attr.path().is_ident("ctrl") {
any = true;
match &attr.meta {
syn::Meta::Path(_) => {
// We use defaults for word
}
_ => match Self::from_meta(&attr.meta) {
Ok(msg_options) => options.merge(&msg_options),
Err(err) => {
emit_error!(attr, "invalid `msg` attribute: {}", err);
return None;
}
},
}
}
}
if any {
Some(options)
} else {
None
}
}
}
pub(crate) enum ErrorStrategy {
/// Panic on error
Panic,
/// Attempts to handle error with `?` operator
Try,
/// Map error to another type then attempts to handle it with `?` operator
Map(syn::Expr),
}
impl Default for ErrorStrategy {
fn default() -> Self {
Self::Panic
}
}
#[derive(Clone)]
pub(crate) struct NestedAttrs(Vec<darling::ast::NestedMeta>);
impl NestedAttrs {
pub(crate) fn to_vec(self) -> Vec<darling::ast::NestedMeta> {
self.0
}
}
impl FromMeta for NestedAttrs {
fn from_list(items: &[darling::ast::NestedMeta]) -> darling::Result<Self> {
Ok(Self(items.to_vec()))
}
}
impl AsRef<[darling::ast::NestedMeta]> for NestedAttrs {
fn as_ref(&self) -> &[darling::ast::NestedMeta] {
&self.0
}
}
#[cfg(test)]
mod tests {
use syn::parse_quote;
use super::*;
#[test]
fn test_msg_from_attributes_none() {
let options = MsgOptions::maybe_from_attributes(&[]);
assert!(options.is_none());
}
#[test]
fn test_msg_from_attribute() {
let attrs = vec![
parse_quote!(#[msg(name = "Foo")]),
parse_quote!(#[other_attr(foo = "bar")]),
];
let options = MsgOptions::maybe_from_attributes(&attrs).unwrap();
assert_eq!(options.name.unwrap().to_string(), "Foo");
}
#[test]
fn test_msg_from_attributes_many() {
let attrs = vec![
parse_quote!(#[msg(name = "Foo")]),
parse_quote!(#[msg(name = "Bar")]),
parse_quote!(#[msg(vis = pub(crate))]),
];
let options = MsgOptions::maybe_from_attributes(&attrs).unwrap();
assert!(options.attrs.is_none());
assert_eq!(options.name.unwrap().to_string(), "Bar");
assert!(matches!(
options.vis.unwrap(),
syn::Visibility::Restricted(_)
));
}
}

View File

@@ -1,48 +0,0 @@
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 }
}
}

76
ludi-macros/src/utils.rs Normal file
View File

@@ -0,0 +1,76 @@
use proc_macro2::Span;
use syn::parse_quote;
/// Returns the identifier of the controller for the given actor.
pub(crate) fn ctrl_ident(actor_ident: &syn::Ident) -> syn::Ident {
syn::Ident::new(
&format!("{}Ctrl", actor_ident.to_string()),
Span::call_site(),
)
}
/// Extracts the output of an async function, returns `None` if the function is not async.
pub(crate) fn extract_output(sig: &syn::Signature) -> Option<syn::Type> {
if sig.asyncness.is_some() {
let return_ty = match sig.output.clone() {
syn::ReturnType::Default => parse_quote!(()),
syn::ReturnType::Type(_, ty) => *ty,
};
Some(return_ty)
} else {
let syn::ReturnType::Type(_, ty) = sig.output.clone() else {
return None;
};
let ty = *ty;
match ty {
syn::Type::ImplTrait(ty) => {
return ty.bounds.iter().find_map(|bound| {
if let syn::TypeParamBound::Trait(bound) = bound {
extract_fut_output(bound)
} else {
None
}
});
}
syn::Type::Path(_) => {
// TODO: Support boxed futures
return None;
}
_ => return None,
}
}
}
fn extract_fut_output(bound: &syn::TraitBound) -> Option<syn::Type> {
let segment = bound.path.segments.last()?;
if segment.ident != "Future" {
return None;
}
let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
return None;
};
args.args.iter().find_map(|arg| {
if let syn::GenericArgument::AssocType(assoc_ty) = arg {
if assoc_ty.ident == "Output" {
return Some(assoc_ty.ty.clone());
}
}
None
})
}
pub(crate) fn is_ludi_attr(attr: &syn::Attribute) -> bool {
let Some(ident) = attr.meta.path().get_ident() else {
return false;
};
match ident.to_string().as_str() {
"msg" | "ctrl" => true,
_ => false,
}
}

194
ludi-macros/src/wrap.rs Normal file
View File

@@ -0,0 +1,194 @@
use std::collections::HashSet;
use darling::{
ast::{Data, Fields},
FromDeriveInput, FromField, FromVariant, Error, error::Accumulator,
};
use quote::{quote, ToTokens};
use proc_macro2::TokenStream;
use syn::{DeriveInput, parse_quote};
pub(crate) fn impl_wrap(input: DeriveInput) -> TokenStream {
let wrap = match Wrap::from_derive_input(&input) {
Ok(msg) => msg,
Err(e) => return e.with_span(&input).write_errors(),
};
quote!(#wrap)
}
#[derive(Debug, FromDeriveInput)]
#[darling(attributes(ludi), supports(enum_newtype), and_then = "Wrap::validate")]
pub(crate) struct Wrap {
ident: syn::Ident,
vis: syn::Visibility,
generics: syn::Generics,
data: Data<Variant, darling::util::Ignored>,
#[darling(skip)]
variants: Vec<Variant>,
}
impl Wrap {
fn validate(mut self) -> Result<Self, Error> {
let mut err = Accumulator::default();
if self.generics.lifetimes().count() > 0 {
err.push(
Error::custom("wrapper can not be generic over lifetimes")
.with_span(&self.generics),
);
}
if self.generics.const_params().count() > 0 {
err.push(
Error::custom("wrapper can not be generic over const parameters")
.with_span(&self.generics),
);
}
self.variants = match &mut self.data {
Data::Enum(variants) => std::mem::take(variants),
Data::Struct(_) => panic!("expected darling to validate that the wrapper is an enum"),
};
let variant_tys = self.variants.iter().map(|variant| &variant.fields.fields[0].ty).collect::<HashSet<_>>();
if variant_tys.len() != self.variants.len() {
err.push(
Error::custom("wrapper can not have duplicate variant types")
.with_span(&self),
);
}
let type_params = self.generics.type_params().map(|param| &param.ident).collect::<HashSet<_>>();
variant_tys.iter().for_each(|ty| {
if let syn::Type::Path(path) = ty {
if path.path.segments.len() == 1 {
let ident = &path.path.segments[0].ident;
if type_params.contains(ident) {
err.push(
Error::custom("wrapper can not have generic variants")
.with_span(&ty),
);
}
}
}
});
err.finish()?;
Ok(self)
}
}
impl ToTokens for Wrap {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self {
ident,
vis,
generics,
variants,
..
} = self;
let mut generics = generics.clone();
let type_params = generics.type_params().map(|param| param.ident.clone()).collect::<Vec<_>>();
let where_clause = generics.make_where_clause();
for param in type_params {
where_clause
.predicates
.push(parse_quote!(#param: Send + 'static));
}
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let return_ident = syn::Ident::new(&format!("{}Return", ident), ident.span());
let (variant_idents, variant_tys) = variants.into_iter().fold(
(Vec::new(), Vec::new()),
|(mut idents, mut tys), variant| {
idents.push(variant.ident.clone());
tys.push(variant.fields.fields[0].ty.clone());
(idents, tys)
},
);
tokens.extend(quote!(
impl #impl_generics ::ludi::Message for #ident #ty_generics #where_clause {
type Return = #return_ident #ty_generics;
}
#vis enum #return_ident #ty_generics #where_clause {
#(
#variant_idents ( <#variant_tys as ::ludi::Message>::Return ),
)*
}
#(
impl #impl_generics From<#variant_tys> for #ident #ty_generics #where_clause {
fn from(value: #variant_tys) -> Self {
Self :: #variant_idents (value)
}
}
)*
#(
impl #impl_generics ::ludi::Wrap<#variant_tys> for #ident #ty_generics #where_clause {
fn unwrap_return(ret: Self::Return) -> Result<<#variant_tys as ::ludi::Message>::Return, ::ludi::MessageError> {
match ret {
Self::Return :: #variant_idents (value) => Ok(value),
_ => Err(::ludi::MessageError::Wrapper),
}
}
}
)*
));
let mut generics = generics.clone();
generics.params.push(parse_quote!(A));
let where_clause = generics.make_where_clause();
where_clause
.predicates
.push(parse_quote!(A: ::ludi::Actor));
for variant_ty in variant_tys {
where_clause
.predicates
.push(parse_quote!(#variant_ty: ::ludi::Dispatch<A>));
}
let (impl_generics, _, where_clause) = generics.split_for_impl();
tokens.extend(quote!(
impl #impl_generics ::ludi::Dispatch<A> for #ident #ty_generics #where_clause
{
async fn dispatch<R: FnOnce(Self::Return) + Send>(
self,
actor: &mut A,
ctx: &mut ::ludi::Context<A>,
ret: R,
) {
match self {
#(
#ident :: #variant_idents (msg) => {
msg.dispatch(actor, ctx, |value| ret(Self::Return :: #variant_idents (value))).await;
}
),*
}
}
}
));
}
}
#[derive(Debug, FromVariant)]
pub(crate) struct Variant {
pub ident: syn::Ident,
pub fields: Fields<Field>,
}
#[derive(Debug, FromField)]
pub(crate) struct Field {
pub ty: syn::Type,
}

View File

@@ -4,16 +4,15 @@ version = "0.1.0"
edition = "2021"
[features]
default = ["macros"]
default = ["macros", "futures-mailbox"]
macros = ["dep:ludi-macros"]
futures-mailbox = ["ludi-core/futures-mailbox"]
[dependencies]
ludi-core = { path = "../ludi-core" }
ludi-macros = { path = "../ludi-macros", optional = true }
futures-util = { version = "0.3", features = ["sink"] }
[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
[[example]]
name = "simple"
required-features = ["macros"]

View File

@@ -1,59 +0,0 @@
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);
}

View File

@@ -1,3 +1,5 @@
#![doc = include_str!("../../README.md")]
pub use ludi_core::*;
#[cfg(feature = "macros")]
pub use ludi_macros::*;

View File

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