Attribute Macro interthread::actor

source ·
#[actor]
Expand description

§Evolves a regular object into an actor

The macro is placed upon an implement block of an object (struct or enum), which has a public or restricted method named new returning Self.

In case if the initialization could potentially fail, the method can be named try_new and return Option<Self> or Result<Self>.

The macro will copy method signatures from all public methods that do not consume the receiver, excluding methods like pub fn foo(self, val: u8) -> () where self is consumed. Please ensure that the receiver is defined as &self or &mut self.

If only a subset of methods is required to be accessible across threads, split the impl block into two parts. By applying the macro to a specific block, the macro will only consider the methods within that block.

§Configuration Options

 
#[interthread::actor( 
    
    channel = 0 * 
              n (usize)

        lib = "std" *
              "smol"
              "tokio"
              "async_std"

        edit( 
             script(..)
             live(..)
            ) 

        file = "path/to/current/file.rs"
        
        name = "" 

       assoc 
        
       debut(
             legend
            ) 
    interact
)]

*  -  default 


§Arguments

§channel

The channel argument specifies the type of channel.

  • 0 (default)
  • usize ( buffer size)

The two macros

#[actor]

and

#[actor(channel=0)]

are in fact identical, both specifying same unbounded channel.

When specifying an usize value for the channel argument in the actor macro, such as

#[actor(channel=4)]

the actor will use a bounded channel with a buffer size of 4. This means that the channel can hold up to 4 messages in its buffer before blocking/suspending the sender.

Using a bounded channel with a specific buffer size allows for control over the memory usage and backpressure behavior of the model. When the buffer is full, any further attempts to send messages will block/suspend until there is available space. This provides a natural form of backpressure, allowing the sender to slow down or pause message production when the buffer is near capacity.

§lib

The lib argument specifies the ‘async’ library to use.

  • "std" (default)
  • "smol"
  • "tokio"
  • "async_std"

§Examples

use interthread::actor;

struct MyActor;

#[actor(channel=10, lib ="tokio")]
impl MyActor{
    pub fn new() -> Self{Self}
}
#[tokio::main]
async fn main(){
    let my_act = MyActorLive::new();
}

§edit

The edit argument specifies the available editing options. When using this argument, the macro expansion will exclude the code related to edit options allowing the user to manually implement and customize those parts according to their specific needs.

The SDPL Model encompasses two main structs, namely ActorScript and ActorLive. Within the edit statement, these are referenced as script and live respectively.

Each struct comprises three distinct sections:

  • def - definition
  • imp - implementation block
  • trt - implemented traits
edit(
    script(
        def,     // <- script definition
        imp(..), // <- list of methods in impl block
        trt(..)  // <- list of traits
    ),
    
    live(
        def,     // <- live definition
        imp(..), // <- list of methods in impl block
        trt(..)  // <- list of traits
    )
)

So this option instructs the macro to:

  • Exclude specified sections of code from the generated model.

Examples:

  • edit(script): Excludes the entire Script enum.
  • edit(live(imp)): Excludes the entire implementation block of the Live struct.
  • edit(live(def, imp(new))): Excludes both the definition of the Live struct and the method ‘new.’
  • edit(script(imp(play)), live(imp(new))): Excludes the ‘play’ method from the Script enum and the ‘new’ method from the Live struct.

Exclusion of code becomes necessary when the user has already customized specific sections of the model. To facilitate the exclusion of parts from the generated model and enable printing them to the file for further user customization, consider the file option, which works in conjunction with the edit option.

§file

This argument is designed to address proc macro file blindness. It requires a string path to the current file as its value. Additionally, within the edit argument, one can use the keyword file to specify which portion of the excluded code should be written to the current module, providing the user with a starting point for customization.

§Examples

Filename: main.rs

pub struct MyActor(u8);

#[interthread::actor(
    file="src/main.rs",
    edit(live(imp( file(increment) )))
)]  

impl MyActor {

