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

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

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

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

Комментарии (5)


  1. wilcot
    05.12.2025 18:00

    Спасибо за статью! У меня появилось желание изучить тему акторов поподробнее, выглядит интересно.

    Посмотрел результаты и код бенчмарка. Подозреваю, что бенчмарки все-таки протестировали время инициализации runtime (tokio vs actix). Выглядит так, что 10, 100, 1000 сообщений не достаточно для того, чтобы увидеть заметный результат.


    1. cupraer Автор
      05.12.2025 18:00

      Да бенчмарки никогда ничего из реального мира эмулировать не могут, только показать бинарное значение «на глаз»: годится / надо доработать. Меня результат устроил. Отдохну и допишу бенчмарки на миллионах сообщений, но сомневаюсь, что что-то прям изменится: узким звеном акторной модели были есть и будут хендлеры сообщений (читай: mailbox).

      tokio vs actix

      Не очень понял. Actix не переизобретал велосипед, там тоже токио под капотом.


  1. voidptr0
    05.12.2025 18:00

    Век живи - век учи новые языки. Мне вот понадобился недавно язык с goto. Искал-искал, а нашел, к своему стыду, FreeBasic. И залип. А в нем и goto и классы и namespaces и Clib и еще куча. Всего. Понятно, что писать на нем ничего серьезного не планирую за пределами текущей задачи, но очень впечатлило.


    1. cupraer Автор
      05.12.2025 18:00

      Я не «учу» новые языки, я просто взял поиграться еще один из хайповых. Тут как с человеческими языками: когда свободно говоришь на полутора десятках из всех возможных языковых групп — на уровень C1 в еще одном нужно пару дней.

      понадобился недавно язык с goto

      А что не так с си и паскалем?


    1. Lewigh
      05.12.2025 18:00

      Мне вот понадобился недавно язык с goto. Искал-искал

      C# язык в котором есть практически все, в том числе goto