Learning Rust Crates

libraries to investigate

  • Serde is a powerful serialization and deserialization framework for Rust.
  • Rayon for data-parallelism via paralellised iterators.
  • Tokio is an asynchronous runtime for Rust, providing an event-driven platform for building fast, reliable, and lightweight network applications
  • rig-core
  • Chrono
  • smithy-rs: you really hate manually implementing OpenAPI specs, AWS open sourced their rust server generator. You model your API in smithy, and then you can code generate an openapi model and rust server for it.
  • leptos: webdevelopment .
  • Anyhow is used for flexible error handling in Rust.
  • llm
    • rust-genai abstraction for ollama and openai / Claude
    • [-] llm-chain llm-chain is a collection of Rust crates designed to help you create advanced LLM applications such as chatbots, agents, and more.
      • Doesn’t support newer GGUF models
    • [-] llm_cpp for synchronous calling of GGUF models
      • works, but managing models and getting the right / updated versions is a hassle and I don’t need the specialized parameter options
  • databases
    • neon for postgres database with s3 compatible object storage for persistence
    • Diesel is a safe, extensible ORM (Object-Relational Mapper) and Query Builder for Rust.
  • Rowan for parsing. 
  • cryptography
  • web servers
    • Rocket is a web framework for Rust that makes it simple to write fast, secure web applications
    • Axum for building web servers on top of Hyper (which is now 1.0!). 
  • MVC frameworks
    • loco - complete batteries included MVC framework
  • Jiff for dates. 
  • logging
    • log and env_logger
  • Tantivy as an alternative to Lucene.
  • app development
    • tauri
    • rust bridge flutter
  • gui
    • iced for commandline GUI
    • tauri for web based gui
    • slint for non webbased gui
  • embassy-rs embedded os

rayon

  • install the crate, then use rayon::prelude::*
  • instantly speed up iterators with rayon by using par_iter() instead of iter()

serde

  • use #[derive(Serialize, Deserialize)] enable structs to be enriched by serde
  • use serde_json:to_string to serialize struct into json

tokio

tokio basics

  • tokio is a toolkit for async runtimes and networking applications
  • use spawn thread around socket process to create multithreading responses
use tokio::net::*;
use mini_redis::{Connection, Frame};

#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("localhost:6379").await.unwrap();
    println!("mini_redis listening on port {:?}", listener.local_addr());
    loop {
        let (socket, _) = listener.accept().await.unwrap();
        tokio::spawn(async move {
            process(socket).await;
        });
    }
}
async fn process(socket: TcpStream) {
    let mut connection = Connection::new(socket);
    if let Some(frame) = connection.read_frame().await.unwrap() {
        println!("{:?}", frame);
        let response = Frame::Error("unimplemented".to_string());
        connection.write_frame(&response).await.unwrap();
    }

}
  • when passing process functions to tokio’s spawn, under the hood “tasks” are created.
    • tasks are very lightweight (64bytes of mem)
    • tasks will be scheduled on the same or other thread
    • feel free to spawn thousands / millions of tasks
  • tasks should not reference memory with a shorter lifetime than static
  • use move in the async block to make passed values get moved

tokio shared state with sync mutex

  • if you want to use data amongst threads, use synchronisation such as Arc
  • using a synchronous mutex from within asynchronous code is fine as long as “contention” remains low
    • contention in this case means different threads and tasks fighting to access the same (locked) value
  • mutex guards should die before calling new asynchronous await functions. We can achieve this by creating blocks for the mutex guard:
async fn increment_and_do_stuff(mutex: &Mutex<i32>) { 
	{ 
		let mut lock: MutexGuard<i32> = mutex.lock().unwrap(); 
		*lock += 1; 
	} // lock goes out of scope here 
	do_something_async().await; 
}

tokio channels (message passing)

  • if we want to share sync resources amongst tokio tasks, we use a channel to manage exclusive access
  • its like a mini queueing system
  • variants of the channels:
    • mpsc: multi-producer, single-consumer
    • oneshot: single-producer, single-consumer
    • broadcast: multi-producer, multi-consumer (each receiver sees every value)
    • watch: multi-producer, multi-consumer (no history is kept, receivers only see most recent)
  • create a channel that can hold 32 messages with let (tx, mut rx) = mpsc::channel(32)

tokio stream piping

  • io::copy can be used to copy input from one stream into the other stream
  • the tcplistener implements both reader and writer, but we can’t borrow the reference to the listener twice.
    • We can use socket.split to separate the reader and writer and use io::copy to copy the stream to the output
  • we can also copy by hand, using socket.read() and socket.write_all
  • don’t forget to handle EOF

tokio framing

  • framing is taking a byte stream and converting it into a stream of frames
  • if you are writing an exact data packet, you can use a write frame function on a struct that matches the frame and writes to a stream byte by byte

tokio timeouts

  • you can specify timeouts on tokio calls to avoid waiting to long for http requests for instance

log facade and env_logger logging

  • use facade log to log log::info!("opened wordlist file: {:?}", wordlist_file);
  • use implementation env_logger using env_logger::init();
  • use structured_logging crate for structured logging, e.g. adding parseable values