Rust Development Guide
Coding Conventions
General Standards
- •Follow standard Rust formatting (run
cargo fmtbefore committing) - •Address all clippy warnings (
cargo clippy -- -D warnings) - •Write tests for new functionality
- •Use meaningful test names prefixed with
test_
Code Organization
- •Testbench tests are integration tests (in
testbench/tests/, not unit tests) - •Each test file in
testbench/tests/is compiled as a separate binary - •Use
HashMap<u32, u32>for instruction/data memory - •Prefer Safe Rust over
unsafe(only useunsafeat FFI boundaries)
Mandatory Code Quality Checks
BEFORE COMMITTING ANY RUST CODE:
1. Format Code
cargo fmt
For the rust-test-program (if modified):
cd rust-test-program cargo fmt cd ..
Check formatting without modifying:
cargo fmt -- --check
2. Run Clippy (Zero Tolerance for Warnings)
ALWAYS use auto-fix first to save time:
cargo clippy --fix --allow-dirty
Then rerun clippy to check for any remaining or newly introduced warnings:
cargo clippy -- -D warnings
All clippy warnings must be addressed. The --fix --allow-dirty flags automatically fix many common issues even with uncommitted changes, avoiding unnecessary manual work. Always rerun clippy after auto-fix to catch any new warnings introduced by the fixes.
For the rust-test-program (if modified):
cd rust-test-program cargo clippy --fix --allow-dirty cargo clippy -- -D warnings cd ..
3. Run Tests
cargo test --verbose
All tests must pass before committing.
Note: The rust-test-program/ directory is a separate Rust project that builds for the RISC-V target platform (riscv32imafc-unknown-none-elf). If you modify any files in this directory, you MUST run the formatting and clippy checks in that directory as well, as it is checked separately in CI.
Best Practices
Memory Management
DO:
- •✅ Use callbacks for event handling
- •✅ Use proper ownership patterns (move semantics, borrowing)
- •✅ Consider
Rc<T>orArc<T>for shared ownership - •✅ Use
RefCell<T>orMutex<T>for interior mutability when needed
DON'T:
- •❌ Never use
Box::leak()to circumvent lifetime issues- •This creates memory leaks
- •Bad practice that defeats Rust's safety guarantees
- •Restructure your code instead
Choosing the Right Approach
The best solution for lifetime/ownership issues depends on the situation:
- •Single-threaded shared ownership: Consider
Rc<RefCell<T>> - •Multi-threaded shared ownership: Consider
Arc<Mutex<T>> - •Callbacks/closures: Often the cleanest solution for event-driven code
- •Restructure ownership: Sometimes refactoring is the right answer
Key Principle: Prefer solutions that work with Rust's ownership system, not against it.
Error Handling
- •Never use
.unwrap()in production code - •Propagate errors with
Result<T, E> - •Define custom error types for clarity
- •Use
?operator for error propagation
Type Safety
- •Prefer explicit types (
u8,u32,u64) overusizewhen modeling hardware - •Use
#[repr(C)]structs for FFI compatibility - •Avoid
ascasting - usetry_into()or explicit masking
Documentation
- •Document public APIs with
///doc comments - •Explain non-obvious implementation details with
//comments - •Include examples in doc comments when helpful
Debugging Rust Code
Enable Verbose Output
cargo test -- --nocapture # See println! output from tests cargo test -- --show-output # Show output even for passing tests
Run Single Test
cargo test test_name --nocapture
Debug Build
cargo build # Debug build (default) cargo build --release # Release build (optimized)
Working with Hardware Simulations
Verilator Integration
When interfacing with Verilator-generated C++ code:
- •Use marlin crate abstractions - don't call Verilator directly
- •Clean build after RTL changes:
cargo cleanto clear Verilator cache - •Check target/verilator/ for compilation artifacts if issues arise
Concrete Data Debugging
CRITICAL: When debugging hardware simulations:
- •✅ Extract actual signal values from simulation
- •✅ Add
$display()to RTL to observe hardware state - •✅ Print actual register/signal values before forming hypotheses
- •✅ Base all reasoning on concrete evidence
- •❌ Don't predict what hardware "should" be doing without checking
Example: Debugging Hardware Behavior
Wrong approach:
// "The PC should be X because..." (assumption-based reasoning) assert_eq!(core.pc, expected_value); // May fail
Right approach:
// First, add debug output to observe actual behavior
println!("Actual PC: 0x{:08x}", core.pc);
// Then form hypothesis based on observed data
// Then add assertions based on understanding from data
Project-Specific Patterns
Test Structure
#[test]
fn test_descriptive_name() {
// Setup
let runtime = create_runtime();
let mut dut = TestModule::new(&runtime);
// Initialize
dut.reset.set(true);
dut.prop();
dut.reset.set(false);
// Test logic
// ...
// Assertions
assert_eq!(dut.output.get(), expected_value);
}
Helper Macros
// Clock cycle helper clock_cycle!(dut); // Runtime creation create_runtime()
Memory Access Patterns
For memory operations in testbenches:
// Read dmem_addr AFTER eval() for stores dut.eval(); let addr = dut.dmem_addr.get(); // Set dmem_rdata BEFORE eval() for loads dut.dmem_rdata.set(value); dut.eval();
Common Anti-Patterns to Avoid
- •Global mutable state: Avoid
static mut - •Unwrapped results: Never use
.unwrap()in long-running code - •Skipping code quality checks: Always run
cargo fmtandcargo clippy --fix --allow-dirtythencargo clippy -- -D warnings - •Manual clippy fixes: Don't manually fix warnings that
cargo clippy --fix --allow-dirtycan handle automatically - •Implicit casting: Use
try_into()instead ofas - •Memory leaks: Never use
Box::leak()to solve lifetime issues
Resources
- •Rust Book: https://doc.rust-lang.org/book/
- •Rust by Example: https://doc.rust-lang.org/rust-by-example/
- •Clippy Lints: https://rust-lang.github.io/rust-clippy/
- •Marlin Documentation: https://docs.rs/marlin/