    pub fn new() -> Self {Self(0)}

    pub fn increment(&mut self){
        self.0 += 1;
    }
}

This is the output after saving:


pub struct MyActor(u8);

#[interthread::actor(
   file="src/main.rs",
   edit(live(imp(increment)))
)]  

impl MyActor {

   pub fn new() -> Self {Self(0)}

   pub fn increment(&mut self){
       self.0 += 1;
   }
}

//++++++++++++++++++[ Interthread  Write to File ]+++++++++++++++++//
// Object Name   : MyActor  
// Initiated By  : #[interthread::actor(file="src/main.rs",edit(live(imp(file(increment)))))]  


impl MyActorLive {
    pub fn increment(&mut self) {
        let msg = MyActorScript::Increment {};
        let _ = self
            .sender
            .send(msg)
            .expect("'MyActorLive::method.send'. Channel is closed!");
    }
}

// *///.............[ Interthread  End of Write  ].................//

To specify the part of your model that should be written to the file, simply enclose it within file(..) inside the edit argument. Once the desired model parts are written, the macro will automatically clean the file arguments, adjusting itself to the correct state.

Attempting to nest file arguments like:

edit( file( script( file( def))))

will result in an error.

A special case of the edit and file conjunction, using edit(file) results in the macro being replaced with the generated code on the file.

Note: While it is possible to have multiple actor macros within the same module, only one of the macro can have file active arguments (file within edit) at a time.

§name

The name attribute allows developers to provide a custom name for actor, overriding the default naming conventions of the crate. This can be useful when there are naming conflicts or when a specific naming scheme is desired.

  • “” (default): No name specified

§Examples

use interthread::actor;
 
pub struct MyActor;
 
#[actor(name="OtherActor")]
impl MyActor {

   pub fn new() -> Self {Self}
}
fn main () {
   let other_act = OtherActorLive::new();
}

§assoc

The assoc option indicates whether associated functions ( also known as static methods ) that return a type of the actor struct are included in generated code as instance methods, allowing them to be invoked on the generated struct itself.

Note: In the forthcoming version 1.3.0, the assoc option will be deprecated and removed, in favor of supporting all non-private static (associated) methods. With this change, every Actor::method will be mirrored by an equivalent named ActorLive::method, effectively calling Actor::method internally. This option enhances the model abstraction, eliminating the need to import the entire Actor type solely to access its associated methods.

§Examples

use interthread::actor;
pub struct Aa;
  
 
#[actor(name="Bb", assoc)]
impl Aa {

    pub fn new() -> Self { Self{} }
 
    // we don't have a `&self`
    // receiver 
    pub fn is_even( n: u8 ) -> bool {
        n % 2 == 0
    }
}

fn main() {
    
    let bb = BbLive::new();
 
    // but we can call it 
    // as if there was one   
    assert_eq!(bb.is_even(84), Aa::is_even(84));
}

§debut

The generated code is designed to compile successfully on Rust versions as early as 1.63.0.

When declared debut, the following additions and implementations are generated:

Within the live struct definition, the following fields are generated:

  • pub debut: std::time::SystemTime
  • pub name: String

The following traits are implemented for the live struct:

  • PartialEq
  • PartialOrd
  • Eq
  • Ord

These traits allow for equality and ordering comparisons based on the debutvalue. The name field is provided for user needs only and is not taken into account when performing comparisons. It serves as a descriptive attribute or label associated with each instance of the live struct.

In the script struct implementation block, which encapsulates the functionality of the model, a static method named debut is generated. This method returns the current system time and is commonly used to set the debut field when initializing instances of the live struct.

Use macro example to see the generated code.

§Examples

use std::thread::spawn;
pub struct MyActor ;

