Если б миром правил краб бы —
правил если б миром краб б,
я б сказал бы, что пора бы
снаряжать большой корабль,
отправляться б за моря бы
посоветовал тогда б
я б — непросто жить у краба
под клешней, как слабый раб.

— «Краб на корабль карабкался, короб корабля корябал…»

Вскрытие показало, что я немного отстал от жизни, и язык программирования «Кровожадный краборжав» уже вполне себе пригоден для написания простеньких хелоуворлдов…

Ладно. В кои-то веки обойдусь без ёрничанья. Официально заявляю: я написал свою первую библиотеку на расте и мне понравилось. Раст — несомненно местами красивый и приятный для работы язык. Написание кода укладывается в зелёный диапазон плотности 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/

Еще раз вставлю сюда КДПВ, уж очень она мне пришлась по душе.

Так выглядит «Кровожадный Ржавокраб», если верить Gemini.
Так выглядит «Кровожадный Ржавокраб», если верить Gemini.