Instruction file imported from SaynaAI/sayna (
.cursor/rules/rust.mdc). Copyright stays with the author.
description: Comprehensive best practices for Rust development (Edition 2024) globs: *.rs alwaysApply: true
Rust Best Practices (2024-2025)
This document outlines a comprehensive set of best practices for Rust development, covering various aspects from code organization to security and tooling. Adhering to these guidelines will help you write idiomatic, efficient, secure, and maintainable Rust code.
Note: This project uses Rust Edition 2024 (released Feb 2025 with Rust 1.85).
1. Code Organization and Structure
1.1. Directory Structure
src/: Contains all the Rust source code.main.rs: The entry point for binary crates.lib.rs: The entry point for library crates.bin/: Contains source files for multiple binary executables within the same project. Each file inbin/will be compiled into a separate executable.modules/orcomponents/: (Optional) For larger projects, group related modules or components into subdirectories. Use descriptive names.tests/: Integration tests. (See Testing section below for more details.)examples/: Example code that demonstrates how to use the library.
benches/: Benchmark tests (usingcriterionor similar).Cargo.toml: Project manifest file.Cargo.lock: Records the exact versions of dependencies used. Do not manually edit..gitignore: Specifies intentionally untracked files that Git should ignore.README.md: Project documentation, including usage instructions, build instructions, and license information.
sayna/ ├── Cargo.toml ├── Cargo.lock ├── src/ │ ├── main.rs # Entry point for a binary crate │ ├── lib.rs # Entry point for a library crate │ ├── modules/ │ │ ├── module_a.rs # A module within the crate │ │ └── module_b.rs # Another module │ └── bin/ │ ├── cli_tool.rs # A separate binary executable │ └── worker.rs # Another binary executable ├── tests/ │ └── integration_test.rs # Integration tests ├── benches/ │ └── my_benchmark.rs # Benchmark tests using Criterion ├── examples/ │ └── example_usage.rs # Example code using the library ├── README.md
1.2. File Naming Conventions
- Rust source files use the
.rsextension. - Module files (e.g.,
module_a.rs) should be named after the module they define. - Use snake_case for file names (e.g.,
my_module.rs).
1.3. Module Organization
- Use modules to organize code into logical units.
- Declare modules in
lib.rsormain.rsusing themodkeyword. - Use
pub modto make modules public. - Create separate files for each module to improve readability and maintainability.
- Use
usestatements to bring items from other modules into scope.
// lib.rs
pub mod my_module;
mod internal_module; // Not public
rust
// my_module.rs
pub fn my_function() {
//...
}
1.4. Component Architecture
- For larger applications, consider using a component-based architecture.
- Each component should be responsible for a specific part of the application's functionality.
- Components should communicate with each other through well-defined interfaces (traits).
- Consider using dependency injection to decouple components and improve testability.
1.5. Code Splitting Strategies
- Split code into smaller, reusable modules.
- Use feature flags to conditionally compile code for different platforms or features.
- Consider using dynamic linking (if supported by your target platform) to reduce binary size.
2. Common Patterns and Anti-patterns
2.1. Design Patterns
- Builder Pattern: For constructing complex objects with many optional parameters.
- Factory Pattern: For creating objects without specifying their concrete types.
- Observer Pattern: For implementing event-driven systems.
- Strategy Pattern: For selecting algorithms at runtime.
- Visitor Pattern: For adding new operations to existing data structures without modifying them.
2.2. Recommended Approaches for Common Tasks
- Data Structures: Use
Vecfor dynamic arrays,HashMapfor key-value pairs,HashSetfor unique elements,BTreeMapandBTreeSetfor sorted collections. - Concurrency: Use
ArcandMutexfor shared mutable state, channels for message passing, and therayoncrate for data parallelism. - Asynchronous Programming: Use
asyncandawaitfor writing asynchronous code. - Error Handling: Use the
Resulttype for recoverable errors andpanic!for unrecoverable errors.
2.3. Anti-patterns and Code Smells
- Unnecessary Cloning: Avoid cloning data unless it is absolutely necessary. Use references instead.
- Excessive
unwrap()Calls: Handle errors properly instead of usingunwrap(), which can cause the program to panic. - Overuse of
unsafe: Minimize the use ofunsafecode and carefully review any unsafe code to ensure it is correct. - Ignoring Compiler Warnings: Treat compiler warnings as errors and fix them.
- Premature Optimization: Focus on writing clear, correct code first, and then optimize only if necessary.
2.4. State Management
- Immutability by Default: Prefer immutable data structures and functions that return new values instead of modifying existing ones.
- Ownership and Borrowing: Use Rust's ownership and borrowing system to manage memory and prevent data races.
- Interior Mutability: Use
Cell,RefCell,Mutex, andRwLockfor interior mutability when necessary, but be careful to avoid data races.
2.5. Error Handling
Result<T, E>: UseResultto represent fallible operations.Tis the success type, andEis the error type.Option<T>: UseOptionto represent the possibility of a missing value.Some(T)for a value,Nonefor no value.?Operator: Use the?operator to propagate errors up the call stack.- Custom Error Types: Define custom error types using enums or structs to provide more context about errors.
anyhowandthiserrorCrates: Consider using theanyhowcrate for simple error handling and thethiserrorcrate for defining custom error types.
3. Performance Considerations
3.1. Optimization Techniques
- Profiling: Use profiling tools to identify performance bottlenecks:
- Samply: Modern profiler with Firefox Profiler UI (recommended for 2024+)
- pprof-rs: CPU profiler with Criterion integration
- cargo-flamegraph: Classic flamegraph generation
- Bytehound: Best available memory profiling tool for Rust
- DHAT/dhat-rs: Memory profiling and allocation tracking
- Benchmarking: Use benchmarking tools to measure performance:
- Divan: Modern go-to benchmark framework (simpler API than Criterion)
- Criterion: Mature statistical benchmarking
- Iai-Callgrind: Instruction-count based (reliable in CI environments)
- Zero-Cost Abstractions: Leverage Rust's zero-cost abstractions, such as iterators, closures, and generics.
- Inlining: Use the
#[inline]attribute to encourage the compiler to inline functions. - LTO (Link-Time Optimization): Enable LTO to improve performance by optimizing across crate boundaries.
3.2. Memory Management
- Minimize Allocations: Reduce the number of allocations and deallocations by reusing memory and using stack allocation when possible.
- Avoid Copying Large Data Structures: Use references or smart pointers to avoid copying large data structures.
- Use Efficient Data Structures: Choose the right data structure for the job based on its performance characteristics.
- Consider
BoxandRc:Boxfor single ownership heap allocation,RcandArcfor shared ownership (latter thread-safe).
3.3. Rendering Optimization
- (Relevant if the Rust application involves rendering, e.g., a game or GUI)
- Batch draw calls: Combine multiple draw calls into a single draw call to reduce overhead.
- Use efficient data structures: Use data structures that are optimized for rendering, such as vertex buffers and index buffers.
- Profile rendering performance: Use profiling tools to identify rendering bottlenecks.
3.4. Bundle Size Optimization
- Strip Debug Symbols: Remove debug symbols from release builds to reduce binary size.
- Enable LTO: LTO can also reduce binary size by removing dead code.
- Use
minisizeProfile: Create aminisizeprofile inCargo.tomlfor optimizing for size. - Avoid Unnecessary Dependencies: Only include the dependencies that are absolutely necessary.
3.5. Lazy Loading
- Load Resources on Demand: Load resources (e.g., images, sounds, data files) only when they are needed.
- Use a Loading Screen: Display a loading screen while resources are being loaded.
- Consider Streaming: Stream large resources from disk or network instead of loading them all at once.
4. Security Best Practices
4.1. Security Auditing Tools
- cargo-audit: Audit dependencies for known vulnerabilities (uses RustSec Advisory Database)
cargo install cargo-audit cargo audit - cargo-deny: Check dependencies for licenses, bans, and advisories
- Run security audits in CI pipelines and pre-commit hooks
4.2. Common Vulnerabilities
- Buffer Overflows: Prevent buffer overflows by using safe indexing methods (e.g.,
get(),get_mut()) and validating input sizes. - SQL Injection: Prevent SQL injection by using parameterized queries and escaping user input.
- Cross-Site Scripting (XSS): Prevent XSS by escaping user input when rendering HTML.
- Command Injection: Prevent command injection by avoiding the use of
std::process::Commandwith user-supplied arguments. - Denial of Service (DoS): Protect against DoS attacks by limiting resource usage (e.g., memory, CPU, network connections).
- Integer Overflows: Use
checked_add,checked_sub,checked_mul, etc. Enableoverflow-checks = truein Cargo.toml for release builds. - Use-After-Free: Rust's ownership system largely prevents this, but be cautious when using
unsafecode or dealing with raw pointers. - Data Races: Avoid data races by using appropriate synchronization primitives (
Mutex,RwLock, channels). - Uninitialized Memory: Rust generally initializes memory, but
unsafecode can bypass this. Be careful when working with uninitialized memory.
4.3. Input Validation
- Validate All Input: Validate all input from external sources, including user input, network data, and file contents.
- Use a Whitelist Approach: Define a set of allowed values and reject any input that does not match.
- Sanitize Input: Remove or escape any potentially dangerous characters from input.
- Limit Input Length: Limit the length of input strings to prevent buffer overflows.
- Check Data Types: Ensure that input data is of the expected type.
5. Testing Approaches
5.1. Unit Testing
- Test Individual Units of Code: Write unit tests to verify the correctness of individual functions, modules, and components.
- Use the
#[test]Attribute: Use the#[test]attribute to mark functions as unit tests. - Use
assert!andassert_eq!: Useassert!andassert_eq!macros to check that the code behaves as expected. - Test Driven Development (TDD): Consider writing tests before writing code.
- Table-Driven Tests: Use parameterized tests or table-driven tests for testing multiple scenarios with different inputs.
5.1.1. Modern Testing Tools
- cargo-nextest: Next-generation test runner with better performance and output
- rstest: Fixture-based testing (like pytest fixtures) with
#[rstest]macro - proptest: Property-based testing with automatic shrinking
- QuickCheck: Alternative property-based testing framework
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
}
5.2. Integration Testing
- Test Interactions Between Components: Write integration tests to verify that different components of the application work together correctly.
- Create a
tests/Directory: Place integration tests in atests/directory at the root of the project. - Use Separate Test Files: Create separate test files for each integration test.
5.3. End-to-End Testing
- Test the Entire Application: Write end-to-end tests to verify that the entire application works as expected.
- Use a Testing Framework: Use a testing framework (e.g.,
cucumber,selenium) to automate end-to-end tests. - Test User Flows: Test common user flows to ensure that the application is usable.
5.4. Test Organization
- Group Tests by Functionality: Organize tests into modules and submodules based on the functionality they test.
- Use Descriptive Test Names: Use descriptive test names that clearly indicate what the test is verifying.
- Keep Tests Separate from Production Code: Keep tests in separate files and directories to avoid cluttering the production code.
- Run tests frequently: Integrate tests into your development workflow and run them frequently to catch errors early.
5.5. Mocking and Stubbing
- Use Mocking Libraries: Use mocking libraries (e.g.,
mockall,mockito) to create mock objects for testing. - Use Traits for Interfaces: Define traits for interfaces to enable mocking and stubbing.
- Avoid Global State: Avoid global state to make it easier to mock and stub dependencies.
6. Common Pitfalls and Gotchas
6.1. Frequent Mistakes
- Borrowing Rules: Misunderstanding Rust's borrowing rules can lead to compile-time errors. Ensure you understand ownership, borrowing, and lifetimes.
- Move Semantics: Be aware of move semantics and how they affect ownership. Data is moved by default, not copied.
- Lifetime Annotations: Forgetting lifetime annotations can lead to compile-time errors. Annotate lifetimes when necessary.
- Error Handling: Not handling errors properly can lead to unexpected panics. Use
Resultand the?operator to handle errors gracefully. - Unsafe Code: Overusing or misusing
unsafecode can lead to undefined behavior and security vulnerabilities.
6.2. Edge Cases
- Integer Overflow: Be aware of integer overflow and use checked arithmetic methods to prevent it.
- Unicode: Handle Unicode characters correctly to avoid unexpected behavior.
- File Paths: Handle file paths correctly, especially when dealing with different operating systems.
- Concurrency: Be careful when writing concurrent code to avoid data races and deadlocks.
6.3. Version-Specific Issues
- Check Release Notes: Review the release notes for new versions of Rust to identify any breaking changes or new features that may affect your code.
- Use
rustup: Userustupto manage multiple versions of Rust. - Update Dependencies: Keep your dependencies up to date to take advantage of bug fixes and new features.
6.4. Compatibility Concerns
- C Interoperability: Be careful when interacting with C code to avoid undefined behavior.
- Platform-Specific Code: Use conditional compilation to handle platform-specific code.
- WebAssembly: Be aware of the limitations of WebAssembly when targeting the web.
6.5. Debugging Strategies
- Use
println!: Useprintln!statements for quick debugging (remove before committing). - Use a Debugger: Use a debugger (e.g.,
gdb,lldb) to step through the code and inspect variables. - Use
assert!: Useassert!to check that the code behaves as expected. - Use Structured Logging: Prefer
tracingoverlogfor structured, async-aware logging:- Use
#[instrument]macro to automatically capture function arguments - Use
tracing-subscriberfor log formatting and filtering - Configure via
RUST_LOGenvironment variable - Integrate with OpenTelemetry via
tracing-opentelemetryfor production observability
- Use
- Clippy: Use Clippy to catch common mistakes and improve code quality.
- Profiling: Use Samply or cargo-flamegraph to profile and visualize the execution of your code.
7. Tooling and Environment
7.1. Recommended Development Tools
- Rustup: For managing Rust toolchains and versions.
- Cargo: The Rust package manager and build tool.
- IDE/Editor: VS Code with the rust-analyzer extension, IntelliJ Rust, or other editors with Rust support.
- Clippy: A linter for Rust code.
- Rustfmt: A code formatter for Rust code.
- Cargo-edit: A utility for easily modifying
Cargo.tomldependencies. - Cargo-watch: Automatically runs tests on file changes.
- lldb or GDB: Debuggers for Rust applications.
7.2. Build Configuration
- Use
Cargo.toml: Configure build settings, dependencies, and metadata in theCargo.tomlfile. - Use Profiles: Define different build profiles for development, release, and testing.
- Feature Flags: Use feature flags to conditionally compile code for different platforms or features.
toml [package] name = "sayna" version = "0.1.0" edition = "2024"
[dependencies] serde = { version = "1.0", features = ["derive"] }
[dev-dependencies] rand = "0.8"
[features] default = ["serde"] # 'default' feature enables 'serde' expensive_feature = []
[profile.release] opt-level = 3 debug = false lto = true
7.3. Linting and Formatting
- Use Clippy: Use Clippy to catch common mistakes and enforce coding standards.
- Use Rustfmt: Use Rustfmt to automatically format code according to the Rust style guide.
- Configure Editor: Configure your editor to automatically run Clippy and Rustfmt on save.
- Pre-commit Hooks: Set up pre-commit hooks to run Clippy and Rustfmt before committing code.
Run Clippy
cargo clippy
Run Rustfmt
cargo fmt
By following these best practices, you can write high-quality Rust code that is efficient, secure, and maintainable. Remember to stay up-to-date with the latest Rust features and best practices to continuously improve your skills and knowledge.