#[interthread::actor( debut )] 
impl MyActor {
    pub fn new() -> Self { Self{} } 
}
fn main() {

    let actor_1 = MyActorLive::new();

    let handle_2 = spawn( move || { 
        MyActorLive::new()
    });
    let actor_2 = handle_2.join().unwrap();

    let handle_3 = spawn( move || {
        MyActorLive::new()
    });
    let actor_3 = handle_3.join().unwrap();
    
    // they are the same type objects
    // but serving differrent threads
    // different actors !   
    assert!(actor_1 != actor_2);
    assert!(actor_2 != actor_3);
    assert!(actor_3 != actor_1);

    // since we know the order of invocation
    // we correctly presume
    assert_eq!(actor_1 > actor_2, true );
    assert_eq!(actor_2 > actor_3, true );
    assert_eq!(actor_3 < actor_1, true );

    // but if we check the order by `debute` value
    assert_eq!(actor_1.debut < actor_2.debut, true );
    assert_eq!(actor_2.debut < actor_3.debut, true );
    assert_eq!(actor_3.debut > actor_1.debut, true );
    
    // This is because the 'debut' 
    // is a time record of initiation
    // Charles S Chaplin (1889)
    // Keanu Reeves      (1964)


    // we can count `live` instances for 
    // every model
    use std::sync::Arc;
    let mut a11 = actor_1.clone();
    let mut a12 = actor_1.clone();

    let mut a31 = actor_3.clone();

    assert_eq!(Arc::strong_count(&actor_1.debut), 3 );
    assert_eq!(Arc::strong_count(&actor_2.debut), 1 );
    assert_eq!(Arc::strong_count(&actor_3.debut), 2 );
            

    // or use getter `count`                 
    assert_eq!(actor_1.inter_get_count(), 3 );
    assert_eq!(actor_2.inter_get_count(), 1 );
    assert_eq!(actor_3.inter_get_count(), 2 );
    

    use std::time::SystemTime;

    // getter `debut` to get its timestamp   
    let _debut1: SystemTime = actor_1.inter_get_debut();

            
    // the name field is not taken 
    // into account when comparison is
    // perfomed       
    assert!( a11 == a12);
    assert!( a11 != a31);

    a11.name = String::from("Alice");
    a12.name = String::from("Bob");

    a31.name = String::from("Alice");

    assert_eq!(a11 == a12, true );
    assert_eq!(a11 != a31, true );

    // setter `name` accepts any ToString  
    a11.inter_set_name('t');
    a12.inter_set_name(84u32);
    a31.inter_set_name(3.14159);

    // getter `name`                      
    assert_eq!(a11.inter_get_name(), "t" );
    assert_eq!(a12.inter_get_name(), "84" );
    assert_eq!(a31.inter_get_name(), "3.14159" );

}

Using debut will generate fore additional methods in live implement block:

  1. inter_set_name(s: ToString): Sets the value of the name field.
  2. inter_get_name() -> &str: Retrieves the value of the name field.
  3. inter_get_debut() -> std::time::SystemTime: Retrieves the value of the debut field, which represents a timestamp.
  4. inter_get_count() -> usize: Provides the strong reference count for the debut field.

Note: Additional generated methods prefixed with inter will have the same visibility as the initiating method new or try_new.

This convention allows

  • easy identification in text editor methods that solely manipulate the internal state of the live struct and/or methods that are added by the interthread macros
  • it mitigates the risk of potential naming conflicts in case if there is or will be a custom method get_name
  • helps the macro identify methods that are intended to be used within its context (see interact)

While debut can be declared as a standalone option, it can also be enhanced by adding the legend sub-option. This sub-option introduces extra inter methods, enabling the model to be saved on the heap upon the last instance being dropped.

Note: Unfortunately, while we’ve established a consistent inter prefix method convention, the legend functionality introduces an exception. To maintain a coherent and orderly pattern try_new -> try_old

§Examples

 pub struct MyActor(u8);


#[interthread::actor( debut(legend) )] 
impl MyActor {

    pub fn new() -> Self { Self(0) }

    pub fn set(&mut self, v: u8){
        self.0 = v;
    } 

    pub fn get_value(&self) -> u8 {
        self.0
    }
}


