pretzelhammer/rust-blog

repository·master·Indexed 27 days ago

https://github.com/pretzelhammer/rust-blog

A comparative guide and implementation of RESTful APIs in Rust, exploring synchronous architectures (Diesel/Rocket) and asynchronous architectures (sqlx/actix-web). The documentation includes detailed tutorials on building a chat server with Tokio, covering TcpListener, broadcast channels, tokio::select!, pinning for non-cancel-safe futures, and strategies for avoiding deadlocks in asynchronous Rust.

Tokens
61.2K
Snippets
164
Records
249
Agent score
89%

What's inside rust-blog

  1. Understand Sizedness in Rust

    master

    In Rust, a type is sized if its size in bytes can be determined at compile-time. This allows instances of the type to be allocated on the stack and passed by value.

    If a type's size cannot be determined at compile-time, it is an unsized type (also known as a DST or Dynamically-Sized Type). Unsized types cannot be placed on the stack and must be passed around by reference.

    Terminology Summary

    PhraseMeaning
    sized typeType with a known size at compile time
    unsized type / DSTDynamically-sized type; size not known at compile time
    unsized coercionCoercing a sized type into an unsized type
    ZSTZero-sized type; instances are 0 bytes in size
    thin pointerA pointer that is 1 width (e.g., pointer to a sized type)
    fat pointerA pointer that is 2 widths (e.g., pointer to an unsized type)
    sliceA double-width pointer to a dynamically sized view into an array
    trait objectA double-width pointer to data and a vtable
    unsized struct pointerA double-width pointer to struct data and the struct's size
  2. Understand WebAssembly (wasm32) Stack-Based ISA

    master

    WebAssembly (wasm32) is a stack-based Instruction Set Architecture (ISA). Instructions operate by pushing and popping values from an implicit stack.

    Key concepts:

    • Stack Operations: Instructions like i32.const push values onto the stack, while arithmetic instructions like i32.add pop values, perform the operation, and push the result back.
    • S-expressions: WebAssembly Textual Format (WAT) supports S-expression syntax for improved readability, e.g., (i32.add (i32.const 4) (i32.const 5)).
    • Discarding Values: Use the drop instruction to remove a value from the stack if it is not needed.
    • Strong Typing: All values and instructions are strongly typed. The four available data types are i32, i64, f32, and f64. Mixing types results in a compile error.
    i32.const 4     ;; push value 4 onto stack
    i32.const 5     ;; push value 5 onto stack
    i32.add         ;; pop 2 values from stack, add them, push result onto stack
    
    ;; Alternatively using S-expressions:
    (i32.add (i32.const 4) (i32.const 5))
    
    ;; Discarding a value:
    i32.const 14
    drop
  3. Summary of common Rust lifetime misconceptions

    master

    This document summarizes key technical nuances regarding Rust lifetimes to avoid common misunderstandings:

    Types and Lifetimes

    • T is a superset of &T and &mut T.
    • &T and &mut T are independent of each other.
    • T: 'static should be read as "T is bounded by the 'static lifetime".
    • If T: 'static, T can be an owned type OR a borrowed type that owns a 'static lifetime.
    • T: 'static implies T contains owned data, which means it:
      • Can be dynamically allocated at runtime.
      • Does not necessarily need to be valid for the entire duration of the program.
      • Is safe and can be modified freely.
      • Can be dynamically dropped at runtime.
      • Can have lifetimes of different durations.
    • T: 'a is more general and flexible than &'a T.
    • T: 'a can be an owned type containing references, or a type that accepts references.
    • &'a T only accepts references.
    • Since 'static >= 'a for any 'a, if T: 'static, then T: 'a is also true.

    Generics and Inference

    • Most Rust code is generic; lifetimes are often elided (omitted) everywhere.
    • Rust's lifetime elision rules are not always correct for every situation.
    • Trait objects have inferred default lifetime bounds.
    • Lifetime elision rules for trait objects are not always correct.
    • A function with the signature for<'a, T> fn() -> &'a T is more flexible than for<T> fn() -> &'static T.

    Best Practices and Compiler Behavior

    • Use descriptive names for lifetime annotations.
    • Be intentional about where and why you apply explicit lifetime annotations.
    • The Rust compiler's suggested fixes aim to make code compile, but they may not satisfy the actual semantic requirements of your program.
    • Avoid reborrowing a mutable reference as a shared reference if it leads to unexpected behavior.
    • Even if a mutable reference is dropped, its lifetime does not end if it was reborrowed.
    • Rust does not know the semantic meaning of your program better than you do.
  4. Project Overview: RESTful API in Sync & Async Rust

    master

    This project demonstrates how to implement a RESTful API for a Kanban-style project management application using two different approaches in Rust:

    1. Sync Implementation: Uses r2d2 for connection pooling, Diesel for ORM/SQL queries, and Rocket for HTTP routing.
    2. Async Implementation: Uses sqlx for SQL queries and connection pooling, and actix-web for HTTP routing.

    The API supports creating/listing/deleting boards and cards, managing card statuses, and includes token-based authentication.

  5. Quickstart guide for learning Rust in 2024

    master

    This opinionated guide outlines a path to go from a Rust beginner to an advanced beginner in approximately 19 to 30 hours. The recommended sequence is:

    1. Read A half hour to learn Rust (30-60 mins).
    2. Complete rustlings (2-3 hours).
    3. Code for 10 hours using resources like 100 Exercises to Learn Rust, the Exercism Rust track, or Advent of Code.
    4. Read Common Rust Lifetime Misconceptions (30-60 mins).
    5. Code for another 10 hours.
    6. Read Tour of Rust's Standard Library Traits (2-4 hours).
  6. Quickstart guide for learning Rust via hands-on coding

    master

    If you want to learn Rust using a practical, hands-on approach rather than reading theoretical documentation first, follow these paths based on your current level:

    For Total Newbies

    1. Read A half-hour to learn Rust by fasterthanlime.
    2. Complete the exercises in the Rustlings repository.

    For Beginners

    1. Start the Exercism Rust Track.
    2. Use the Rust Standard Library Docs to navigate and find practical examples.
    3. Use Rust by Example as a high-level reference for syntax and features.
    4. Only consult The Book when you need a deep dive into a specific concept.
    5. Review other members' solutions on Exercism (sorted by most-starred) to learn idiomatic patterns.

    For Advanced Beginners

    1. Work through the Advent of Code 2018 exercises.
    2. Compare your solutions to BurntSushi's Advent of Code 2018 Rust solutions to learn clean, readable, and idiomatic Rust code.
  7. Manage SQL schema migrations with diesel-cli

    master

    Use diesel-cli to bootstrap your database, generate migration files, and execute them. diesel-cli uses the DATABASE_URL environment variable (which can be loaded from an .env file) to connect to your database.

    • diesel setup: Initializes the database and creates a migrations directory.
    • diesel migration generate <name>: Creates a new migration directory containing up.sql (for schema changes) and down.sql (for reverting changes).
    • diesel migration run: Executes all pending migrations in chronological order and updates the Diesel schema file.
    diesel setup
    diesel migration generate create_boards
    diesel migration run
  8. Setup tokens table for authentication with Diesel

    master

    To support token-based authentication, create a tokens table using Diesel migrations. The table requires an id (TEXT PRIMARY KEY) and an expired_at (TIMESTAMP WITH TIME ZONE) field.

    diesel migration generate create_tokens
    # Edit the generated SQL files
    diesel migration run

    -- create_tokens up.sql CREATE TABLE IF NOT EXISTS tokens ( id TEXT PRIMARY KEY, expired_at TIMESTAMP WITH TIME ZONE NOT NULL );

    -- create_tokens down.sql DROP TABLE IF EXISTS tokens;

  9. Run and test chat server examples

    master

    If you have cloned the chat-server repository, you can run specific tutorial steps and interact with them using the just command.

    • Run a specific example: just example {number} (e.g., just example 01)
    • Connect via telnet: just telnet

    Source code for these examples is located in the examples/ directory of the chat-server repository.

  10. Opt-out of the default `Sized` bound in generics using `?Sized`

    master

    In Rust, all generic type parameters are automatically bound with the Sized trait by default. This means the compiler expects the type T to have a known size at compile time.

    If you are writing a generic function that takes a type T behind a pointer (such as &T, Box<T>, or Rc<T>), you should often opt-out of this default bound using the ?Sized syntax (pronounced "optionally sized" or "maybe sized"). This allows the function to accept unsized types like str or [T].

    Without ?Sized, passing a reference to an unsized type (like &str) will result in a compile error because the compiler tries to resolve T to the underlying unsized type (e.g., str) rather than the pointer itself.

  11. Implement token-based authentication using Rocket Request Guards

    master

    Implement authentication by creating a Token struct and implementing the rocket::request::FromRequest trait for it. This allows you to protect routes by simply adding _t: Token as a parameter to your handler functions. The implementation should:

    1. Check for the presence of the Authorization header.
    2. Verify the header uses the Bearer <token> format.
    3. Use the application's managed state (e.g., State<Db>) to validate the token against a database.
    4. Return Outcome::Success(token) if valid, or appropriate Outcome::Failure (e.g., Unauthorized or BadRequest) if invalid.
    use rocket::request::{FromRequest, Request, Outcome};
    use crate::models::Token;
    
    impl<'a, 'r> FromRequest<'a, 'r> for Token {
        type Error = &'static str;
        fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {
            // ... implementation logic to validate Bearer token ...
        }
    }
    
    // Usage in a route:
    #[rocket::get("/boards")]
    fn boards(db: State<Db>, _t: Token) -> Result<Json<Vec<Board>>, StdErr> {
        db.boards().map(Json)
    }