Ockam logo
Product
OverviewCloud SDKEdge SDKEmbedded SDKRegistryRouter
Contact us

Rust ​Library Coding Standard

This guide will help us keep the style in our Rust Library consistent.

This guide builds on the normal rust style guide here

Names

Make Names Fit

Names are the heart of programming. In the past people believed knowing someone's true name gave them magical power over that person. If you can think up the true name for something, you give yourself and the people coming after power over the code. Don't laugh! A name is the result of a long deep thought process about the ecology it lives in. Only a programmer who understands the system as a whole can create a name that "fits" with the system. If the name is appropriate everything fits together naturally, relationships are clear, meaning is derivable, and reasoning from common human expectations works as expected.

If you find all your names could be Thing and DoIt then you should probably revisit your design.

Types

  • Use platform agnostic types like usize and isize vs u32 or i32.
  • Use BTreeMap over HashMap because
    • Predictable iterating
    • Avoids Hash flooding DoS attacks

Function Names

Rust uses snake casing for function names.

  • Usually every function performs an action, so the name should make clear what it does: check_for_errors() instead of error_check(), dump_data_to_file() instead of data_file(). This will also make functions and data objects more distinguishable. Structs are often nouns. By making function names verbs and following other naming conventions programs can be read more naturally.
  • Suffixes are sometimes useful:
    • max ​ - to mean the maximum value something can have.
    • count ​ - the current count of a running count variable.
    • key ​ - key value.
  • For example: retry_max to mean the maximum number of retries, retry_cnt to mean the current retry count.
  • Prefixes are sometimes useful:
    • is ​ - to ask a question about something. Whenever someone sees ​ Is ​ they will know it's a question.
    • get ​ - get a value.
    • set ​ - set a value.
    • to ​ - convert existing value into something else.
    • from ​ - create a new value from an existing one.
  • For example: is_hit_retry_limit.

When in doubt, use longer names for clarity versus shorter names that can potentially be confusing.

Variable Naming & Declarations

  • Variable declarations should be limited to one per line.
  • Variables should be descriptively named, using abbreviations sparingly if at all.
  • Words should be separated by underscores.
  • Variables should be all lower case
  • Global variables are discouraged in Rust, except in rare cases such as when implementing FFI.
  • Global variables and constants are all upper case.
  • Types are usually inferred by the compiler, only include the type if the compiler can't do the inference.
  • Only use mut if needed. Prefer to use immutable variables as much as possible.
1const MAX_BUFFER_SIZE = 65535;
2let remote_host_dns_name = “www.ockam.io”;
3let mut bytes_received = 0;

Include Units in Names

If a variable represents time, weight, or some other unit then include the unit in the name so developers can more easily spot problems.

1let timeout_msecs = 0;
2let my_weight_lbs = 0;

Structure and Enumeration Names

  • Use Pascal Case.
  • Field names are all lower case with words separated by underscores ('_').
  • Field names are sorted alphabetically.
  • Rust supports tupling. However, Tuples with more than 2 elements are confusing. Use a struct with named fields instead.
  • If only one or two fields are in the struct, the tuple version may be allowed as long as the intented use is clear.
1struct Foo {
2 foo Next, /* List of active foo */
3 bar usize,
4 baz: usize, /* Bitfield */
5};

Global Constants

Global constants should be all caps with '_' separators.

1const A_GLOBAL_CONSTANT: usize = 5;

Macro Names

Macros are all lower case with '_' word separators

