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

Тема
Описание
Доп.

#![no_std] - крейт будет связан с основным крейтом libcore, а не со стандартным крейтом std.

#![no_std] указывает, что эта программа не будет ссылаться на стандартный крейт, std. Вместо этого он будет ссылаться на свое подмножество core

no_std Для написание прошивки, ядра или кода загрузчика

Компилятор вставляет импорты основных примитивов по умолчанию.

Что бы отключить стандартные импорты используйте

#![no_implicit_prelude]

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;

Effective Rust

effective-rust/no-std

no_std

Это имеет смысл только в том случае, если все ящики, от которых вы зависите также no_std.

В std есть две большие части:

  • базовая библиотека языка → core
  • аллоцируемые структуры → alloc
  • взаимодействие с ОС → std (filesystem, threads, sockets, mutex, stdout, таймеры)

Но когда мы отключаем std т.е. используем no_std то alloc нам нужно явно подключить (core остается и так)

Rust поставляется со стандартной библиотекой, которая называется std, который включает код для широкого спектра распространённых задач: от стандартных структур данных до работы с сетями, от поддержки многопоточности до файлового ввода-вывода. Для удобства некоторые элементы из std автоматически импортируются в вашу программу через ::prelude набор общих use операторов, которые делают доступными распространенные типы без необходимости использования их полных имен (например, Vec вместо std::vec::Vec).

Атрибут #![no_std] уровня ящика в верхней части src/lib.rs

Rust поддерживает конфигурации без полной стандартной библиотеки std, что позволяет использовать его в системах, не имеющих полноценной операционной системы (например, без файловой системы, без сети). Rust также поддерживает сборку кода для сред, где невозможно предоставить полную стандартную библиотеку, таких как загрузчики, прошивки и встраиваемые платформы в целом.

При std компилятор вставляет импорты основных примитивов по умолчанию.

#![allow(unused)]
#![feature(prelude_import)]
fn main() {
#[prelude_import]
use std::prelude::rust_2021::*;
}

Что бы отключить стандартные импорты используйте

#![allow(unused)]
#![no_implicit_prelude]
fn main() {
}

Но для no_std необходимо явно включать нужные импорты.

Проверка зависимостей на no_std

Так как компилятор не сообщает что std а что no_std мы можем явно задать цель сборки в которой отсутвует std и тогда подтягиваемые зависимсоти с std сломают компиляцию, что даст нам понимание от чего избавится.

Мы собираем проект под таргет, в котором вообще нет std, например: thumbv6m-none-eabi (ARM Cortex-M0, популярный no_std target)

  1. Добавить локально (и в CI) таргет для cross-compile
rustup target add thumbv6m-none-eabi
  1. Команда для проверки no_std сборки

Если у вас библиотека (lib.rs):

cargo build --lib --target thumbv6m-none-eabi

Если у вас бинарник, то лучше отключить std через features:

Cargo features аддитивные — они только добавляют возможности, но никогда не должны выключать. Поэтому опция — добавляет std, а не отключает его. (Cargo объединяет features, выполняя логическое ИЛИ поэтому если один выключит а другой включи то по итогу получится features включена)

По умолчанию включена фича std

  • Если std включена → включаются dep1/std и dep2/std
  • Если std выключена → твой crate работает в no_std

Очень важно: Фича std только добавляет функциональность, а не отключает.

Cargo.toml:

[features]
default = ["std"]
std = ["dep1/std", "dep2/std"] 

И в коде:

#![cfg_attr(not(feature = "std"), no_std)]

Сборка:

cargo build --lib --no-default-features --target thumbv6m-none-eabi

Вариант через cargo tree:

if cargo tree -i std | grep -q "std"; then
    echo "❌ ERROR: std detected in dependency tree!"
    exit 1
else
    echo "✔ OK: no std in dependencies"
fi

Нельзя так:

