pickle your rust to make it into shiny chrome
CSP/Actor model architecture for Rust applications.
Generic building blocks for actor-model concurrency. Each type maps to a common inter-thread communication pattern, eliminating the per-actor boilerplate of defining message enums, oneshot channels, and caller wrappers.
The big win: actors own their state inside a spawned thread.
Communication is by value through channels - moved, not borrowed.
Callers hold typed channel handles that are Clone + Send. No
Arc<Mutex<T>>, no lifetime annotations, no &self vs &mut self
disputes. Rust's ownership system does exactly what you want when
every interaction is an ownership transfer.
| Type | Caller | Actor | Pattern |
|---|---|---|---|
Func<Req, Resp> | call(req) -> resp | msg.req, msg.reply(resp) | function call |
Query<Resp> | call() -> resp | msg.reply(resp) | getter |
Proc<Req> | call(req) | msg.req, msg.done() | setter / command |
Signal | call() | msg.done() | trigger / reset |
Inbox<T> | send(v) / try_send(v) | inbox.recv() | fire-and-forget |
Lifecycle | stop() | stopping() | shutdown |
All synchronous types use rendezvous channels (zero-capacity). The caller blocks until the actor processes the message. This eliminates races where a buffered write appears to succeed but the actor hasn't seen it yet.
use pickle::{Signal, Query, Lifecycle, spawn};
use crossbeam_channel::select;
struct Counter {
inc: Signal,
get: Query<i32>,
lc: Lifecycle,
}
impl Counter {
fn new() -> Self {
let mut c = Counter {
inc: Signal::new(),
get: Query::new(),
lc: Lifecycle::new(),
};
let inc = c.inc.receiver();
let get = c.get.receiver();
let stopping = c.lc.stopping();
spawn(&mut c.lc, move || {
let mut n: i32 = 0;
loop {
select! {
recv(stopping) -> _ => return,
recv(inc) -> msg => {
let msg = msg.unwrap();
n += 1;
msg.done();
},
recv(get) -> msg => {
let msg = msg.unwrap();
msg.reply(n);
},
}
}
});
c
}
fn inc(&self) { self.inc.call(); }
fn get(&self) -> i32 { self.get.call() }
}
RwLock. Allcoordination is via channels and select.
interact through owned channel handles. Lifetimes don't appear.
happens-before: when call() returns, the actor has processed the
message. Use Inbox for intentionally async paths.
Callers get copies via response channels, never references.
implement, no framework, no registration.
lookup, filtered query)
config)
(set value, record event)
flush, increment)
Use try_send to drop on backpressure.
This library implements the intra-domain actor patterns described in Moxie Architecture Patterns - specifically the actor loop (section 3.1), typed operation channels (section 3.2), the synchronous-mutation rule (section 3.3), and context-aware calls (section 3.6). The five channel types are a direct realization of the taxonomy from section 3.2.
Same architecture, three languages:
go get git.smesh.lol/actorpickle = { git = "https://git.smesh.lol/pickle" }npm install anneal[dependencies]
pickle = { git = "https://git.smesh.lol/pickle" }
AGPL-3.0-or-later