Embedded and no_std Rust

Purpose and Scope

Embedded Rust in this repository is best understood as the Rust language and core libraries applied to programs that cannot assume an operating system, a process runtime, threads, files, or a heap allocator. The project README explicitly calls out embedded devices as one of Rust’s performance-oriented use cases, while also identifying this repository as the main source tree for the compiler, standard library, and documentation. That combination matters: embedded development is not a separate language mode, but a way of using the same compiler and library layers with a smaller platform contract.

Sources: README.md, library/core/src/lib.rs

The official Embedded Book frames the user-facing goal as programming bare-metal systems such as microcontrollers while still benefiting from Rust’s type system and ownership model. In repository terms, the first boundary to understand is the distinction between the full standard library and the core library. Normal Rust crates can use the standard library by default, but a bare-metal crate usually opts out of that default and starts from the dependency-free foundation instead. The source documentation for the core library describes precisely why that layer is suitable for constrained environments.

Sources: library/core/src/lib.rs

This page is for readers who already know that they need a no-std or embedded setup, but want to connect the product documentation language to the source tree. It explains what the core library provides, what it deliberately avoids, where target-specific compiler knowledge lives, and how the repository tests the ability to build core for selected target configurations. It is not a board-support guide; instead, it helps you choose the next source area to inspect when a no-std build, panic strategy, target specification, or build-std experiment behaves unexpectedly.

Sources: compiler/rustc_target/README.md, tests/build-std/configurations/rmake.rs

Relevant Source Files

  • README.md — Establishes that this is the main Rust source repository, links the official learning and documentation entry points, and names embedded devices as a performance-sensitive Rust use case.
  • library/core/src/lib.rs — Documents the core library as the dependency-free foundation of Rust’s libraries, with no heap allocation, concurrency, I/O, libc, or system-library dependency, and lists symbols consumers may need to provide.
  • compiler/rustc_target/README.md — Identifies the compiler crate that contains low-level details specific to compilation targets.
  • tests/build-std/configurations/rmake.rs — Exercises building a scratch no-std library with build-std for core across Tier 1 targets and selected optimization, debug, and panic settings.

Core Primitives

The most important primitive for no-std Rust is the core library. Its crate-level documentation calls it the portable glue between the language and the libraries, defining intrinsic and primitive building blocks used by all Rust code. That wording is stronger than saying core is merely a subset of the standard library. It is the bottom layer that the standard library depends on, and it is designed to avoid linking to upstream libraries, system libraries, or libc. For embedded readers, that means core is the language-adjacent surface available before platform integration begins.

Sources: library/core/src/lib.rs

The second primitive is the crate-level decision to opt out of the default standard library. The build-std test writes a minimal library containing the no-std crate attribute and then asks Cargo to build core for a target. That test is small, but it captures the shape of many embedded experiments: create a library or firmware crate, declare that it does not use the host standard library, select a target, and ensure that the low-level core pieces can be built for that target. The repository test uses this to validate infrastructure, not to model an application.

Sources: tests/build-std/configurations/rmake.rs

The third primitive is target knowledge. Embedded systems are target-sensitive because pointer width, available atomics, ABI conventions, linker behavior, and panic strategy can all vary. The rustc target crate README is concise, but its purpose is direct: it contains very low-level details specific to compilation targets. In practice, when a no-std build differs between host and device, the relevant question is often not whether the Rust language feature exists, but whether the compiler’s target description, code generation assumptions, and required runtime symbols match the environment being built for.

Sources: compiler/rustc_target/README.md

System-to-Code Mapping

The repository’s top-level orientation maps embedded Rust to the same major components used by the rest of Rust. The compiler compiles the program, the libraries define the available APIs, and documentation teaches users which layer is appropriate. The README’s documentation links point newcomers toward the official learning material, while its “Why Rust?” section explains the qualities that make Rust attractive for embedded devices: memory efficiency, reliability from ownership and types, and tooling. Those product claims correspond to source areas rather than to a single embedded-only subsystem.

Sources: README.md

At the library layer, core defines what is available when platform services are absent. Its documentation says it is minimal and not aware of heap allocation, concurrency, or I/O because those require platform integration. This is the core design rule for no-std code: APIs that require an allocator, operating-system handle, filesystem, network stack, thread scheduler, or libc routine do not belong in the platform-agnostic base. A firmware project can add those facilities through device crates, allocators, board support packages, or a custom runtime, but core itself stays small and portable.

Sources: library/core/src/lib.rs

At the compiler layer, target definitions are where platform differences become actionable. The README for the target crate does not enumerate targets in the snippet, but it states that this crate owns low-level target-specific details. The build-std test demonstrates one way the project consumes those details: it asks rustc to print all target specifications as JSON, reads target metadata, and selects Tier 1 targets for coverage. That flow shows that target specifications are machine-readable inputs to tooling, tests, and compiler behavior, not only prose documentation.

Sources: compiler/rustc_target/README.md, tests/build-std/configurations/rmake.rs

Execution Flow