[features]
no_std = []
#![allow(unused)]
fn main() {
#[cfg(feature = "no_std")]
fn something_without_std() { ... }

#[cfg(not(feature = "no_std"))]
fn something_with_std() { ... }
}

Потому что один пользователь включает no_std, другой нет → итог = включено → ломается.

core

Библиотека core содержит код который не выделяет память в heap. Поэтому структур данных Vec, Map, Sets там нет.

Даже при разработке для самых ограниченных платформ многие фундаментальные типы из стандартной библиотеки остаются доступными. Например, Option и Result. Они по-прежнему доступны через core::{Option, Result, Iterator, slice}

Типы из core доступны для всех программ Rust автоматически. Однако, как правило, их необходимо явно use указывать в no_std среде, посколькуstd prelude отсутствует.

alloc

Для использования heap в среде no_std должен быть аллокатор (например linked_list_allocator, buddy_system_allocator, jemalloc, ваш собственный…). Нужно включить crate alloc и тогда будут доступны структуры данных для работы с памятью heap

#![allow(unused)]
fn main() {
alloc::boxed::Box<T>
alloc::rc::Rc<T>
alloc::sync::Arc<T>
alloc::vec::Vec<T>
alloc::string::String
alloc::format!
alloc::collections::BTreeMap<K, V>
alloc::collections::BTreeSet<T>
}

Тип std::vec::Vec на самом деле это alloc::vec::Vec

Отсутствуют коллекции HashMap и HashSet так как для генерации хешей нужны возможности ОС. Но есть BTreeMap и BTreeSet.

Отсутствует структура синхронизации std::sync::Mutex. Для многопоточного кода в no_std используют crate spin

Если вы это сделали — у вас появляется heap и выделение памяти в no_std

#![allow(unused)]
#![no_std]
fn main() {
extern crate alloc;

use alloc::vec::Vec;

#[global_allocator]
static ALLOC: MyAllocator = MyAllocator::new();

}

Каждый вызов Vec::push() может вызвать аллокацию. Но Rust не предполагает что выделение памяти в heap может дать ошибку, т.е. Rust предполагает, что аллокатор не может провалиться. Нет способа обработать failure и продолжить выполнение, как в C (malloc → NULL). И это поведение Rust не подходит для embedded, kernel, или ограниченных систем.

Rust начал добавлять альтернативы, которые возвращают Result:

  • Vec::try_reserve(n) — резервирует память, возвращает Result<(), AllocError>
  • Box::try_new(value) — возвращает Result<Box<T>, AllocError> (nightly)

Пока нет полноценного Vec::try_push(), поэтому приходится сначала резервировать память, а потом делать push().

#![allow(unused)]
fn main() {
fn try_build_a_vec() -> Result<Vec<u8>, String> {
    let mut v = Vec::new();

    let required_size = 4;
    // Сначала резервируем память через try_reserve
    v.try_reserve(required_size)
        .map_err(|_e| format!("Failed to allocate {} items!", required_size))?;

    // Если аллокация успешна → безопасно делаем push
    v.push(1);
    v.push(2);
    v.push(3);
    v.push(4);

    Ok(v)
}
}

Или можно не использовать heap и отключить выделение памяти. В некоторых системах (например Linux kernel, embedded):

Можно отключить глобальную обработку OOM через no_global_oom_handling.

Тогда любая попытка infallible аллокации станет ошибкой компиляции/сборки, если она случайно появится.

Проблема сборки под no_std

Rust должен знать, на какой процессор будет компилировать, для этого нужно выбрать цель, по умолчанию используется x86_64-unknown-linux-gnu в Linux. Но когда мы отключаем std #![no_std] то цель нужно явно указать.

Цель нужна для правильной компиляции core/alloc под конкретный процессор.

Аллокатор linked_list_allocator для блокировки (spinlock) использует spinning_top который нуждается в атомарных инструкциях (compare_exchange, compare_exchange_weak) на AtomicBool.

