Если б миром правил краб бы —
правил если б миром краб б,
я б сказал бы, что пора бы
снаряжать большой корабль,
отправляться б за моря бы
посоветовал тогда б
я б — непросто жить у краба
под клешней, как слабый раб.
Вскрытие показало, что я немного отстал от жизни, и язык программирования «Кровожадный краборжав» уже вполне себе пригоден для написания простеньких хелоуворлдов…
Ладно. В кои-то веки обойдусь без ёрничанья. Официально заявляю: я написал свою первую библиотеку на расте и мне понравилось. Раст — несомненно местами красивый и приятный для работы язык. Написание кода укладывается в зелёный диапазон плотности 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/
Еще раз вставлю сюда КДПВ, уж очень она мне пришлась по душе.

