A high-performance 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 original C# runtime is designed for high-throughput local actor message processing and has reached around 250M ~ 270M msg/s in local in-process ping-pong benchmarks, with a best observed result of 277M msg/s.
The goal of this Rust port is to keep the original actor runtime design as close as possible while adapting the implementation to Rust ownership, threading, lifetime, and module rules.
Original C# project:
Dignus.ActorServer
├─ Dignus.Actor.Abstractions
├─ Dignus.Actor.Core
├─ Dignus.Actor.Network
└─ Benchmark
Rust port target:
Dignus.ActorServer.Rust
├─ dignus-actor-core
├─ actor-network
└─ benchmark
Current workspace status:
Dignus.ActorServer.Rust
├─ dignus-actor-core
└─ benchmarks (ping-pong: dignus-actor-core + actix / ractor / kameo / xtra / coerce)
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
- Preserve the original actor scheduling and dispatcher model
| C# | Rust |
|---|---|
namespace |
mod |
internal |
pub(crate) |
interface |
trait |
abstract class |
trait + actor context |
IDisposable |
explicit dispose() |
[ThreadStatic] |
thread_local! |
SemaphoreSlim |
custom signal |
Thread |
std::thread::JoinHandle |
volatile bool |
AtomicBool |
Interlocked |
atomic operations |
SynchronizationContext.Post |
dispatcher continuation scheduling |
The dispatcher keeps the original execution idea:
Post message
↓
Enqueue actor mail
↓
Schedule actor runner on owner dispatcher
↓
Signal dispatcher thread
↓
Dispatcher drains scheduled queue
↓
Actor runner executes actor receive logic
↓
Actor handles message
If the receive logic becomes pending, mailbox processing for that actor is stopped until the pending receive 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.
Actor receive logic may suspend and resume while preserving actor execution order.
receive message
↓
access actor state
↓
await
↓
resume receive logic
↓
access actor state again
While an actor receive operation is pending, the actor does not process the next mailbox message.
Pending receive handling is part of the actor execution model.
Receive operation becomes pending
↓
Store pending receive state
↓
Stop mailbox processing for this actor
↓
Wake schedules actor execution again
↓
Pending receive is checked first
↓
Ready resumes normal mailbox processing
This preserves the single-message execution model of the original C# actor runtime.
Kill() changes the actor lifecycle state and rejects new messages.
If the actor currently has a pending receive operation, the runtime does not forcibly cancel it. Finalization occurs only after the pending receive completes and the actor returns to its owner dispatcher.
If a receive operation 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 operation may be active per actor
- Mailbox processing is stopped while receive logic is pending
- Pending receive must not be polled concurrently
- Actor finalization must not run while receive logic is still pending
- Actor implementations should validate dispatcher context before accessing actor-owned mutable state after dispatcher switching
- owns dispatchers
- spawns actors
- routes posts and kills
- disposes actors and dispatchers
- defines actor receive behavior
- stores runtime actor context
- provides self reference and dispatcher verification
- posts messages
- posts actor mail
- kills actor
- owns actor instance
- owns mailbox
- executes actor receive logic
- manages pending receive state
- finalizes actor kill
- owns scheduled execution queue
- owns dispatcher thread
- runs scheduled actor work
- switches async continuation to another actor dispatcher
Reproducible in-process ping-pong throughput benchmarks live in
benchmarks/, including the same benchmark implemented on five
mainstream Rust actor frameworks for comparison.
Same machine, same methodology (348 pairs / 1,000 pipeline / 10s, fire-and-forget), 3-run ranges:
| Framework | Throughput (msg/s) |
|---|---|
dignus-actor-core (this project) |
~365–400M |
| actix 0.13 | ~195–206M |
| ractor 0.14 | ~107M |
| kameo 0.20 | ~70–90M |
| xtra 0.6 | ~83–85M |
| coerce 0.8 | ~58–80M |
Among the Rust actor frameworks tested, dignus-actor-core is the fastest on this
benchmark, using its own std::thread dispatcher. The original C# runtime
(~620–640M on the same machine) reaches higher.
benchmarks/README.md for full
methodology, versions, environment, how to reproduce, and caveats.
This benchmark result is from the original C# Dignus.ActorServer implementation.
It is included as a baseline target for the Rust port.
Local in-process ping-pong benchmark.
Test environment:
CPU: Intel Core i5-12400F
RAM: 32 GB
OS: Windows x64
Benchmark conditions:
Actor Pair Count: 348
Actual Actor Count: 696
Pipeline Size Per Pair: 1,000
Benchmark Duration: 10 seconds
Counter: per-actor local counter, summed after completion
Best observed result:
Processed Messages: 2,907,908,768
Elapsed: 10.475 sec
Throughput: 277,599,493 msg/s
Representative result:
Throughput: around 250M ~ 270M msg/s
⚠️ Different machine — do not compare this number directly with the Benchmarks table above. This ~277M figure is from the original author's Intel Core i5-12400F (6 cores / 12 threads), a weaker CPU with far fewer cores. The throughput scales with core count (more cores → more pairs run in parallel). For an apples-to-apples comparison, the same C# benchmark was re-run on the same 32-thread machine as the Rust benchmarks and reaches ~620–640M msg/s (seebenchmarks/). On that same machine, C# (~640M) is still ahead of this Rust port (~380M).
Notes:
- This benchmark measures local actor message throughput only.
- It does not include network, serialization, database access, logging, or game logic.
- Per-message global synchronization was intentionally avoided.
- Results may vary depending on CPU scheduling, background processes, power mode, GC timing, and runtime warm-up.
- This is not the Rust port benchmark result.
From the workspace root:
cargo checkOr from dignus-actor-core:
cargo checkRoot Cargo.toml:
[workspace]
members = [
"dignus-actor-core"
]dignus-actor-core/Cargo.toml:
[package]
name = "dignus-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.