Но цель сборки thumbv6m-none-eabi не поддерживают атомарные инструкции.

Цель сборки thumbv7em-none-eabihf поддерживают атомарные инструкции и всё работает.

Для симуляции no_std-бинарника, таргет без атомарных инструкций thumbv6m-none-eabi для проверки отсутствия std зависимостей:

(нужен свой аллокатор)


rustup target add thumbv6m-none-eabi
cargo build --package app_no_std --no-default-features --target thumbv6m-none-eabi

Для no_std-бинарника без проверки отсутствия std зависимостей, таргет с атомарными инструкциями thumbv7em-none-eabihf:

(используем библиотечный аллокатор linked_list_allocator)

 
rustup target add thumbv7em-none-eabihf
cargo build --package app2_no_std --no-default-features --target thumbv7em-none-eabihf 

libcore он предоставляет API-интерфейсы для языковых примитивов, таких как числа с плавающей запятой, строки и срезы, а также API-интерфейсы, которые предоставляют функции процессора, такие как атомарные операции и инструкции SIMD. Однако ему не хватает API-интерфейсов для всего, что связано с распределением памяти в куче и вводом-выводом.

alloc

crate spin

Когда вы используете no_std, вы фактически отключаете поддержку многих возможностей стандартной библиотеки, но вы всё ещё можете использовать функциональность, предоставляемую alloc, если ваша среда поддерживает её.

Вы можете использовать no_std без alloc, если вам не нужны динамические структуры данных, и вы хотите минимизировать зависимость от дополнительного кода.

Втягивание ящика alloc позволяет многим знакомым друзьям обращаться к ним по их настоящим именам:

alloc::boxed::Box<T>
alloc::rc::Rc<T>
alloc::sync::Arc<T>
alloc::vec::Vec<T>
alloc::string::String
alloc::format!
alloc::collections::BTreeMap<K, V>
alloc::collections::BTreeSet<T>

Благодаря этим возможностям становится возможным обеспечить no_std совместимость многих библиотечных ящиков, например, если библиотека не использует ввод-вывод или сетевое взаимодействие.

Но в alloc нет коллекций HashMap и HashSet специфичных для std, поэтому используйте BTreeMap и BTreeSet Еще одним заметным отсутствием является отсутствие функций синхронизации, таких как std::sync::Mutex, который требуется для многопоточного кода ( пункт 17 ). Эти типы специфичны для std потому что они полагаются на специфичные для ОС примитивы синхронизации, которые не доступны без ОС. Если вам нужно написать код, который является no_std и многопоточным, сторонние контейнеры, такие как spin вероятно, ваш единственный вариант.

[dependencies]
# Необходим для работы с `alloc` в `no_std`
alloc = "1.0"


#![no_std]
#![no_main]

extern crate alloc;

use alloc::vec::Vec;
use core::panic::PanicInfo;

#[no_mangle]
pub extern "C" fn main() -> ! {
    let mut vec = Vec::new();
    vec.push(1);
    vec.push(2);
    // Ваша логика
    loop {}
}

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

Ошибочное распределение

effective-rust/no-std.html#fallible-allocation

К сожалению, стандарт Rust библиотека alloc включает в себя распространенное предположение, что Выделение памяти в куче не может завершиться неудачей, и это не всегда верное предположение и скорее всего, сведется к следующему: panic! и завершение программы при недостатке памяти в куче.

fn try_build_a_vec() -> Result<Vec<u8>, String> {
    let mut v = Vec::new();

    // Perform a careful calculation to figure out how much space is needed,
    // here simplified to...
    let required_size = 4;

    v.try_reserve(required_size)
        .map_err(|_e| format!("Failed to allocate {} items!", required_size))?;

    // We now know that it's safe to do:
    v.push(1);
    v.push(2);
    v.push(3);
    v.push(4);

    Ok(v)
}
fn main(){}

