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
- definitionimp
- implementation blocktrt
- 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
withinedit
) 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, everyActor::method
will be mirrored by an equivalent namedActorLive::method
, effectively callingActor::method
internally. This option enhances the model abstraction, eliminating the need to import the entireActor
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 debut
value.
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:
inter_set_name(s: ToString)
: Sets the value of the name field.inter_get_name() -> &str
: Retrieves the value of the name field.inter_get_debut() -> std::time::SystemTime
: Retrieves the value of the debut field, which represents a timestamp.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 methodnew
ortry_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, thelegend
functionality introduces an exception. To maintain a coherent and orderly patterntry_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
- The interact variables should be prefixed with
inter_
. - Special interact variables are
inter_send
andinter_recv
. - Declaring an
inter_variable_name : Type
, within actor method arguments implies that thelive
instance has a methodfn inter_get_variable_name(&self) -> Type
which takes no arguments and returns theType
. Exceptions to this rule apply for special interact variables. - If the actor method returns a type, accessing special interact variables is not allowed.
- 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.