Skip to content

Commit a6e0863

Browse files
books: hyperactor-book: hyperactor macros (#388)
Summary: Pull Request resolved: #388 new section documenting the hyperactor macros Reviewed By: mariusae Differential Revision: D77595758 fbshipit-source-id: 714c31736f0386e51c69d3974ebf4d9a7033b374
1 parent 1e36110 commit a6e0863

File tree

9 files changed

+438
-1
lines changed

9 files changed

+438
-1
lines changed

books/hyperactor-book/src/SUMMARY.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# Summary
22

33
- [Introduction](./introduction.md)
4+
- [Macros](macros/index.md)
5+
- [`#[derive(Handler)]`](macros/handler.md)
6+
- [`#[derive(HandleClient)]`](macros/handle_client.md)
7+
- [`#[derive(RefClient)]`](macros/ref_client.md)
8+
- [`#[derive(Named)]`](macros/named.md)
9+
- [`#[export]`](macros/export.md)
10+
- [`#[forward]`](macros/forward.md)
411
- [Mailboxes and Routers](mailboxes/index.md)
512
- [Ports](mailboxes/ports.md)
613
- [MailboxSender](mailboxes/mailbox_sender.md)

books/hyperactor-book/src/introduction.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ This book describes the design and implementation of the hyperactor runtime.
44

55
The goal is to provide a clear, structured explanation of how actors communicate safely and efficiently across distributed systems using hyperactor’s abstractions.
66

7-
Work in progress.
7+
We hope this becomes the book we wish we had when we started working with Monarch. Work in progress.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# `#[export]`
2+
3+
The `#[hyperactor::export]` macro turns a regular `Actor` implementation into a remotely spawnable actor, registering its type information, `spawn` function, and supported message handlers for discovery and use across processes or runtimes.
4+
5+
## What It Adds
6+
7+
When applied to an actor type like this:
8+
9+
```rust
10+
#[hyperactor::export(
11+
spawn = true,
12+
handlers = [ShoppingList],
13+
)]
14+
struct ShoppingListActor(HashSet<String>);
15+
```
16+
The macro expands to include:
17+
- A `Named` implementation for the actor
18+
- A `Binds<Self>` implementation that registers supported message types
19+
- Implementations of `RemoteHandles<T>` for each type in the `handlers = [...]` list
20+
- A `RemoteActor` marker implementation
21+
- If `spawn = true`, a `RemotableActor` implementation and an inventory registration of the `spawn` function.
22+
23+
This enables the actor to be:
24+
- Spawned dynamically by name
25+
- Routed to via typed messages
26+
- Reflected on at runtime (for diagnostics, tools, and orchestration)
27+
28+
## Generated Implementations (simplified)
29+
```rust
30+
impl RemoteActor for ShoppingListActor {}
31+
32+
impl RemoteHandles<ShoppingList> for ShoppingListActor {}
33+
impl RemoteHandles<Signal> for ShoppingListActor {}
34+
35+
impl Binds<ShoppingListActor> for ShoppingListActor {
36+
fn bind(ports: &Ports<Self>) {
37+
ports.bind::<ShoppingList>();
38+
}
39+
}
40+
41+
impl Named for ShoppingListActor {
42+
fn typename() -> &'static str {
43+
"my_crate::ShoppingListActor"
44+
}
45+
}
46+
```
47+
If `spawn = true`, the macro also emtis:
48+
```rust
49+
impl RemotableActor for ShoppingListActor {
50+
fn gspawn(...) -> ...
51+
}
52+
```
53+
Along with a registration into inventory:
54+
```
55+
inventory::submit!(SpawnableActor {
56+
name: ...,
57+
gspawn: ...,
58+
get_type_id: ...,
59+
});
60+
```
61+
This allows the actor to be discovered and spawned by name at runtime.
62+
63+
## Summary
64+
65+
The `#[export]` macro makes an actor remotely visible, spawnable, and routable by declaring:
66+
- What messages it handles
67+
- What messages it handles
68+
- How to bind those messages
69+
- What its globally unique name is
70+
- (Optionally) how to spawn it dynamically
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# `#[forward]`
2+
3+
The `#[hyperactor::forward]` macro connects a user-defined handler trait implementation (like `ShoppingListHandler`) to the core `Handler<T>` trait required by the runtime.
4+
5+
In short, it generates the boilerplate needed to route incoming messages of type `T` to your high-level trait implementation.
6+
7+
## What it generates
8+
9+
The macro expands to:
10+
```rust
11+
#[async_trait]
12+
impl Handler<ShoppingList> for ShoppingListActor {
13+
async fn handle(&mut self, ctx: &Context<Self>, message: ShoppingList) -> Result<(), Error> {
14+
<Self as ShoppingListHandler>::handle(self, ctx, message).await
15+
}
16+
}
17+
```
18+
This avoids having to manually match on enum variants or duplicate message logic.
19+
20+
## When to use it
21+
22+
Use `#[forward(MessageType)]` when:
23+
24+
- You’ve defined a custom trait (e.g., `ShoppingListHandler`)
25+
- You’re handling a message enum (like `ShoppingList`)
26+
- You want the runtime to route messages to your trait automatically.
27+
28+
This is most often used alongside `#[derive(Handler)]`, which generates the corresponding handler and client traits for a user-defined message enum.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# `#[derive(HandleClient)]`
2+
3+
`#[derive(Handler)]` generates both the server-side handler trait (`ShoppingListHandler`) and the client-side trait definition (`ShoppingListClient`). However, it does not implement the client trait for any specific type.
4+
5+
This is where `#[derive(HandleClient)]` comes in.
6+
7+
## What It Adds
8+
9+
`#[derive(HandleClient)]` generates the following implementation:
10+
11+
```rust
12+
impl<T> ShoppingListClient for ActorHandle<T>
13+
where
14+
T: ShoppingListHandler + Send + Sync + 'static`
15+
```
16+
17+
This means you can call methods like `.add(...)` or `.list(...)` directly on an `ActorHandle<T>` without needing to manually implement the `ShoppingListClient` trait:
18+
19+
In other words, `HandleClient` connects the generated `ShoppingListClient` interface (from `Handler`) to the concrete type `ActorHandle<T>`.
20+
21+
## Generated Implementation (simplified)
22+
```rust
23+
use async_trait::async_trait;
24+
use hyperactor::{
25+
ActorHandle,
26+
anyhow::Error,
27+
cap::{CanSend, CanOpenPort},
28+
mailbox::open_once_port,
29+
metrics,
30+
Message,
31+
};
32+
33+
#[async_trait]
34+
impl<T> ShoppingListClient for ActorHandle<T>
35+
where
36+
T: ShoppingListHandler + Send + Sync + 'static,
37+
{
38+
async fn add(&self, caps: &impl CanSend, item: String) -> Result<(), Error> {
39+
self.send(caps, ShoppingList::Add(item)).await
40+
}
41+
42+
async fn remove(&self, caps: &impl CanSend, item: String) -> Result<(), Error> {
43+
self.send(caps, ShoppingList::Remove(item)).await
44+
}
45+
46+
async fn exists(
47+
&self,
48+
caps: &impl CanSend + CanOpenPort,
49+
item: String,
50+
) -> Result<bool, Error> {
51+
let (reply_to, recv) = open_once_port(caps)?;
52+
self.send(caps, ShoppingList::Exists(item, reply_to)).await?;
53+
Ok(recv.await?)
54+
}
55+
56+
async fn list(
57+
&self,
58+
caps: &impl CanSend + CanOpenPort,
59+
) -> Result<Vec<String>, Error> {
60+
let (reply_to, recv) = open_once_port(caps)?;
61+
self.send(caps, ShoppingList::List(reply_to)).await?;
62+
Ok(recv.await?)
63+
}
64+
65+
```
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# `#[derive(Handler)]`
2+
3+
The `#[derive(Handler)]` macro generates the infrastructure for sending and receiving typed messages in hyperactor. When applied to an enum like this:
4+
```rust
5+
#[derive(Handler)]
6+
enum ShoppingList {
7+
// Fire-and-forget messages
8+
Add(String),
9+
Remove(String),
10+
11+
// Request-response messages
12+
Exists(String, #[reply] OncePortRef<bool>),
13+
List(#[reply] OncePortRef<Vec<String>>),
14+
}
15+
```
16+
... it generates **two key things**:
17+
18+
### 1. `ShoppingListHandler` trait
19+
This trait defines a method for each variant, and a `handle` method to route incoming messages:
20+
```rust
21+
use async_trait::async_trait;
22+
use hyperactor::anyhow::Error;
23+
24+
#[async_trait]
25+
pub trait ShoppingListHandler: hyperactor::Actor + Send + Sync {
26+
async fn add(&mut self, ctx: &Context<Self>, item: String) -> Result<(), Error>;
27+
async fn remove(&mut self, ctx: &Context<Self>, item: String) -> Result<(), Error>;
28+
async fn exists(&mut self, ctx: &Context<Self>, item: String) -> Result<bool, Error>;
29+
async fn list(&mut self, ctx: &Context<Self>) -> Result<Vec<String>, Error>;
30+
31+
async fn handle(&mut self, ctx: &Context<Self>, msg: ShoppingList) -> Result<(), Error> {
32+
match msg {
33+
ShoppingList::Add(item) => {
34+
self.add(ctx, item).await
35+
}
36+
ShoppingList::Remove(item) => {
37+
self.remove(ctx, item).await
38+
}
39+
ShoppingList::Exists(item, reply_to) => {
40+
let result = self.exists(ctx, item).await?;
41+
reply_to.send(ctx, result)?;
42+
Ok(())
43+
}
44+
ShoppingList::List(reply_to) => {
45+
let result = self.list(ctx).await?;
46+
reply_to.send(ctx, result)?;
47+
Ok(())
48+
}
49+
}
50+
}
51+
}
52+
```
53+
Note:
54+
- `Add` and `Remove` are **oneway**: no reply port
55+
- `Exists` and `List` are **call-style**: they take a `#[reply] OncePortRef<T>` and expect a response to be sent back.
56+
57+
### 2. `ShoppingListClient` trait
58+
59+
Alongside the handler, the `#[derive(Handler)]` macro also generates a client-side trait named `ShoppingListClient`. This trait provides a convenient and type-safe interface for sending messages to an actor.
60+
61+
Each method in the trait corresponds to a variant of the message enum. For example:
62+
```rust
63+
use async_trait::async_trait;
64+
use hyperactor::anyhow::Error;
65+
use hyperactor::cap::{CanSend, CanOpenPort};
66+
67+
#[async_trait]
68+
pub trait ShoppingListClient: Send + Sync {
69+
async fn add(&self, caps: &impl CanSend, item: String) -> Result<(), Error>;
70+
async fn remove(&self, caps: &impl CanSend, item: String) -> Result<(), Error>;
71+
async fn exists(&self, caps: &impl CanSend + CanOpenPort, item: String) -> Result<bool, Error>;
72+
async fn list(&self, caps: &impl CanSend + CanOpenPort) -> Result<Vec<String>, Error>;
73+
}
74+
```
75+
76+
#### Capability Parameter
77+
Each method takes a caps argument that provides the runtime capabilities required to send the message:
78+
- All methods require `CanSend`.
79+
- Methods with `#[reply]` arguments additionally require `CanOpenPort`.
80+
81+
In typical usage, `caps` is a `Mailbox`.
82+
83+
#### Example Usage
84+
```rust
85+
let mut proc = Proc::local();
86+
let actor = proc.spawn::<ShoppingListActor>("shopping", ()).await?;
87+
let client = proc.attach("client").unwrap();
88+
89+
// Fire-and-forget
90+
actor.add(&client, "milk".into()).await?;
91+
92+
// With reply
93+
let found = actor.exists(&client, "milk".into()).await?;
94+
println!("got milk? {found}");
95+
```
96+
Here, actor is an `ActorHandle<ShoppingListActor>` that implements `ShoppingListClient`, and `client` is a `Mailbox` that provides the necessary capabilities.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Macros
2+
3+
This section documents the macros provided by hyperactor for actor and message integration.
4+
5+
These macros support a complete message-passing workflow: from defining message enums and generating client APIs, to routing messages and exporting actors for dynamic or remote use.
6+
7+
- [`#[derive(Handler)]`](handler.md) — generate message handling and client traits for actor enums
8+
- [`#[derive(HandleClient)]`](handle_client.md) — implement the generated client trait for `ActorHandle<T>`
9+
- [`#[derive(RefClient)]`](ref_client.md) — implement the generated client trait for `ActorRef<T>`
10+
- [`#[derive(Named)]`](named.md) — give a type a globally unique name and port for routing and reflection
11+
- [`#[export]`](export.md) — make an actor remotely spawnable and routable by registering its type, handlers, and and optionally spawnable from outside the current runtime
12+
- [`#[forward]`](forward.md) — route messages to a user-defined handler trait implementation
13+
14+
## Macro Summary
15+
16+
- **`#[derive(Handler)]`**
17+
Generates handler and client traits for a message enum.
18+
19+
- **`#[derive(HandleClient)]`**
20+
Implements the client trait for `ActorHandle<T>`.
21+
22+
- **`#[derive(RefClient)]`**
23+
Implements the client trait for `ActorRef<T>`.
24+
25+
- **`#[derive(Named)]`**
26+
Registers the type with a globally unique name and port.
27+
28+
- **`#[export]`**
29+
Makes an actor spawnable and routable via inventory.
30+
31+
- **`#[forward]`**
32+
Forwards messages to a user-defined handler trait implementation.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# `#[derive(Named)]`
2+
3+
The `#[derive(Named)]` macro implements the `hyperactor::Named` trait for a type, making it identifiable at runtime through a globally unique string and stable hash.
4+
5+
## The `Named` trait
6+
7+
The `hyperactor::data::Named` trait is the foundation of type identification in hyperactor. It gives each type a globally unique identity based on its name used in routing.
8+
```rust
9+
pub trait Named: Sized + 'static {
10+
fn typename() -> &'static str;
11+
fn typehash() -> u64 { ... }
12+
fn typeid() -> TypeId { ... }
13+
fn port() -> u64 { ... }
14+
fn arm(&self) -> Option<&'static str> { ... }
15+
unsafe fn arm_unchecked(self_: *const ()) -> Option<&'static str> { ... }
16+
}
17+
```
18+
19+
### Trait Methods
20+
21+
#### `typename() -> &'static str`
22+
23+
Returns the globally unique, fully-qualified type name for the type. This should typically look like:
24+
```rust
25+
"foo::bar::Corge<quux::garpy::Waldo>"
26+
```
27+
28+
#### `typehash() -> u64`
29+
30+
Returns a stable hash derived from `typename()`. This value is used for message port derivation.
31+
```rust
32+
cityhasher::hash(Self::typename())
33+
```
34+
35+
#### `typeid() -> TypeId`
36+
37+
Returns the Rust `TypeId` for the type (, which is only unique within a single binary).
38+
39+
#### `port() -> u64`
40+
41+
Returns a globally unique port number for the type:
42+
```rust
43+
Self::typehash() | (1 << 63)
44+
```
45+
Typed ports are reserved in the range 2^63 .. 2^64 - 1.
46+
47+
### `arm(&self) -> Option<&'static str>`
48+
49+
For enum types, this returns the name of the current variant, e.g., "Add" or "Remove".
50+
51+
### `unsafe fn arm_unchecked(ptr: *const ()) -> Option<&'static str>`
52+
53+
The type-erased version of `arm()`. Casts ptr back to `&Self` and calls `arm()`.
54+
55+
Useful for dynamic reflection when the concrete type isn’t statically known
56+
57+
### Runtime Registration
58+
59+
In addition to implementing the `Named` trait, the macro registers the type’s metadata at startup using the `inventory` crate:
60+
```rust
61+
const _: () = {
62+
static __INVENTORY: ::inventory::Node = ::inventory::Node {
63+
value: &TypeInfo { ... },
64+
...
65+
};
66+
// Registers the type info before main() runs
67+
#[link_section = ".init_array"]
68+
static __CTOR: unsafe extern "C" fn() = __ctor;
69+
};
70+
```
71+
This allows the type to be discovered at runtime, enabling:
72+
- Message dispatch from erased or serialized inputs
73+
- Introspection and diagnostics
74+
- Dynamic spawning or reflection
75+
- Tooling support
76+
77+
Types registered this way appear in the global `inventory::iter<TypeInfo>` set, which is how the hyperactor runtime locates known message types.

0 commit comments

Comments
 (0)