Rust Rubric

This document lists conventions to follow when writing Rust in the Fuchsia Source Tree. These conventions are a combination of best practices, project preferences, and some choices made for the sake of consistency.

Guidelines

Naming

Casing conforms to Rust idioms

See C-CASE.

Ad-hoc conversions follow as_, to_, into_ conventions

See C-CONV.

Getter names follow Rust convention

With a few exceptions, the get_ prefix is not used for getters in Rust code.

See C-GETTER.

Methods on collections that produce iterators follow iter, iter_mut, into_iter

See C-ITER.

Iterator type names match the methods that produce them

See C-ITER-TY.

Names use a consistent word order

See C-WORD-ORDER.

Interoperability

Types eagerly implement common traits

Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Display, Default should all be implemented when appropriate.

See C-COMMON-TRAITS.

Conversions use the standard traits From, AsRef, AsMut

See C-CONV-TRAITS.

Collections implement FromIterator and Extend

See C-COLLECT.

Data structures implement Serde's Serialize, Deserialize

See C-SERDE.

Types are Send and Sync where possible

See C-SEND-SYNC.

Error types are meaningful and well-behaved

See C-GOOD-ERR.

Binary number types provide Hex, Octal, Binary formatting

See C-NUM-FMT.

Generic reader/writer functions take R: Read and W: Write by value

See C-RW-VALUE.

Macros

Input syntax is evocative of the output

See C-EVOCATIVE.

Macros compose well with attributes

See C-MACRO-ATTR.

Item macros work anywhere that items are allowed

See C-ANYWHERE.

Item macros support visibility specifiers

See C-MACRO-VIS.

Type fragments are flexible

See C-MACRO-TY.

Documentation

Crate level docs are thorough and include examples

See C-CRATE-DOC.

All items have a rustdoc example

See C-EXAMPLE.

Examples use ?, not try!, not unwrap

See C-QUESTION-MARK.

Function docs include error, panic, and safety considerations

See C-FAILURE.

See C-LINK.

Rustdoc does not show unhelpful implementation details

See C-HIDDEN.

Every unsafe block has an accompanying justification

Safety justifications should begin with // SAFETY: and explain why the unsafe block is sound.

Do

// SAFETY: <why this unsafe operation's safety requirements are met>

Don't

// Safety: <...>
// [SAFETY] <...>
// <...>
// SAFETY: Trust me.

Unsafe code should explain why the unsafe block is necessary and why the code contained within the block is sound. If there are safe alternatives which may appear suitable but cannot be used, the reason why they cannot be used should be documented as well.

Do

// SAFETY: The `bytes` returned from our string builder are guaranteed to be
// valid UTF-8. We used to call `from_utf8`, but this caused performance issues
// with large inputs.
let s = unsafe { String::from_utf8_unchecked(bytes) };

Don't

// SAFETY: We shouldn't have to validate `bytes`, and the safe version is slow.
let s = unsafe { String::from_utf8_unchecked(bytes) };

Justifications should directly address the requirements for the operation. It's okay to summarize as long as all of the requirements are addressed.

Do

// SAFETY: The caller has guaranteed that `ptr` is valid for reads, properly
// aligned, and points to a properly-initialized `T`.
unsafe {
    let x = ptr.read();
}

Do

// SAFETY: The caller has guaranteed that `ptr` points to a valid `T`.
unsafe {
    let x = ptr.read();
}

Don't

// SAFETY: `ptr` is safe to read.
unsafe {
    let x = ptr.read();
}

Safety justifications should address why an operation is justified, not just that an operation is justified.

Do

const BUFFER_LEN: usize = 1024;
fn partially_init(n: usize) -> MaybeUninit<[i32; BUFFER_LEN]> {
    // These asserts ensure our safety conditions are met later on
    const _: () = assert!(BUFFER_LEN <= 1024);
    assert!(n < BUFFER_LEN);

    let mut buffer = MaybeUninit::<[i32; BUFFER_LEN]>::uninit();
    let ptr = buffer.as_mut_ptr().cast::<i32>();
    for i in 0..n {
        // SAFETY:
        // - `ptr` points to the first `i32` of `buffer`.
        // - `buffer` has space for BUFFER_LEN elements and we asserted that
        //   `n < BUFFER_LEN`.
        // - We asserted that `BUFFER_LEN <= 1024`, so `size_of::<i32>() * i`
        //   is at most 4096 which is less than `isize::MAX` and `usize::MAX`.
        let element = unsafe { &mut *ptr.add(i) };
        *element = i as i32;
    }

    buffer
}

Don't

const BUFFER_LEN: usize = 1024;
fn partially_init(n: usize) -> MaybeUninit<[i32; BUFFER_LEN]> {
    // Why are these asserts here?
    const _: () = assert!(BUFFER_LEN <= 1024);
    assert!(n < BUFFER_LEN);

    let mut buffer = MaybeUninit::<[i32; BUFFER_LEN]>::uninit();
    let ptr = buffer.as_mut_ptr().cast::<i32>();
    for i in 0..n {
        // SAFETY:
        // - `ptr` is in bounds or one byte past the end of an allocated object.
        // - `ptr + i` is also in bounds.
        // - The computed offset, in bytes, doesn't overflow an `isize`.
        // - The offset being in bounds does not rely on "wrapping around" the
        //   address space.
        let element = unsafe { &mut*ptr.add(i) };
        *element = i as i32;
    }

    buffer
}

Unsafe traits are documented, unsafe trait impls are justified

Unsafe traits should be documented according to the same guidelines as unsafe functions.

Unsafe trait definitions should document safety considerations (See C-FAILURE), and unsafe trait implementations should be justified (See "Every unsafe block has an accompanying justification"))

Do

