fn main() {
const fn array_size() -> usize {
#[cfg(feature="use_more_ram")]
{ 1024 }
#[cfg(not(feature="use_more_ram"))]
{ 128 }
}
static BUF: [u32; array_size()] = [0u32; array_size()];
}
These are new to stable Rust as of 1.31, so documentation is still sparse. The functionality available to const fn is also very limited at the time of writing; in future Rust releases it is expected to expand on what is permitted in a const fn.
Rust provides an extremely powerful macro system. While the C preprocessor operates almost directly on the text of your source code, the Rust macro system operates at a higher level. There are two varieties of Rust macro: macros by example and procedural macros. The former are simpler and most common; they look like function calls and can expand to a complete expression, statement, item, or pattern. Procedural macros are more complex but permit extremely powerful additions to the Rust language: they can transform arbitrary Rust syntax into new Rust syntax.
In general, where you might have used a C preprocessor macro, you probably want to see if a macro-by-example can do the job instead. They can be defined in your crate and easily used by your own crate or exported for other users. Be aware that since they must expand to complete expressions, statements, items, or patterns, some use cases of C preprocessor macros will not work, for example a macro that expands to part of a variable name or an incomplete set of items in a list.
As with Cargo features, it is worth considering if you even need the macro. In many cases a regular function is easier to understand and will be inlined to the same code as a macro. The #[inline] and #[inline(always)] attributes give you further control over this process, although care should be taken here as well — the compiler will automatically inline functions from the same crate where appropriate, so forcing it to do so inappropriately might actually lead to decreased performance.
Explaining the entire Rust macro system is out of scope for this tips page, so you are encouraged to consult the Rust documentation for full details.
Most Rust crates are built using Cargo (although it is not required). This takes care of many difficult problems with traditional build systems. However, you may wish to customise the build process. Cargo provides build.rs scripts for this purpose. They are Rust scripts which can interact with the Cargo build system as required.
Common use cases for build scripts include:
• provide build-time information, for example statically embedding the build date or Git commit hash into your executable
• generate linker scripts at build time depending on selected features or other logic
• change the Cargo build configuration
• add extra static libraries to link against
At present there is no support for post-build scripts, which you might traditionally have used for tasks like automatic generation of binaries from the build objects or printing build information.
Using Cargo for your build system also simplifies cross-compiling. In most cases it suffices to tell Cargo --target thumbv6m-none-eabi and find a suitable executable in target/thumbv6m-none-eabi/debug/myapp.
For platforms not natively supported by Rust, you will need to build libcore for that target yourself. On such platforms, Xargo can be used as a stand-in for Cargo which automatically builds libcore for you.
In C you are probably used to accessing arrays directly by their index:
int16_t arr[16];
int i;
for(i=0; i<sizeof(arr)/sizeof(arr[0]); i++) {
process(arr[i]);
}
In Rust this is an anti-pattern: indexed access can be slower (as it needs to be bounds checked) and may prevent various compiler optimisations. This is an important distinction and worth repeating: Rust will check for out-of-bounds access on manual array indexing to guarantee memory safety, while C will happily index outside the array.
Instead, use iterators:
let arr = [0u16; 16];
for element in arr.iter() {
process(*element);
}
Iterators provide a powerful array of functionality you would have to implement manually in C, such as chaining, zipping, enumerating, finding the min or max, summing, and more. Iterator methods can also be chained, giving very readable data processing code.
See the Iterators in the Book and Iterator documentation for more details.
In Rust, pointers (called raw pointers) exist but are only used in specific circumstances, as dereferencing them is always considered unsafe -- Rust cannot provide its usual guarantees about what might be behind the pointer.
In most cases, we instead use references, indicated by the & symbol, or mutable references, indicated by &mut. References behave similarly to pointers, in that they can be dereferenced to access the underlying values, but they are a key part of Rust's ownership system: Rust will strictly enforce that you may only have one mutable reference or multiple non-mutable references to the same value at any given time.
In practice this means you have to be more careful about whether you need mutable access to data: where in C the default is mutable and you must be explicit about const, in Rust the opposite is true.
One situation where you might still use raw pointers is interacting directly with hardware (for example, writing a pointer to a buffer into a DMA peripheral register), and they are also used under the hood for all peripheral access crates to allow you to read and write memory-mapped registers.
In C, individual variables may be marked volatile, indicating to the compiler that the value in the variable may change between accesses. Volatile variables are commonly used in an embedded context for memory-mapped registers.
In Rust, instead of marking a variable as volatile, we use specific methods to perform volatile access: core::ptr::read_volatile and core::ptr::write_volatile. These methods take a *const T or a *mut T (raw pointers, as discussed above) and perform a volatile read or write.
For example, in C you might write:
volatile bool signalled = false;