Programação no_std
Esta aula explora a programação no_std em Rust, abordando o que é, quando usar (especialmente em sistemas embarcados), as diferenças entre core e std, e as limitações. Você aprenderá a criar programas sem o sistema operacional e a usar as bibliotecas core e alloc.
Bem-vindo à aula sobre programação no_std em Rust. Em muitos cenários de desenvolvimento, especialmente em sistemas embarcados, kernels de sistema operacional ou ambientes com recursos extremamente limitados, não temos a disponibilidade de um sistema operacional completo. Rust oferece suporte a essa realidade através do atributo #![no_std], que permite escrever código que não depende da biblioteca padrão (std). Nesta aula, vamos explorar o que isso significa, quando é necessário, como as bibliotecas core e std se diferenciam, e quais limitações devemos enfrentar.
Compreender o no_std é essencial para quem deseja trabalhar com microcontroladores, drivers de dispositivos, sistemas operacionais ou qualquer ambiente onde o suporte a alocação dinâmica de memória e a abstrações de alto nível não estejam disponíveis. Vamos começar com uma visão geral e depois aprofundar em cada aspecto.
O que é
A programação no_std em Rust refere-se à criação de programas que não utilizam a biblioteca padrão (std). Em vez disso, eles dependem apenas da biblioteca core, que é uma parte essencial da linguagem e fornece funcionalidades básicas como tipos primitivos, iteradores, traits fundamentais e operações de baixo nível que não requerem um sistema operacional. O atributo #![no_std] no início do crate (geralmente no main.rs ou lib.rs) instrui o compilador a não vincular automaticamente a std e a usar apenas o que é fornecido pelo core.
Quando você escreve um programa no_std, você assume a responsabilidade de fornecer ou gerenciar elementos que normalmente seriam fornecidos pelo sistema operacional, como alocação de memória (se necessário), manipulação de entrada/saída e tratamento de panics. Isso torna o código mais portátil e adequado para ambientes onde não há um SO, como em microcontroladores.
Um exemplo simples de um crate no_std:
#![no_std]
// Sem a linha acima, o compilador tentaria usar std.
// Agora podemos usar apenas itens de core.
pub fn soma(a: i32, b: i32) -> i32 {
a + b
}
Note que não usamos println! ou outras macros da std, pois elas não estão disponíveis. Em vez disso, teríamos que implementar nossa própria saída ou usar uma biblioteca específica para o hardware.
Quando usar (embarcados)
A programação no_std é amplamente utilizada no desenvolvimento para sistemas embarcados, como microcontroladores (Arduino, STM32, ESP32, etc.) e outros dispositivos com recursos limitados. Nesses ambientes, não há um sistema operacional completo; o programa é executado diretamente no hardware. Portanto, não podemos depender de funcionalidades como alocação dinâmica de memória (heap), threads ou acesso a sistema de arquivos, que exigem suporte do SO.
Além dos embarcados, no_std é usado em:
- Kernels de sistemas operacionais (ex.: Redox OS, que é escrito em Rust).
- Drivers de dispositivos que operam em modo kernel.
- Bibliotecas que precisam ser portáveis para qualquer plataforma, incluindo aquelas sem SO.
- Aplicações de alto desempenho onde a sobrecarga da std não é aceitável.
Em sistemas embarcados, o código no_std é compilado para o alvo específico (por exemplo, thumbv7em-none-eabihf para ARM Cortex-M) e vinculado diretamente ao hardware, sem nenhum sistema operacional intermediário. Isso permite um controle fino sobre o uso de memória e recursos.
Um exemplo típico de código para microcontrolador usando no_std e a crate cortex_m_rt (para inicialização):
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _; // panics são tratados como halt
#[entry]
fn main() -> ! {
// Código do programa
loop {}
}
Aqui, #![no_main] indica que não usamos a função main padrão do Rust, pois em sistemas embarcados a inicialização é feita por um runtime específico.
core vs std
A biblioteca core é a base da linguagem Rust e contém tipos e funções que não dependem de um sistema operacional. Ela inclui:
- Tipos primitivos:
i32,u8,bool,char, etc. - Traits fundamentais:
Copy,Clone,Drop,Iterator,From,Into, etc. - Coleções básicas:
Option,Result,Iterator. - Funções de memória:
mem::size_of,mem::swap, etc. - Operações de ponteiro:
ptr. - Tipos de erro e panics:
panic!.
Por outro lado, a biblioteca std é uma extensão do core que adiciona funcionalidades que dependem do sistema operacional, como:
- Alocação de memória dinâmica (heap) via
Box,Vec,String. - Entrada/saída (I/O) com arquivos, rede, etc.
- Threads e sincronização.
- Processos e ambiente do sistema.
- Funções de relógio e tempo.
Quando você usa #![no_std], você não pode usar diretamente itens da std, mas pode usar core. Para usar coleções como Vec ou String em um ambiente no_std, você precisa da biblioteca alloc, que fornece a infraestrutura de alocação dinâmica, mas requer que você forneça um alocador global (por exemplo, usando a crate alloc-cortex-m).
Exemplo de uso de core:
#![no_std]
use core::fmt;
struct Ponto { x: i32, y: i32 }
impl fmt::Display for Ponto {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
Observe que core::fmt está disponível, mas não podemos usar println! porque ele requer std::io.
Limitações
Programar em no_std impõe várias limitações que devemos considerar:
- Sem alocação dinâmica padrão: Não podemos usar
Vec,String,Boxetc., a menos que integremos a bibliotecaalloce forneçamos um alocador global. Isso é complexo e muitas vezes evitado em favor de buffers estáticos. - Sem I/O padrão: Não temos
println!,File,stdin, etc. Precisamos implementar nossa própria comunicação com o hardware (por exemplo, via UART) ou usar bibliotecas específicas da plataforma. - Sem threads: Não há suporte a threads do SO. Em sistemas embarcados, usamos interrupções ou RTOS (Real-Time Operating Systems) para concorrência.
- Sem panics com mensagens completas: A macro
panic!não pode fornecer mensagens formatadas porque isso exigiria alocação dinâmica. Emno_std, o tratamento de panic é reduzido, muitas vezes apenas parando o sistema. - Sem acesso a funcionalidades dependentes de SO: Como rede, sistema de arquivos, variáveis de ambiente, etc.
- Inicialização manual: O código precisa definir seu próprio ponto de entrada e inicialização do hardware, já que não há runtime do Rust (como o que configura a pilha e chama
main). Usamos crates comocortex_m_rtpara isso.
Apesar dessas limitações, no_std é poderoso porque permite escrever código altamente portátil e de baixo nível, aproveitando a segurança de memória do Rust sem sacrificar o desempenho.
Um exemplo de limitação em código: tentar usar Vec sem alloc resultará em erro de compilação:
#![no_std]
// Isto não compila: Vec não está disponível sem alloc
// let v: Vec<i32> = Vec::new();
Para usar Vec, você precisaria adicionar extern crate alloc; e configurar um alocador, como mostrado abaixo:
#![no_std]
extern crate alloc;
use alloc::vec::Vec;
// Você também precisa fornecer um alocador global (ex.: usando a crate alloc-cortex-m)
Boas Práticas e Observações Finais
Ao trabalhar com no_std, algumas boas práticas incluem:
- Utilize buffers estáticos sempre que possível, evitando alocação dinâmica.
- Use a crate
panic-haltoupanic-abortpara simplificar o tratamento de panics em ambientes embarcados. - Prefira tipos de tamanho fixo e evite tipos como
Stringa menos que necessário. - Para projetos embarcados, use abstrações de HAL (Hardware Abstraction Layer) como
embedded-halpara portabilidade. - Teste seu código em um emulador ou hardware real desde o início, pois erros de runtime são difíceis de depurar sem I/O.
Em resumo, a programação no_std é uma habilidade valiosa para desenvolvedores Rust que desejam explorar sistemas de baixo nível. Compreender as diferenças entre core e std e as limitações envolvidas permitirá que você escreva código eficiente e seguro para uma variedade de plataformas.
Referências
- Atributo no_std na referência Rust
- Documentação da biblioteca core
- Documentação da biblioteca std
- The Embedded Rust Book
- Documentação da biblioteca alloc
- Crate panic-halt
- Crate embedded-hal
Exercícios
- Explique a diferença fundamental entre
coreestdem Rust.
core contém tipos e funções que não dependem de um sistema operacional, enquanto a std adiciona funcionalidades que dependem do SO, como alocação dinâmica, I/O, threads, etc. Em no_std, usamos apenas core (e opcionalmente alloc).no_std.no_std que define uma função que retorna o maior de dois números.#![no_std]
pub fn max(a: i32, b: i32) -> i32 {
if a > b { a } else { b }
}
no_std?no_std, não há alocador de memória padrão, então não podemos usar Vec, String, Box etc. Para usá-los, precisamos integrar a biblioteca alloc e fornecer um alocador global.println! não está disponível em no_std.println! faz parte da biblioteca std e depende de funcionalidades de I/O e formatação que exigem um sistema operacional. Em no_std, não temos essas funcionalidades; precisamos implementar nossa própria saída ou usar bibliotecas específicas da plataforma.