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, Box etc., a menos que integremos a biblioteca alloc e 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. Em no_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 como cortex_m_rt para 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-halt ou panic-abort para simplificar o tratamento de panics em ambientes embarcados.
  • Prefira tipos de tamanho fixo e evite tipos como String a menos que necessário.
  • Para projetos embarcados, use abstrações de HAL (Hardware Abstraction Layer) como embedded-hal para 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

Exercícios

  1. Explique a diferença fundamental entre core e std em Rust.

✓ Resposta: A biblioteca 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).
  • Liste três situações em que faz sentido usar no_std.
  • ✓ Resposta: 1) Sistemas embarcados (microcontroladores); 2) Desenvolvimento de kernels de SO; 3) Bibliotecas que precisam ser portáveis para qualquer plataforma sem SO.
  • Escreva um exemplo de código Rust no_std que define uma função que retorna o maior de dois números.
  • ✓ Resposta:
    #![no_std]
    
    pub fn max(a: i32, b: i32) -> i32 {
        if a > b { a } else { b }
    }
    
  • Qual é a principal limitação em relação à alocação dinâmica de memória em no_std?
  • ✓ Resposta: Em 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.
  • Explique por que a macro println! não está disponível em no_std.
  • ✓ Resposta: A macro 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.