Если б миром правил краб бы —
правил если б миром краб б,
я б сказал бы, что пора бы
снаряжать большой корабль,
отправляться б за моря бы
посоветовал тогда б
я б — непросто жить у краба
под клешней, как слабый раб.
Вскрытие показало, что я немного отстал от жизни, и язык программирования «Кровожадный краборжав» уже вполне себе пригоден для написания простеньких хелоуворлдов…
Ладно. В кои-то веки обойдусь без ёрничанья. Официально заявляю: я написал свою первую библиотеку на расте и мне понравилось. Раст — несомненно местами красивый и приятный для работы язык. Написание кода укладывается в зелёный диапазон плотности wtf/sec, а инструментарий заслуживает всяческих похвал (кроме кросс-публикации документации на https://docs.rs/, которая в 2025 году занимает час — хоть донаты шли, ей-богу).
Итак, я написал библиотеку, которая позволит эрлангистам проще вкатываться в раст. Акторная модель притворяется краденой из эрланга, с примитивами GenServer и GenStatem, с деревьями супервизоров, с боксированными сообщениями, мэйлбоксами, и привычной терминологией. Библиотека названа joerl, светлой памяти Джо Армстронга, с которым мне посчастливилось быть знакомым, и который сильнейшим образом повлиял на менталитет разработчика во мне.
Но довольно болтовни!
Результат бенчмарков:
• Actor spawn: ~6.15 µs per spawn
• GenServer calls: ~85 µs for 10 synchronous calls
• GenServer casts: ~6.16 ms for 10 async casts (including wait)
• GenStatem transitions: ~156 µs for 10 state transitions (20 events)
• Supervisor creation: ~11-12 ms for 5-20 childrenНеплохо!
Примеры использования можно найти в репе https://github.com/am-kantox/joerl/tree/main/joerl/examples — покажу тут, чисто для затравки, самый простой пример — счётчик инкапсулированный в процесс.
// стейт «процесса» (рарутины)
struct Counter {
count: i32,
}
// имплементация трейта «примитивный процесс» (рарутина)
#[async_trait]
impl Actor for Counter {
async fn started(&mut self, ctx: &mut ActorContext) {
println!("Counter actor started with pid {}", ctx.pid());
}
async fn handle_message(&mut self, msg: Message, ctx: &mut ActorContext) {
if let Some(cmd) = msg.downcast_ref::<&str>() {
match *cmd {
"increment" => {
self.count += 1;
println!("[{}] Count incremented to: {}", ctx.pid(), self.count);
}
"decrement" => {
self.count -= 1;
println!("[{}] Count decremented to: {}", ctx.pid(), self.count);
}
"get" => {
println!("[{}] Current count: {}", ctx.pid(), self.count);
}
"stop" => {
println!("[{}] Stopping counter", ctx.pid());
ctx.stop(joerl::ExitReason::Normal);
}
_ => {
println!("[{}] Unknown command: {}", ctx.pid(), cmd);
}
}
}
}
async fn stopped(&mut self, reason: &joerl::ExitReason, ctx: &mut ActorContext) {
println!("[{}] Counter stopped with reason: {}", ctx.pid(), reason);
}
}Пора запускать и проверять:
#[tokio::main]
async fn main() {
println!("=== Counter Actor Example ===\n");
let system = ActorSystem::new();
let counter = system.spawn(Counter { count: 0 });
// Send some messages
counter.send(Box::new("increment")).await.unwrap();
counter.send(Box::new("increment")).await.unwrap();
counter.send(Box::new("increment")).await.unwrap();
counter.send(Box::new("get")).await.unwrap();
counter.send(Box::new("decrement")).await.unwrap();
counter.send(Box::new("get")).await.unwrap();
counter.send(Box::new("stop")).await.unwrap();
// Wait for actor to process messages
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
println!("\nExample completed!");
}Давайте посмотрим, что там:
❯ cargo run --example counter
Compiling joerl v0.1.0 (/opt/Proyectos/Rust/joerl)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
Running `target/debug/examples/counter`
=== Counter Actor Example ===
Counter actor started with pid <0.1.0>
[<0.1.0>] Count incremented to: 1
[<0.1.0>] Count incremented to: 2
[<0.1.0>] Count incremented to: 3
[<0.1.0>] Current count: 3
[<0.1.0>] Count decremented to: 2
[<0.1.0>] Current count: 2
[<0.1.0>] Stopping counter
[<0.1.0>] Counter stopped with reason: normal
Example completed!Вот, как-то так.
Итоги: я не писал ничего серьёзного на расте до сегодняшнего дня. Имплементация библиотеки заняла у меня часов восемь довольно напряженной работы. Помогло то, что я досконально знал, как это реализовано в эрланге, поэтому по сути не сочинял код с нуля, а переводил свои знания из головы в редактор. Документация и бо́льшая часть тестов написаны моим стажёром Т1009 (это контаминация терминатора и Т9).
Если без фанатизма, присущего, почему-то, адептам раста, — язык мне понравился. Я интуитивно понимал, что гуглить, когда спотыкался о незнакомый синтаксис (а чаще — просто догадывался). Местами (в тестах и бенчмарках, не в основном коде, конечно) я плевал на понятия и просто писал говнокод, если утыкался в непонятные мне по неопытности ворнинги:
#[derive(Message)]
#[rtype(result = "()")]
pub struct PingMsg(#[allow(dead_code)] pub usize);Директив слишком много, должно быть возможно проще, но мне уже лень. Ну и, напоследок, конечно битва за пояс племени якодзун с Актиксом.
1. Actor Spawn Time
◦ joerl: 6.04 µs
◦ actix: 9.56 µs
◦ Winner: joerl is ~37% faster ✓
2. Message Send (10 messages)
◦ joerl: 3.11 ms
◦ actix: 3.48 ms
◦ Winner: joerl is ~11% faster ✓
3. Message Send (100 messages)
◦ joerl: 3.13 ms
◦ actix: 3.51 ms
◦ Winner: joerl is ~11% faster ✓
4. Message Send (1000 messages)
◦ joerl: 3.44 ms
◦ actix: 3.71 ms
◦ Winner: joerl is ~7% faster ✓
5. Throughput (1000 messages)
◦ joerl: 11.86 ms
◦ actix: 11.64 ms
◦ Winner: actix is ~2% faster ✗
6. Multiple Actors (10 actors)
◦ joerl: 6.18 ms
◦ actix: 6.57 ms
◦ Winner: joerl is ~6% faster ✓
7. Multiple Actors (50 actors)
◦ joerl: 6.23 ms
◦ actix: 6.64 ms
◦ Winner: joerl is ~6% faster ✓После этого мне язык понравился еще больше.
Документация: https://docs.rs/joerl/latest/joerl/
Еще раз вставлю сюда КДПВ, уж очень она мне пришлась по душе.