#![no_std] для target thumbv7m-none-eabi

# Show targets:
$ rustup component list --installed

# Install your target:
$ rustup target add thumbv7m-none-eabi

$ rustup show

File .cargo/config.toml:

[build]
target = "thumbv7m-none-eabi"

Build:

$ cargo build
$ cargo build --target thumbv7m-none-eabi (если нет файла config.toml)

File main.rs:

#![no_main]
#![no_std]

use ::core::panic::PanicInfo;

#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
    loop {}
}

global (heap) allocator

comprehensive-rust/bare-metal/alloc

#![no_main]
#![no_std]

extern crate alloc;
extern crate panic_halt as _;

use alloc::string::ToString;
use alloc::vec::Vec;
use buddy_system_allocator::LockedHeap;

#[global_allocator]
static HEAP_ALLOCATOR: LockedHeap<32> = LockedHeap::<32>::new();

static mut HEAP: [u8; 65536] = [0; 65536];

pub fn entry() {
    // Safe because `HEAP` is only used here and `entry` is only called once.
    unsafe {
        // Give the allocator some memory to allocate.
        HEAP_ALLOCATOR
            .lock()
            .init(HEAP.as_mut_ptr() as usize, HEAP.len());
    }

    // Now we can do things that require heap allocation.
    let mut v = Vec::new();
    v.push("A string".to_string());
}

Будет управлять версией компилятора

$ cat rust-toolchain.toml 

[toolchain]
channel = "nightly-2022-06-09

#![no_std]
#![feature(start, lang_items)]

// Говорим компилятору влинковать libc
#[cfg(target_os = "linux")]
#[link(name = "c")]

extern "C" {
  // Объявляем внешнюю функцию из libc
  fn puts(s: *const u8) -> i32;
}

#[start] // Говорим, что выполнение надо начинать с этого символа
fn main(_argc: isize, _argv: *const *const u8) -> isize {
  unsafe {
    // В Расте строки не нуль-терминированные
    puts("Hello, world!\0".as_ptr());
  }
  return 0;
}

#[panic_handler] // Удовлетворяем компилятор
fn panic(_panic: &core::panic::PanicInfo<'_>) -> ! {
  loop {}
}

#[lang = "eh_personality"] // Удовлетворяем компилятор
extern "C" fn eh_personality() {}

Портирование библиотеки std в no_std

deploying-rust-in-existing-firmware

Портирование библиотеки std в no_std

Если библиотека не поддерживает no_std, ее все равно можно перенести в среду без ОС, особенно для анализаторов форматов файлов и других рабочих нагрузок, не зависящих от ОС. Функциональность более высокого уровня, такая как обработка файлов, многопоточность и асинхронный код, может представлять собой более сложную задачу. В таких случаях такая функциональность может быть скрыта за флажками функций, чтобы по-прежнему обеспечивать базовую функциональность в сборке no_std.

Чтобы перенести крейт std в no_std (core+alloc):

В файле Cargo.toml добавьте стандартную функцию, затем добавьте эту стандартную функцию к функциям по умолчанию. Добавьте следующие строки в начало lib.rs:

#![no_std]
#[cfg(feature = "std")]
extern crate std;
extern crate alloc;
  • Переместите все директивы использования из std в core или alloc.
  • Добавьте директивы use для всех типов, которые в противном случае были бы автоматически импортированы прелюдией std, например alloc::vec::Vec и alloc::string::String
  • Скройте все, что не существует в core или alloc и иначе не может поддерживаться в сборке no_std (например, доступ к файловой системе), за защитой #[cfg(feature = "std")]
  • Все, что необходимо для взаимодействия со встроенной средой, возможно, придется обрабатывать явно, например функции ввода-вывода. Скорее всего, они должны находиться за охраной #[cfg(not(feature = "std"))]
  • Отключите std для всех зависимостей (то есть измените их определения в Cargo.toml, если используете Cargo)