fn main() {

    let h = std::thread::spawn( || {
        let mut act = MyActorLive::new();
        act.inter_set_name("Zombie"); 
        act.set(121);
    });
    
    let _ = h.join();

    let old_act = MyActorLive::try_old("Zombie").unwrap();

    assert_eq!("Zombie".to_string(), old_act.inter_get_name());
    assert_eq!(121u8, old_act.get_value());
}

When the thread scope ends, objects are dropped. Simply using drop(..) won’t suffice. To conclude the thread scope correctly, use join(). Then, you can call try_old on the live struct to reinitialize the old model.

§interact

The interact option is designed to provide the model with comprehensive non-blocking functionality, along with convenient internal getter calls to access the state of the live instance via so called inter variables in actor methods.

§Rules and Definitions

  1. The interact variables should be prefixed with inter_.
  2. Special interact variables are inter_send and inter_recv.
  3. Declaring an inter_variable_name : Type, within actor method arguments implies that the live instance has a method fn inter_get_variable_name(&self) -> Type which takes no arguments and returns the Type. Exceptions to this rule apply for special interact variables.
  4. If the actor method returns a type, accessing special interact variables is not allowed.
  5. Only one end of special interact variables can be accessed at a time.

The primary purpose of interact is to leverage its oneshot inter_send and inter_recv ends. This allows for a form of non-blocking behavior: one end of the channel will be directly sent into the respective method, while the other end will be returned from the live instance method.

§Examples


pub struct MyActor;

#[interthread::actor( interact )] 
impl MyActor {

   pub fn new() -> Self { Self{} } 

   // oneshot channel can be accessed 
   // in methods that do not return 
   pub fn heavy_work(&self, inter_send: oneshot::Sender<u8>){

       std::thread::spawn(move||{
           // do some havy computation
           let _ = inter_send.send(5);
       });
   }
}

fn main () {

   let actor = MyActorLive::new();

   // the signature is different
   let recv: oneshot::Receiver<u8> = actor.heavy_work(); 
   let int = recv.recv().unwrap();

   assert_eq!(5u8, int);
}

While a method that does not return a type (see original heavy_work) typically does not require a oneshot channel, the model will accommodate the user’s request by instantiating a channel pair.

 
pub fn heavy_work(&self) -> oneshot::Receiver<u8> {
    let (inter_send, inter_recv) = oneshot::channel::<u8>();
    let msg = MyActorScript::HeavyWork {
        input: (inter_send),
    };
    let _ = self
        .sender
        .send(msg)
        .expect("'MyActorLive::method.send'. Channel is closed!");
    inter_recv
}

Also interact will detect interact variables in actor methods and subsequently call required getters within respective method of the live instance.

§Examples

pub struct MyActor(String);

#[interthread::actor(debut, interact )] 
impl MyActor {

    pub fn new() -> Self { Self("".to_string()) } 

    // We know there is a getter `inter_get_name`
    // Using argument `inter_name` we imply
    // we want the return type of that getter
    pub fn set_value(&mut self, inter_name: String){
        self.0 = inter_name;
    }
    pub fn get_value(&self) -> String {
        self.0.clone()
    }
}

fn main () {

    let mut actor = MyActorLive::new();

    // Setting name for `live` instance
    actor.inter_set_name("cloud");

    // Setting actor's value now
    // Note the signature, it's not the same  
    actor.set_value();

    assert_eq!("cloud".to_string(), actor.get_value());
}

Here is how live instance method set_value will look like:


pub fn set_value(&mut self) {
    let inter_name = self.inter_get_name();
    let msg = MyActorScript::SetValue {
        input: inter_name,
    };
    let _ = self
        .sender
        .send(msg)
        .expect("'MyActorLive::method.send'. Channel is closed!");
}

The signature has changed; it no longer takes arguments, as the getter call is happening inside providing the required type. It will work for any custom getter as long as it adheres to rule 3.