Rust for PHP Developers (Final Part)

This will be final part of the series “Rust for PHP Developers”. In this part we will cover the following topics:

  1. Crates and Modules
  2. Testing
  3. Unsafe Rust
  4. Cargo and Some popular Crates
  5. Blanket trait implementations
  6. Async Rust

Crates and Modules

In Rust, a crate is a package of Rust code. It can be a binary or a library. Anything that you writing is usually inside a crate. So every project you create with Cargo is a crate.

KeywordRefers to
crate::The root of your current crate
super::The parent module of the current module
self::The current module (explicitly)

Let’s see some examples of a crate structure:

my_crate/
├── src/
│   ├── lib.rs
│   ├── foo/
│   │   ├── mod.rs
│   │   └── bar.rs

// src/lib.rs
pub mod foo; // This declares a module named `foo`

// src/foo/mod.rs
pub mod bar; // This declares a submodule named `bar`

pub fn hello() {
    println!("Hello from foo!");
}

// src/foo/bar.rs
// use a function from the parent module
use super::hello;
pub fn greet() {
    hello(); // Calls the function from the parent module
    println!("Hello from bar!");
}

// use a function from the root 
use crate::foo::hello as crate_hello;
pub fn greet_from_root() {
    crate_hello(); // Calls the function from the root module
}

A module is a way to organize code within a crate. Modules can be nested, and they help in encapsulating functionality. Consider it similar to namespaces in PHP.

// src/lib.rs
mod my_module {
    pub fn my_function() { // This function is public and can be accessed outside this module
        println!("Hello from my_module!");
    }
    fn my_private_function() { // This function is private and cannot be accessed outside this module
        println!("This is a private function.");
    }
}
// src/main.rs
mod my_module;
fn main() {
    my_module::my_function();
}

Why use modules?

  • Encapsulation: Modules allow you to encapsulate functionality, making sure that only the necessary parts are exposed. Everything is private by default. Note about pub keyword: For pub Traits the methods are public for implementations, for pub Enums the variants are public.
  • Organization: They help in organizing code logically, making it easier to navigate and maintain.
  • Reusability: You can create reusable components that can be shared across different parts

Testing

Rust has a built-in test framework that allows you to write unit tests, integration tests, and documentation tests. Tests are typically placed in the same file as the code they test, or in a separate tests directory.

Unit Tests

If you want to write unit tests, you can use the #[cfg(test)] attribute to conditionally compile test code. Unit tests are usually placed in the same file as the code they test.

// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
    a + b
} 
#[cfg(test)]  // cfg(test) attribute ensures that this module is only compiled when running tests
mod tests {
    use super::*; // Import everything from the parent module

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }
} 

Integration Tests

Integration tests are placed in the tests directory and are compiled as separate crates. They can access the public API of your crate.

my_crate/
├── src/
│   ├── lib.rs
├── tests/
│   ├── integration_test.rs
// tests/integration_test.rs
use my_crate::add; // Import the function from the library
#[test]
fn test_add() {
    assert_eq!(add(2, 3), 5);
}

Running Tests

You can run tests using the following command:

cargo test

This will compile your code and run all tests, including unit tests and integration tests.

Unsafe Rust

Unsafe Rust allows you to perform operations that the Rust compiler cannot guarantee to be safe. This means you need to take some more responsibility for ensuring memory safety. Unsafe Rust is used for low-level programming, such as interfacing with C code or performing manual memory management.
Unsafe Rust is marked with the unsafe keyword. Here are some common use cases:

  • Dereferencing raw pointers
  • Calling unsafe functions or methods
  • Accessing or modifying mutable static variables
  • Implementing unsafe traits
// Unsafe Rust example
static mut GLOBAL: i32 = 0; // A mutable static variable

unsafe fn access_global() {
    println!("Global value: {}", GLOBAL);
}

fn main() {
    let x: i32 = 42;
    let r: *const i32 = &x; // Create a raw pointer
    unsafe {
        println!("Value: {}", *r); // Dereference the raw pointer
    }

    unsafe {
        // Accessing a mutable static variable
        GLOBAL = 10; 
        println!("Global value: {}", GLOBAL);
    }

    unsafe {
        access_global(); // Call an unsafe function
    }
}