/// A labeler that always returns unique labels.
///
/// # Safety
///
/// Every time `create_unique_label()` is called on the same labeler, it must
/// return a distinct `u32`.
unsafe trait UniqueLabeler {
    /// Returns a new unique label.
    fn create_unique_label(&mut self) -> u32;
}

struct SequentialLabeler {
    next: Option<u32>,
}

// SAFETY: `create_unique_label()` will always return the next sequential label
// or panic if all available labels are exhausted.
unsafe impl UniqueLabeler for SequentialLabeler {
    fn create_unique_label(&mut self) -> u32 {
        if let Some(next) = self.next {
            self.next = (next < u32::MAX).then(|| next + 1);
            next
        } else {
            panic!("sequential unique labels exhausted");
        }
    }
}

Don't

/// A labeler that always returns unique labels.
unsafe trait UniqueLabeler {
    /// Returns a new unique label.
    fn create_unique_label(&mut self) -> u32;
}

struct SequentialLabeler {
    next: u32,
}

unsafe impl UniqueLabeler for SequentialLabeler {
    fn create_unique_label(&mut self) -> u32 {
        // This will have correct panicking behavior in debug builds because
        // integer overflow is trapped. In a release build, this will still
        // overflow but our labeler will not panic!
        let result = self.next;
        next += 1;
        result
    }
}

Unsafe operations are always in an unsafe block

unsafe functions are not considered unsafe contexts in Fuchsia. Unsafe operations must always be located inside an unsafe block, even if they are in an unsafe function body.

Do

unsafe fn clear_slice(ptr: *mut i32, len: usize) {
    assert!(len.checked_mul(mem::size_of::<i32>()).unwrap() < isize::MAX);

    // SAFETY:
    // - The caller has guaranteed that `ptr` points to `len` consecutive, valid
    //   i32s and that the data at behind `ptr` is not simultaneously accessed
    //   through any other pointer.
    // - We asserted that the total size of the slice is less than isize::MAX.
    let slice = unsafe { slice::from_raw_parts_mut(ptr, len) };
    for x in slice.iter_mut() {
        *x = 0;
    }
}

Don't

unsafe fn clear_slice(ptr: *mut i32, len: usize) {
    // We forgot to assert that the length of the slice is less than isize::MAX!
    // If we justified our call to from_raw_parts_mut, we would have been much
    // more likely to remember.

    let slice = slice::from_raw_parts_mut(ptr, len);
    for x in slice.iter_mut() {
        *x = 0;
    }
}

Predictability

Smart pointers do not add inherent methods

See C-SMART-PTR.

Conversions live on the most specific type involved

See C-CONV-SPECIFIC

Functions with a clear receiver are methods

See C-METHOD.

Functions do not take out-parameters

See C-NO-OUT.

Operator overloads are unsurprising

See C-OVERLOAD.

Only smart pointers implement Deref and DerefMut

See C-DEREF.

Constructors are static, inherent methods

See C-CTOR.

Flexibility

Functions expose intermediate results to avoid duplicate work

See C-INTERMEDIATE.

Caller decides where to copy and place data

See C-CALLER-CONTROL.

Functions minimize assumptions about parameters by using generics

See C-GENERIC.

Traits are object-safe if they may be useful as a trait object

See C-OBJECT.

Type safety

Newtypes provide static distinctions

See C-NEWTYPE.

Arguments convey meaning through types, not bool or Option

See C-CUSTOM-TYPE.

Types for a set of flags are bitflags, not enums

See C-BITFLAG.

Builders enable construction of complex values

See C-BUILDER.

Dependability

Functions validate their arguments

See C-VALIDATE.

Destructors never fail

See C-DTOR-FAIL.

Destructors that may block have alternatives

See C-DTOR-BLOCK.

Debuggability

All public types implement Debug

See C-DEBUG.

Debug representation is never empty

See C-DEBUG-NONEMPTY.

Future Proofing

Sealed traits protect against downstream implementations

See C-SEALED.

Structs have private fields

See C-STRUCT-PRIVATE.

Newtypes encapsulate implementation details

See C-NEWTYPE-HIDE.

Data structures do not duplicate derived trait bounds

See C-STRUCT-BOUNDS.

Updating the guidelines

To propose additions or modifications, open a CL and cc fuchsia-rust-api-rubric@google.com to ensure it is reviewed. Use any feedback to iterate on the proposal.

Once feedback has been addressed, any one of the OWNERS of this file who is not the proposal author may act as facilitator and move the proposal to last call. The facilitator will send an email to fuchsia-rust-api-rubric@google.com announcing last call on the proposal. The proposal will be open for feedback for 7 calendar days.

At the end of the last call period and once relevant concerns have been discussed or addressed, a facilitator will comment on the CL with a final decision based on the review feedback and discussion. Any controversial decisions should be made with adequate public discussion of the relevant issues, and should include the rationale in the CL comment. The decision outcome should also be sent to the email thread.

If a proposal is accepted, the facilitator will leave a +2 and the author can then submit it.

Pending Topics

Pending topics are tracked in the Rust Issue Tracker component.

Relationship with upstream Rust API guidelines

This rubric contains most of the Rust API Guidelines, however the following official guidelines are omitted:

  • C-FEATURE as Fuchsia does not currently support features for crates.
  • C-METADATA as Fuchsia does not maintain internal Cargo.toml files.
  • C-HTML-ROOT as Fuchsia does not currently publish most Rust code to crates.io.
  • C-RELNOTES as most Rust code in Fuchsia "lives at HEAD".
  • C-STABLE as Fuchsia does not currently publish most Rust code to crates.io.
  • C-PERMISSIVE as all of Fuchsia's Rust code is under the Fuchsia license.

The following Fuchsia-specific guidelines are included: