Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Loops and iterators

A C loop often does several jobs at once: it traverses memory, controls when iteration stops, and computes a result. A literal Rust port can preserve all of that machinery even after the data has become a safe Rust collection.

Consider this loop from bm:

for (size_t i = 0; i < b->n_tags; i++) {
    if (strstr(b->tags[i], query))
        return 1;
}
return 0;

Initial Rust port

A direct Rust translation looks like this:

struct Bookmark { tags: Vec<String> }
fn matches(bookmark: &Bookmark, query: &str) -> bool {
for index in 0..bookmark.tags.len() {
    if bookmark.tags[index].contains(query) {
        return true;
    }
}
false
}

The calculation never uses the numeric value of index; it only needs the tag stored there. Rust can iterate over those values directly:

struct Bookmark { tags: Vec<String> }
fn matches(bookmark: &Bookmark, query: &str) -> bool {
for tag in &bookmark.tags {
    if tag.contains(query) {
        return true;
    }
}
false
}

The index existed only because C needs one to reach each element. A for loop over the values is a good final form, especially when its body does several things.

Idiomatic Rust port

The tag loop computes whether any tag matches. The any operation states that directly:

struct Bookmark { tags: Vec<String> }
fn matches(bookmark: &Bookmark, query: &str) -> bool {
bookmark.tags.iter().any(|tag| tag.contains(query))
}

any() stops after the first match, so the control flow is the same as the early return in the C loop. Keep that kind of behavior in mind when you port a loop: short-circuiting, ordering, and whether values are borrowed or moved should all stay the same.

Pointer-bumping loops

The other common C shape walks a string by moving a pointer forward until it reaches the NUL terminator:

size_t n = 0;
for (const char *p = s; *p; p++) {
    if (*p == ',')
        n++;
}

A literal port keeps the raw pointer, which means every read and every step is unsafe, and the loop is only correct as long as the string really ends in a NUL:

use std::ffi::c_char;
let s = c"rust,ffi,c".as_ptr();
let mut n = 0;
let mut p: *const c_char = s;
// SAFETY: `s` points to a live, NUL-terminated string, and the loop stops at
// the terminator.
while unsafe { *p } != 0 {
    if unsafe { *p } == b',' as c_char {
        n += 1;
    }
    p = unsafe { p.add(1) };
}
assert_eq!(n, 2);

Borrow the C string as a &CStr instead. to_bytes() gives you the bytes before the terminator as a slice, so there is no terminator to look for and no pointer to move. The loop becomes an iterator over those bytes:

let c_str = c"rust,ffi,c";
let n = c_str.to_bytes().iter().filter(|&&byte| byte == b',').count();
assert_eq!(n, 2);

Keep raw pointers at the boundary

This refactoring begins after the FFI layer has validated raw pointers and converted them into safe Rust slices, strings, or collections. Pointer validation and slice construction stay in the boundary adapter; the iterator chain operates only on safe Rust values.

Head to the exercise

The starter code in exercises/03_intermediate/09_loops_and_iterators is behaviorally correct, so its tests already pass. wr fails because Clippy finds index-based loops whose indexes aren’t part of the calculation.

Refactor the four functions without changing their results, ordering, short-circuiting, or borrowing behavior. They correspond to any, position, filter().count(), and a filter/map/collect pipeline with a nested any.

Exercise

The exercise for this section is located in 03_intermediate/09_loops_and_iterators