Rust port of the Dignus Actor Framework.
This repository is a work-in-progress port of the original C# Dignus.ActorServer project to Rust.
The goal is to keep the original actor runtime design as close as possible while adapting the implementation to Rust ownership, threading, and module rules.
Original C# project:
Dignus.ActorServer
├─ Dignus.Actor.Abstractions
├─ Dignus.Actor.Core
├─ Dignus.Actor.Network
└─ Benchmark
Rust port target:
Dignus.ActorServer.Rust
├─ actor-core
├─ actor-network
└─ benchmark
Current workspace status:
Dignus.ActorServer.Rust
└─ actor-core
The Rust implementation follows these rules:
- Keep the original C# structure where practical
- Avoid unnecessary abstraction during porting
- Prefer direct Rust equivalents over redesign
- Use Rust modules instead of C# namespaces
- Use
Arc,Mutex, atomics, and thread-local storage where required - Keep dispatcher execution dedicated to worker threads
- Hide runtime-only internals where Rust module visibility allows it
| C# | Rust |
|---|---|
namespace |
mod |
internal |
pub(crate) |
interface |
trait |
abstract class |
trait + ActorContext |
IDisposable |
explicit dispose() |
[ThreadStatic] |
thread_local! |
SemaphoreSlim |
custom Signal |
Thread |
std::thread::JoinHandle |
volatile bool |
AtomicBool |
Interlocked |
atomic operations |
ObjectPoolBase<T> |
Mutex<Vec<Arc<T>>> based pool |
SendOrPostCallback + state |
FnOnce() closure |
SynchronizationContext.Post |
dispatcher continuation scheduling |
The dispatcher keeps the original execution idea:
Post message
↓
Enqueue ActorMail
↓
Schedule ActorRunner on owner dispatcher
↓
Signal dispatcher thread
↓
Dispatcher drains scheduled queue
↓
ActorRunner creates and polls actor receive future
↓
Actor handles message
If the receive future returns Pending, mailbox processing for that actor is stopped until the pending receive future completes.
The C# version supports dispatcher switching through:
await ActorAwait.Join(actor);The Rust port keeps the same concept:
ActorAwait::join(actor).await;This schedules the continuation onto the target actor dispatcher.
ActorAwait::join(target).await is a dispatcher context switch. It does not transfer actor ownership.
If actor state is accessed after a dispatcher switch, the actor implementation is responsible for returning to the correct dispatcher or validating the context.
The Rust port keeps the C# actor continuation model where practical.
ActorBase::on_receive may return a future that borrows actor state across await points. This allows actor implementations to keep a C#-like flow:
on_receive
↓
access actor state
↓
await
↓
continue receive logic
↓
access actor state again
When a receive continuation is moved to another dispatcher using ActorAwait::join(target).await, actor-owned mutable state should not be accessed unless the continuation has returned to the owning dispatcher or the implementation explicitly validates the context.
Pending receive polling is separated from normal mailbox execution.
Receive future returns Pending
↓
Store pending receive future
↓
Stop mailbox processing for this actor
↓
Wake schedules pending receive task
↓
Pending receive task polls only the pending future
↓
Ready schedules ActorRunner back to owner dispatcher
A wake does not directly schedule the normal ActorRunner. This prevents mailbox messages from being processed on a dispatcher that only woke the pending receive future.
PendingReceiveState tracks pending receive transitions such as idle, scheduled, polling, and schedule-requested states. This avoids lost wakes and prevents concurrent polling of the same receive future.
Kill() changes the actor lifecycle state to killing and rejects new messages.
If the actor currently has a pending receive future, the runtime does not forcibly cancel it. Finalization occurs only after the pending receive future completes and the actor runner returns to the owner dispatcher.
If a receive future never completes, actor finalization also does not complete. This follows the original C# runtime semantics and is considered the actor implementation's responsibility.
The runtime relies on the following internal invariants:
- Only one receive future may be active per actor
- A pending receive future must not be polled concurrently
- Mailbox processing is stopped while a receive future is pending
- Pending receive polling is handled by a pending receive task, not by the normal mailbox runner
- Pending receive completion schedules the normal actor runner back to the owner dispatcher
- Actor finalization must not run while a receive future is still pending
- Actor implementations should validate dispatcher context before accessing actor-owned mutable state after dispatcher switching
-
ActorSystem- owns dispatchers
- spawns actors
- routes posts and kills
- disposes actors and dispatchers
-
ActorBase- user-implemented actor trait
- defines
on_receive - optionally defines
on_kill
-
ActorContext- stores runtime actor state
- owns dispatcher reference
- owns self actor reference
- provides context verification
-
ActorRef- posts messages
- posts actor mail
- kills actor
-
ActorRunner- owns actor instance
- owns mailbox
- creates actor receive future
- schedules owner dispatcher mailbox execution
- finalizes actor kill
-
PendingReceiveState- tracks pending receive future state
- prevents concurrent pending future polling
- preserves wake requests that occur while polling
-
ActorPendingReceiveTask- polls only the stored pending receive future
- never processes actor mailbox messages
-
ActorDispatcher- owns scheduled actor queue
- owns dispatcher thread
- runs scheduled actor tasks
-
ActorAwait- switches async continuation to another actor dispatcher
From the workspace root:
cargo checkOr from actor-core:
cargo checkRoot Cargo.toml:
[workspace]
resolver = "2"
members = [
"actor-core"
]actor-core/Cargo.toml:
[package]
name = "actor-core"
version = "0.1.0"
edition = "2021"
[dependencies]Some C# features do not have direct Rust standard library equivalents.
Examples:
abstract classprotectedSynchronizationContextThread.Priority- Background thread setting
- C# reference-based object pooling
These are adapted only where the current Rust runtime structure requires them.
Licensed under the MIT License.
See LICENSE in the project root.