Unsafe Rust is powerful but should be used sparingly. Always prefer safe Rust unless you have a compelling reason to use unsafe code.

Cargo and Some Popular Crates

Cargo is the Rust package manager and build system. It helps you manage dependencies, build your project, and run tests. When you create a new Rust project using Cargo, it generates a Cargo.toml file, which is used to manage dependencies and project metadata.

Cargo Commands

  • cargo new my_project: Create a new Rust project.
  • cargo build: Build the project. By default it builds in debug mode.
  • cargo build --release: Build the project in release mode (optimized). Rust compiler can do great optimizations in release mode, one such example is inlining functions or pre-computing some values.
  • cargo run: Build and run the project.
  • cargo test: Run tests.
  • cargo check: Check the code for errors without building.

Popular Crates

  • serde: A framework for serializing and deserializing Rust data structures.
  • tokio: An asynchronous runtime for Rust, used for building concurrent applications.
  • reqwest: An HTTP client for making requests.
  • clap: A command-line argument parser for Rust applications.
  • rayon: A data parallelism library that makes it easy to write parallel code.
  • chrono: A date and time library for Rust.

Adding Dependencies

To add a dependency to your project, you can modify the Cargo.toml file.

[dependencies]
serde = "1.0"
tokio = { version = "1.0", features = ["full"] }

You can specify the version of the crate you want to use. Cargo will automatically download and compile the specified version of the crate when you build your project.

Feature Flags

Feature flags allow you to enable or disable optional functionality in your dependencies. You can specify features in your Cargo.toml file.

[dependencies]
serde = { version = "1.0", features = ["derive"] }

Feature flags can help reduce the size of your binary by only including the necessary functionality. The Feature flags can be looked up on the docs (top right) or by going to the source code and look for #[cfg(feature = "feature_name")]

Blanket Trait Implementations

In Rust, a blanket trait implementation is a way to implement a trait for all types that satisfy certain bounds. This is often used to provide default behavior for a wide range of types without having to implement the trait for each individual type.

Here’s an example of a blanket trait implementation:

trait SaysHello {
    fn say_hello(&self);
}

impl <T> SaysHello for T {
    fn say_hello(&self) {
        println!("Hello from a type that implements SaysHello!");
    }
}

In this example, the SaysHello trait is implemented for all types T. This means that any type can call the say_hello method, and it will print a default message.

Async Rust

Asynchronous programming in Rust is primarily handled through the async and await keywords, which allow you to write non-blocking code that can handle multiple tasks concurrently. This is very close to how asynchronous programming works in JavaScript or Python.
In Rust, an async fn is a function that returns a Future, which represents a value that may not be available yet. You can use the await keyword to wait for the future to resolve.
Here’s a simple example of an asynchronous function:

use std::time::Duration;
use tokio::time::sleep;   

async fn async_function() {
    println!("Starting async function...");
    sleep(Duration::from_secs(2)).await; // Simulate an asynchronous operation
    println!("Async function completed!");
}
#[tokio::main] // This macro sets up the Tokio runtime
async fn main() {
    async_function().await; // Call the asynchronous function
}

You must have noticed the #[tokio::main] attribute. This is a macro provided by the Tokio runtime that sets up the asynchronous runtime for your application. The Rust’s default runtime is synchronous.

Using Async Libraries

Many libraries in Rust are designed to work asynchronously. For example, the reqwest library provides an asynchronous HTTP client that allows you to make non-blocking HTTP requests.
Here’s an example of making an asynchronous HTTP GET request using reqwest:

use reqwest::Client;
#[tokio::main]
async fn main() {
    let client = Client::new();
    let response = client.get("https://api.example.com/data")
        .send()
        .await
        .expect("Failed to send request");
    let body = response.text().await.expect("Failed to read response body");
    println!("Response: {}", body);
}

That’s all for the final part of the series “Rust for PHP Developers”. We have covered a lot of ground, from basic syntax to advanced topics like unsafe Rust and asynchronous programming. Rust is a powerful language that can help you write safe and efficient code, and we hope this series has given you a good foundation to start exploring it further. Happy coding!

Leave a Reply

Your email address will not be published. Required fields are marked *