A typical no-std workflow begins by choosing the library boundary. If the program cannot depend on the default standard library, the crate declares that choice and writes against core-compatible APIs. The repository test makes this boundary intentionally stark by writing only a tiny library with a no-std declaration. Real firmware would add device registers, startup code, interrupt handlers, and perhaps an allocator, but the minimal test is valuable because it isolates whether core itself can be built for the selected target configuration before any board-specific code complicates the result.

Sources: tests/build-std/configurations/rmake.rs

After the crate boundary comes the target selection. The test obtains target specifications from rustc using the unstable all-target-specs JSON output, then inspects each target’s metadata for a tier value. It asserts that in-tree targets have tier metadata and uses Tier 1 targets for the build matrix. For developers, the lesson is that target metadata is part of the contract. If you are working on platform support, missing or inconsistent metadata can break tests and tooling even before a user program reaches linking or hardware execution.

Sources: tests/build-std/configurations/rmake.rs

The next part of the flow is profile variation. The test intentionally avoids every possible flag combination, but covers optimization levels, debug information levels, and panic settings chosen to expose likely problems in compiler-builtins and generated code. It exercises release builds, opt levels zero and three, debug levels zero and two, and panic modes including abort and immediate abort. Embedded projects often make similar tradeoffs: release size, debugging quality, and panic behavior affect whether the binary is useful on a constrained board or in a simulator.

Sources: tests/build-std/configurations/rmake.rs

A representative command shape, mirroring the test’s intent rather than prescribing a stable user interface, is:

cargo build --release -Zbuild-std=core --target <target> -j1

The unstable flags in the repository test are significant. The test sets bootstrap-related environment and uses unstable options because it validates compiler infrastructure inside the Rust tree. Users should treat this as evidence of how Rust’s own tests exercise core builds, not as a guarantee that every flag is stable for application projects. When translating the pattern to an embedded project, start from the official Embedded Book and target documentation, then use repository tests to understand what the compiler team considers important to keep working.

Sources: tests/build-std/configurations/rmake.rs

Implementation Details and Edge Cases

The core library still assumes a few low-level symbols exist, even though it does not link to libc or system libraries. Its documentation lists memory routines such as copying, moving, setting, comparing, and string length functions, and explains that code generation backends may generate them or that compiler-builtins can provide them. For embedded developers, this is an important edge case: no-std does not mean no runtime obligations at all. It means the obligations are smaller, more explicit, and often supplied by the target, compiler-builtins, or project-specific runtime support.

Sources: library/core/src/lib.rs

Panic handling is another explicit boundary. Core’s documentation states that consumers must define a panic function taking panic information and that the implementation must not return, with the implementation marked using the panic handler attribute. This is why many embedded examples discuss panic crates or custom panic behavior early. Without the standard library, there is no default host-oriented panic reporting path to rely on. The repository’s build matrix also varies panic behavior, showing that panic strategy is not an afterthought for low-level Rust configurations.

Sources: library/core/src/lib.rs, tests/build-std/configurations/rmake.rs

Exception handling personality is a related edge case. Core documents a personality symbol used by compiler failure mechanisms and notes that crates which do not trigger panic can be assured it is never called. In small firmware, this distinction can matter for linking and binary size. A project configured to abort on panic may avoid unwinding machinery, while another configuration may need additional symbols. When debugging a no-std link failure, inspect whether the failure is caused by missing memory routines, a missing panic handler, personality requirements, or target-specific assumptions.

Sources: library/core/src/lib.rs

Testing Signals

The build-std configuration test is the strongest repository signal for no-std infrastructure in the supplied evidence. It generates many tasks by combining target, optimization level, debug level, and panic strategy, then runs them concurrently while respecting the job count supplied by bootstrap. The comments note that the test can be memory hungry and that it is tuned to find problems generating code for compiler-builtins. That tells contributors what the project is defending: not one example board, but the repeatability of building core across important target and profile combinations.

Sources: tests/build-std/configurations/rmake.rs

The same test also clarifies the current scope of coverage. It presently compiles Tier 1 targets, but its own comments say that because it uses build-std for core, it could use any list of targets. That distinction is useful when interpreting failures. A Tier 1 failure in this test suggests a regression in a high-priority supported configuration. A non-Tier 1 embedded target may still be viable, but it may rely on different testing, out-of-tree target specifications, or project-specific runtime crates. The repository’s test therefore provides confidence while leaving room for broader ecosystem experimentation.

Sources: tests/build-std/configurations/rmake.rs

Next Steps

If you are learning embedded Rust, read the Embedded Book alongside the standard library and core library documentation so you can separate language concepts from platform services. Then create the smallest possible no-std crate for your target before adding drivers or board support. If you are contributing to Rust itself, follow failures from a no-std build toward the relevant layer: core documentation for missing runtime symbols, target infrastructure for specification problems, and build-std tests for regressions in core compilation. Related OpenWiki pages expand the library layering, target architecture, and source-build workflows.

Sources: README.md, library/core/src/lib.rs, compiler/rustc_target/README.md, tests/build-std/configurations/rmake.rs