Use macros to keep code DRY (don't repeat yourself).

Error Handling

Use one enum that represents all the possible errors that can occur that inherits the Fail trait from the failure crate. Use a struct that wraps the enum to provide stacktraces and contexts. All these should be in one file named errors.rs.

When other packages and crates throw errors, implement the From trait from that error to the enum and error struct in errors.rs. This avoids the need to convert between error types throughout the rest of the code.

1use failure::{Backtrace, Context, Fail};
2
3#[derive(Clone, Copy, Fail, Debug)]
4pub enum ErrorKind {
5 #[display = "Failed to initialize"]
6 Init,
7 #[display = "Failed to read or write to network"]
8 NetworkError
9}
10
11#[derive(Debug)]
12pub struct Error {
13 inner: Context<ErrorKind>,
14}
15

Packaging

Cargo.toml

Sort fields in each section alphabetically. Use the latest "edition" in the [package] section. (Right now its 2018).

Crate declaration

Only use extern crate <crate-name> when importing macros.

Formatting

Braces

Placement

Of the three major brace placement strategies one is recommended:

1if condition {
2 while condition {
3 ...
4 }
5 ...
6}

When Braces are Needed

All if, while and do statements must either have braces or be on a single line.

Always Uses Braces Form

All if, while and do statements require braces even if there is only a single statement within the braces.

1if 1 == somevalue {
2 somevalue = 2;
3}

Justification

It ensures that when someone adds a line of code later there are already braces and they don't forget. It provides a more consistent look. This doesn't affect execution speed. It's easy to do.

One Line Form

1if 1 == somevalue { somevalue = 2; }

Justification

It provides safety when adding new lines while maintaining a compact readable form.

Add Comments to Closing Braces

Adding a comment to closing braces can help when you are reading code because you don't have to find the begin brace to know what is going on.

1loop {
2 if (valid) {
3 } /* if valid */
4 else {
5 } /* not valid */
6} /* end forever */

Consider Screen Size Limits

Some people like blocks to fit within a common screen size so scrolling is not necessary when reading code.

Parens () with Key Words and Functions Policy

  • Do not put parens next to keywords. Put a space between.
  • Do put parens next to function names.
  • Do not use parens in return statements when it's not necessary.

Justification

Keywords are not functions. By putting parens next to keywords keywords and function names are made to look alike.

1if condition {
2}
3while condition {
4}
5
6strcpy(s, s1);
7return 1;

A Line Should Not Exceed 120 Characters

Lines should not exceed 120 characters.

If Then Else Formatting

Layout

It's up to the programmer. Different bracing styles will yield slightly different looks. One common approach is:

1if condition {
2} else if condition {
3} else {
4}

If you have ​ else if ​ statements then it is usually a good idea to always have an else block for finding unhandled cases. Maybe put a log message in the else even if there is no corrective action taken.

Condition Format

Always put the constant on the left hand side of an equality/inequality comparison. For example:

1if 6 == errorNum ...

One reason is that if you leave out one of the = signs, the compiler will find the error for you. A second reason is that it puts the value you are looking for right up front where you can find it instead of buried at the end of your expression. It takes a little time to get used to this format, but then it really gets useful.

match Formatting

  • The ​ default ​ case should always be present and trigger an error if it should not be reached, yet is reached.
  • If you need to create variables put all the code in a block.
  • Prefer putting all code in blocks unless its a single expression
1match ... {
2 1 => {
3 ...
4 /* comments */
5 },
6 2 => {
7 let v: usize;
8 ...
9 },
10 3 => {
11 ...
12 }
13 _ => {}
14}

Use of goto, continue, break and ?:

Goto

Goto statements should be used in only one situation, and that is to use the label to break to an outer loop.

1'nextbasis: for basis in bases {
2 ...
3 for _ in 1..trials {
4 ...
5 if condition {
6 break 'nextbasis;
7 }
8 }
9}

Continue and Break

Continue and break are really disguised gotos so they are covered here. Continue and break like goto should be used sparingly as they are magic in code. With a simple spell the reader is beamed to god knows where for some usually undocumented reason. The two main problems with continue are:

  • It may bypass the test condition
  • It may bypass the increment/decrement expression Consider the following example where both problems occur:
1loop {
2 ...
3 /* A lot of code */
4 ...
5 if condition {
6 continue;
7 }
8 ...
9 /* A lot of code */
10 ...
11 if i > STOP_VALUE { break; }
12}

"A lot of code" is necessary in order that the problem cannot be caught easily by the programmer.

From the above example, a further rule may be given: Mixing continue with break in the same loop is a sure way to disaster.

One Statement Per Line

There should be only one statement per line unless the statements are very closely related.

Justification

The code is easier to read. Use some white space too. Nothing better than to read code that is one line after another with no white space or comments.

One Variable Per Line

Related to this is always define one variable per line:

Do:

1let count = 0;
2let mut bytes_received = 0;

Don’t:

1let bytes_received, count;

Justification

  • Documentation can be added for the variable on the line.
  • It's clear that the variables are initialized.
  • Declarations are clear which reduces the probability of declaring a struct when you meant to declare just an int.

Make Macro Names Unique

Like global variables macros can conflict with macros from other packages.

  • Prepend macro names with package names.
  • Avoid simple and common names like max and min.

Short Functions

Functions should limit themselves to a page or two of code.

Justification

  • The idea is that the each method represents a technique for achieving a single objective.
  • Most arguments of inefficiency turn out to be false in the long run.
  • True function calls are slower than not, but there needs to be a thought out decision (see premature optimization).

Documentation

