Introdução ao desenvolvimento de sistemas com Rust

1. Por que Rust? Contexto e Filosofia

No desenvolvimento de sistemas modernos, a segurança de memória sempre foi um desafio central. Linguagens como C e C++ oferecem controle total sobre o hardware, mas abrem portas para vulnerabilidades graves: vazamentos de memória, buffer overflows e use-after-free. Rust surge como uma alternativa que combina o desempenho bruto dessas linguagens com garantias de segurança em tempo de compilação — sem a necessidade de um garbage collector.

A filosofia de Rust se baseia em três pilares: confiabilidade, desempenho e produtividade. O compilador é rigoroso: ele impede, já na compilação, erros que em outras linguagens só seriam detectados em execução. Isso torna Rust ideal para sistemas críticos, como navegadores (o motor Servo, usado no Firefox), infraestrutura de armazenamento (Dropbox reescreveu partes de seu sistema de arquivos em Rust) e sistemas embarcados.

2. Primeiros Passos: Instalação e Ecossistema

Para começar, instale o Rust via rustup, o instalador oficial:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Isso instala o compilador rustc e o gerenciador de pacotes cargo. O cargo é a ferramenta central: ele cria projetos, gerencia dependências, compila e testa o código.

Crie seu primeiro projeto:

cargo new meu_primeiro_projeto
cd meu_primeiro_projeto
cargo run

O comando cargo new gera uma estrutura com Cargo.toml (manifesto do projeto) e src/main.rs. O cargo run compila e executa o binário. Simples e rápido.

3. Fundamentos da Linguagem: Variáveis, Tipos e Controle de Fluxo

Em Rust, variáveis são imutáveis por padrão. Para tornar uma variável mutável, use mut:

let x = 5;        // imutável
let mut y = 10;   // mutável
y = 20;           // permitido

O sombreamento (shadowing) permite reutilizar o nome de uma variável com um novo valor ou tipo:

let valor = "123";
let valor: u32 = valor.parse().expect("Falha ao converter");

Os tipos primitivos incluem inteiros (i32, u64, u8), ponto flutuante (f64, f32), booleanos (bool) e caracteres (char). Tuplas e arrays são tipos compostos:

let tupla: (i32, f64, char) = (42, 3.14, 'R');
let array: [i32; 3] = [1, 2, 3];
let slice = &array[0..2];  // referência a uma parte do array

Estruturas de controle seguem a sintaxe esperada:

if x > 10 {
    println!("x é maior que 10");
} else {
    println!("x é menor ou igual a 10");
}

let mut contador = 0;
loop {
    contador += 1;
    if contador == 5 { break; }
}

for numero in 1..5 {
    println!("{}", numero);
}

4. Ownership, Borrowing e Lifetime: O Coração da Segurança

O sistema de ownership é o que torna Rust único. Cada valor em Rust tem um único dono. Quando o dono sai de escopo, o valor é automaticamente desalocado.

let s1 = String::from("Olá");
let s2 = s1;  // s1 é movido para s2 — s1 não pode mais ser usado
// println!("{}", s1);  // ERRO: s1 foi movido

O borrowing permite referenciar um valor sem tomar posse dele. Referências imutáveis (&T) são múltiplas, mas não permitem modificar o valor. Referências mutáveis (&mut T) são exclusivas:

fn calcular_tamanho(s: &String) -> usize {
    s.len()
}

fn modificar(s: &mut String) {
    s.push_str(" mundo");
}

Os lifetimes são anotações que informam ao compilador por quanto tempo as referências são válidas. Na prática, o compilador infere a maioria dos lifetimes, mas em funções complexas precisamos explicitá-los:

fn maior<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

5. Structs, Enums e Pattern Matching

Structs permitem criar tipos personalizados:

struct Usuario {
    nome: String,
    idade: u32,
    ativo: bool,
}

let user = Usuario {
    nome: String::from("Alice"),
    idade: 30,
    ativo: true,
};

Métodos são definidos com impl:

impl Usuario {
    fn saudacao(&self) -> String {
        format!("Olá, meu nome é {}", self.nome)
    }
}

Enums são extremamente poderosos. O tipo Option<T> representa um valor que pode ou não existir:

let presente: Option<i32> = Some(42);
let ausente: Option<i32> = None;

O tipo Result<T, E> representa sucesso ou falha:

fn dividir(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("Divisão por zero"))
    } else {
        Ok(a / b)
    }
}

Pattern matching com match desestrutura enums de forma segura:

match dividir(10.0, 2.0) {
    Ok(resultado) => println!("Resultado: {}", resultado),
    Err(erro) => println!("Erro: {}", erro),
}

6. Tratamento de Erros com Result e Option

O operador ? simplifica a propagação de erros:

fn ler_arquivo(caminho: &str) -> Result<String, std::io::Error> {
    let conteudo = std::fs::read_to_string(caminho)?;  // propaga o erro automaticamente
    Ok(conteudo)
}

Combinadores como map, and_then e unwrap_or oferecem controle fino:

let numero = "42".parse::<i32>()
    .map(|n| n * 2)
    .unwrap_or(0);  // se falhar, retorna 0

Para projetos maiores, crates como thiserror e anyhow facilitam a criação de tipos de erro customizados e o tratamento simplificado.

7. Concorrência Segura com Threads e async/await

Rust oferece concorrência sem data races graças ao sistema de ownership. Threads são criadas com std::thread:

use std::thread;

let handle = thread::spawn(|| {
    println!("Thread filha executando");
});
handle.join().unwrap();

Para comunicação entre threads, usamos canais (mpsc):

use std::sync::mpsc;

let (tx, rx) = mpsc::channel();
thread::spawn(move || {
    tx.send("Mensagem da thread").unwrap();
});
println!("Recebido: {}", rx.recv().unwrap());

Estado compartilhado usa Mutex e Arc (contagem de referências atômica):

use std::sync::{Arc, Mutex};

let contador = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
    let contador = Arc::clone(&contador);
    handles.push(thread::spawn(move || {
        let mut num = contador.lock().unwrap();
        *num += 1;
    }));
}

Para programação assíncrona, o runtime Tokio é o padrão:

use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let tarefa = tokio::spawn(async {
        sleep(Duration::from_secs(1)).await;
        println!("Tarefa assíncrona concluída");
    });
    tarefa.await.unwrap();
}

8. Projeto Prático: Uma Ferramenta de Linha de Comando Simples

Vamos criar uma ferramenta que lê um arquivo e conta palavras, usando clap para argumentos de linha de comando.

Adicione ao Cargo.toml:

[dependencies]
clap = { version = "4.0", features = ["derive"] }

Estrutura do código em src/main.rs:

use clap::Parser;
use std::fs;
use std::path::PathBuf;

#[derive(Parser)]
#[command(name = "contador")]
struct Args {
    /// Caminho do arquivo
    arquivo: PathBuf,
}

fn main() {
    let args = Args::parse();

    let conteudo = fs::read_to_string(&args.arquivo)
        .expect("Erro ao ler o arquivo");

    let palavras: Vec<&str> = conteudo.split_whitespace().collect();
    println!("Número de palavras: {}", palavras.len());
}

Compile e execute:

cargo build --release
./target/release/contador arquivo.txt

O binário é leve, rápido e seguro. Para publicar, basta distribuir o executável gerado.


Rust não é apenas mais uma linguagem; é uma nova forma de pensar sobre sistemas, onde segurança e desempenho caminham juntos. Comece com projetos pequenos, explore a documentação oficial e, em breve, você estará construindo ferramentas robustas e eficientes.

Referências