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:
- Crates and Modules
- Testing
- Unsafe Rust
- Cargo and Some popular Crates
- Blanket trait implementations
- 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.
Keyword | Refers 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, forpub
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!