Use the following in the header of the lib.rs or main.rs file to help limit bad styles by the compiler

1#![deny(
2 missing_docs,
3 trivial_casts,
4 trivial_numeric_casts,
5 unconditional_recursion,
6 unused_import_braces,
7 unused_lifetimes,
8 unused_qualifications,
9 unused_extern_crates,
10 unused_parens,
11 while_true
12)]

Comments Should Tell a Story

Consider your comments a story describing the system. Expect your comments to be extracted by a robot and formed into a man page. Class comments are one part of the story, method signature comments are another part of the story, method arguments another part, and method implementation yet another part. All these parts should weave together and inform someone else at another point of time just exactly what you did and why.

Document Decisions

Comments should document decisions. At every point where you had a choice of what to do place a comment describing which choice you made and why. Archeologists will find this the most useful information.

Make Gotchas Explicit

Explicitly comment variables changed out of the normal control flow or other code likely to break during maintenance. Embedded keywords are used to point out issues and potential problems. Consider a robot will parse your comments looking for keywords, stripping them out, and making a report so people can make a special effort where needed.

Gotcha Keywords

  • author: specifies the author of the module

  • version: specifies the version of the module

  • param: specifies a parameter into a function

  • return: specifies what a function returns

  • see <link>: creates a link in the documentation to the file/function/variable to consult to get a better understanding on what the current block of code does.

  • TODO: what remains to be done

  • bug: report a bug found in the piece of code

Gotcha Formatting

  • Make the gotcha keyword the first symbol in the comment.
  • Comments may consist of multiple lines, but the first line should be a self-containing, meaningful summary.
  • The writer's name and the date of the remark should be part of the comment. This information is in the source repository, but it can take a quite a while to find out when and by whom it was added. Often gotchas stick around longer than they should. Embedding date information allows other programmer to make this decision. Embedding who information lets us know who to ask.

Commenting function declarations

Functions headers should be in the file where they are declared using the triple forward slashes.

Layering

Layering is the primary technique for reducing complexity in a system. A system should be divided into layers. Layers should communicate between adjacent layers using well defined interfaces. When a layer uses a non-adjacent layer then a layering violation has occurred. A layering violation simply means we have dependency between layers that is not controlled by a well defined interface. When one of the layers changes code could break. We don't want code to break so we want layers to work only with other adjacent layers.

Sometimes we need to jump layers for performance reasons. This is fine, but we should know we are doing it and document appropriately.

Miscellaneous

General advice

This section contains some miscellaneous do's and don'ts.

  • Don't use floating-point variables where discrete values are needed. Using a float for a loop counter is a great way to shoot yourself in the foot. Always test floating-point numbers as <= or >=, never use an exact comparison (== or !=).

  • Use cargo fmt to help style your code

  • Use cargo clippy to check for better and preferred manners of performing the same code.

Commenting Out Large Code Blocks

Sometimes large blocks of code need to be commented out for testing.

Add a Comment to Document Why

Add a short comment explaining why it is not implemented, obsolete or temporarily disabled.

No Magic Numbers

A magic number is a bare naked number used in source code. It's magic because no-one has a clue what it means including the author inside 3 months. For example:

1if 22 == foo { start_thermo_nuclear_war(); }
2else if 19 == foo { refund_lotso_money(); }
3else if 16 == foo { infinite_loop(); }
4else { cry_cause_im_lost(); }

In the above example what do 22 and 19 mean? If there was a number change or the numbers were just plain wrong how would you know? ​Instead of magic numbers use a real name that means something. You can use constants as names. For example:

1pub const PRESIDENT_WENT_CRAZY: usize = 22;
2const WE_GOOFED: usize= 19;
3const THEY_DIDNT_PAY: usize = 16;
4
5if PRESIDENT_WENT_CRAZY == foo { start_thermo_nuclear_war(); }
6else if WE_GOOFED == foo { refund_lotso_money(); }
7else if THEY_DIDNT_PAY == foo { infinite_loop(); }
8else {
9happy_days_i_know_why_im_here(); }

Now isn't that better? The const option is preferable because when debugging the debugger has enough information to display both the value and the label.

Error Return Check Policy

  • Never use unwrap on Result. If the type is Err, it will panic and crash the program. The only exception is if it has already been checked for error previously or in test code.
  • Never use unwrap on Option for the same reason if the type is None as Result is Err.

References

Adapted from http://www.possibility.com/Cpp/CppCodingStandard.html and NetBSD's style guidelines

Previous

C Library Style

Next

Graphics

On this page
Edit this page
Star Ockam repo