Explorando os Superpoderes do Unsafe Rust

Beginner

This tutorial is from open-source community. Access the source code

Introdução

Bem-vindo ao Unsafe Rust. Este laboratório faz parte do Rust Book. Você pode praticar suas habilidades em Rust no LabEx.

Neste laboratório, exploraremos o unsafe Rust, um recurso que nos permite contornar as garantias de segurança de memória impostas em tempo de compilação e nos dá superpoderes extras, ao mesmo tempo em que entendemos os riscos e responsabilidades envolvidos em seu uso.

Unsafe Rust

Todo o código que discutimos até agora teve as garantias de segurança de memória do Rust impostas em tempo de compilação. No entanto, o Rust tem uma segunda linguagem escondida dentro dele que não impõe essas garantias de segurança de memória: ela é chamada de unsafe Rust e funciona como o Rust regular, mas nos dá superpoderes extras.

O Unsafe Rust existe porque, por natureza, a análise estática é conservadora. Quando o compilador tenta determinar se o código mantém ou não as garantias, é melhor que ele rejeite alguns programas válidos do que aceitar alguns programas inválidos. Embora o código possa estar correto, se o compilador Rust não tiver informações suficientes para ter confiança, ele rejeitará o código. Nesses casos, você pode usar código unsafe para dizer ao compilador: "Confie em mim, eu sei o que estou fazendo." Esteja avisado, no entanto, que você usa o Unsafe Rust por sua própria conta e risco: se você usar código unsafe incorretamente, problemas podem ocorrer devido à insegurança da memória, como a desreferenciação de ponteiros nulos.

Outra razão pela qual o Rust tem um alter ego unsafe é que o hardware subjacente do computador é inerentemente unsafe. Se o Rust não permitisse que você fizesse operações unsafe, você não poderia realizar certas tarefas. O Rust precisa permitir que você faça programação de sistemas de baixo nível, como interagir diretamente com o sistema operacional ou até mesmo escrever seu próprio sistema operacional. Trabalhar com programação de sistemas de baixo nível é um dos objetivos da linguagem. Vamos explorar o que podemos fazer com o Unsafe Rust e como fazê-lo.

Superpoderes Unsafe

Para mudar para o Unsafe Rust, use a palavra-chave unsafe e, em seguida, inicie um novo bloco que contém o código unsafe. Você pode realizar cinco ações no Unsafe Rust que não pode no Rust seguro, que chamamos de superpoderes unsafe. Esses superpoderes incluem a capacidade de:

  1. Desreferenciar um ponteiro bruto (raw pointer)
  2. Chamar uma função ou método unsafe
  3. Acessar ou modificar uma variável estática mutável
  4. Implementar uma trait unsafe
  5. Acessar campos de unions

É importante entender que unsafe não desliga o verificador de empréstimos (borrow checker) ou desabilita qualquer outra verificação de segurança do Rust: se você usar uma referência em código unsafe, ela ainda será verificada. A palavra-chave unsafe apenas dá acesso a esses cinco recursos que, então, não são verificados pelo compilador quanto à segurança da memória. Você ainda terá algum grau de segurança dentro de um bloco unsafe.

Além disso, unsafe não significa que o código dentro do bloco é necessariamente perigoso ou que definitivamente terá problemas de segurança de memória: a intenção é que, como programador, você garanta que o código dentro de um bloco unsafe acessará a memória de uma maneira válida.

As pessoas são falíveis e erros acontecerão, mas ao exigir que essas cinco operações unsafe estejam dentro de blocos anotados com unsafe, você saberá que quaisquer erros relacionados à segurança da memória devem estar dentro de um bloco unsafe. Mantenha os blocos unsafe pequenos; você agradecerá mais tarde quando investigar bugs de memória.

Para isolar o código unsafe o máximo possível, é melhor encapsular esse código dentro de uma abstração segura e fornecer uma API segura, o que discutiremos mais tarde no capítulo quando examinarmos funções e métodos unsafe. Partes da biblioteca padrão são implementadas como abstrações seguras sobre código unsafe que foi auditado. Envolver código unsafe em uma abstração segura impede que o uso de unsafe vaze para todos os lugares onde você ou seus usuários podem querer usar a funcionalidade implementada com código unsafe, porque usar uma abstração segura é seguro.

Vamos analisar cada um dos cinco superpoderes unsafe por sua vez. Também veremos algumas abstrações que fornecem uma interface segura para código unsafe.

Desreferenciando um Ponteiro Bruto (Raw Pointer)

Em "Referências Pendentes (Dangling References)", mencionamos que o compilador garante que as referências sejam sempre válidas. O Unsafe Rust tem dois novos tipos chamados ponteiros brutos (raw pointers) que são semelhantes às referências. Assim como com as referências, os ponteiros brutos podem ser imutáveis ou mutáveis e são escritos como *const T e *mut T, respectivamente. O asterisco não é o operador de desreferenciação; faz parte do nome do tipo. No contexto de ponteiros brutos, imutável significa que o ponteiro não pode ser diretamente atribuído após ser desreferenciado.

Diferente das referências e ponteiros inteligentes (smart pointers), os ponteiros brutos:

  • Podem ignorar as regras de empréstimo (borrowing rules) por terem ponteiros imutáveis e mutáveis ou múltiplos ponteiros mutáveis para o mesmo local
  • Não têm garantia de apontar para memória válida
  • Podem ser nulos
  • Não implementam nenhuma limpeza automática

Ao optar por não ter o Rust impor essas garantias, você pode abrir mão da segurança garantida em troca de maior desempenho ou da capacidade de interagir com outra linguagem ou hardware onde as garantias do Rust não se aplicam.

A Listagem 19-1 mostra como criar um ponteiro bruto imutável e um mutável a partir de referências.

let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

Listagem 19-1: Criando ponteiros brutos a partir de referências

Observe que não incluímos a palavra-chave unsafe neste código. Podemos criar ponteiros brutos em código seguro; simplesmente não podemos desreferenciar ponteiros brutos fora de um bloco unsafe, como você verá em breve.

Criamos ponteiros brutos usando as para converter uma referência imutável e uma mutável em seus tipos de ponteiro bruto correspondentes. Como os criamos diretamente de referências garantidas como válidas, sabemos que esses ponteiros brutos específicos são válidos, mas não podemos fazer essa suposição sobre qualquer ponteiro bruto.

Para demonstrar isso, em seguida, criaremos um ponteiro bruto cuja validade não podemos ter tanta certeza. A Listagem 19-2 mostra como criar um ponteiro bruto para um local arbitrário na memória. Tentar usar memória arbitrária é indefinido: pode haver dados naquele endereço ou pode não haver, o compilador pode otimizar o código para que não haja acesso à memória, ou o programa pode terminar com uma falha de segmentação. Normalmente, não há uma boa razão para escrever código como este, mas é possível.

let address = 0x012345usize;
let r = address as *const i32;

Listagem 19-2: Criando um ponteiro bruto para um endereço de memória arbitrário

Lembre-se de que podemos criar ponteiros brutos em código seguro, mas não podemos desreferenciar ponteiros brutos e ler os dados aos quais eles apontam. Na Listagem 19-3, usamos o operador de desreferenciação * em um ponteiro bruto que requer um bloco unsafe.

let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

unsafe {
    println!("r1 is: {}", *r1);
    println!("r2 is: {}", *r2);
}

Listagem 19-3: Desreferenciando ponteiros brutos dentro de um bloco unsafe

Criar um ponteiro não causa nenhum dano; é somente quando tentamos acessar o valor ao qual ele aponta que podemos acabar lidando com um valor inválido.

Observe também que nas Listagens 19-1 e 19-3, criamos ponteiros brutos *const i32 e *mut i32 que apontavam para o mesmo local de memória, onde num é armazenado. Se, em vez disso, tentássemos criar uma referência imutável e uma mutável para num, o código não teria compilado porque as regras de propriedade do Rust não permitem uma referência mutável ao mesmo tempo que quaisquer referências imutáveis. Com ponteiros brutos, podemos criar um ponteiro mutável e um ponteiro imutável para o mesmo local e alterar dados por meio do ponteiro mutável, potencialmente criando uma condição de corrida de dados (data race). Tenha cuidado!

Com todos esses perigos, por que você usaria ponteiros brutos? Um caso de uso importante é ao interagir com código C, como você verá em "Chamando uma Função ou Método Unsafe". Outro caso é ao construir abstrações seguras que o verificador de empréstimos não entende. Apresentaremos funções unsafe e, em seguida, analisaremos um exemplo de uma abstração segura que usa código unsafe.

Chamando uma Função ou Método Unsafe

O segundo tipo de operação que você pode realizar em um bloco unsafe é chamar funções unsafe. Funções e métodos unsafe se parecem exatamente com funções e métodos regulares, mas eles têm um unsafe extra antes do restante da definição. A palavra-chave unsafe neste contexto indica que a função tem requisitos que precisamos cumprir quando chamamos essa função, porque o Rust não pode garantir que tenhamos atendido a esses requisitos. Ao chamar uma função unsafe dentro de um bloco unsafe, estamos dizendo que lemos a documentação desta função e assumimos a responsabilidade de cumprir os contratos da função.

Aqui está uma função unsafe chamada dangerous que não faz nada em seu corpo:

unsafe fn dangerous() {}

unsafe {
    dangerous();
}

Devemos chamar a função dangerous dentro de um bloco unsafe separado. Se tentarmos chamar dangerous sem o bloco unsafe, obteremos um erro:

error[E0133]: call to unsafe function is unsafe and requires
unsafe function or block
 --> src/main.rs:4:5
  |
4 |     dangerous();
  |     ^^^^^^^^^^^ call to unsafe function
  |
  = note: consult the function's documentation for information on
how to avoid undefined behavior

Com o bloco unsafe, estamos afirmando ao Rust que lemos a documentação da função, entendemos como usá-la corretamente e verificamos se estamos cumprindo o contrato da função.

Os corpos das funções unsafe são efetivamente blocos unsafe, então, para realizar outras operações unsafe dentro de uma função unsafe, não precisamos adicionar outro bloco unsafe.

Criando uma Abstração Segura sobre Código Unsafe

Só porque uma função contém código unsafe não significa que precisamos marcar a função inteira como unsafe. Na verdade, encapsular código unsafe em uma função segura é uma abstração comum. Como exemplo, vamos estudar a função split_at_mut da biblioteca padrão, que requer algum código unsafe. Vamos explorar como podemos implementá-la. Este método seguro é definido em fatias mutáveis: ele pega uma fatia e a transforma em duas, dividindo a fatia no índice fornecido como argumento. A Listagem 19-4 mostra como usar split_at_mut.

let mut v = vec![1, 2, 3, 4, 5, 6];

let r = &mut v[..];

let (a, b) = r.split_at_mut(3);

assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);

Listagem 19-4: Usando a função segura split_at_mut

Não podemos implementar esta função usando apenas Rust seguro. Uma tentativa pode se parecer com a Listagem 19-5, que não compilará. Para simplificar, implementaremos split_at_mut como uma função em vez de um método e apenas para fatias de valores i32, em vez de para um tipo genérico T.

fn split_at_mut(
    values: &mut [i32],
    mid: usize,
) -> (&mut [i32], &mut [i32]) {
    let len = values.len();

    assert!(mid <= len);

    (&mut values[..mid], &mut values[mid..])
}

Listagem 19-5: Uma tentativa de implementação de split_at_mut usando apenas Rust seguro

Esta função primeiro obtém o comprimento total da fatia. Em seguida, ela afirma que o índice fornecido como um parâmetro está dentro da fatia, verificando se ele é menor ou igual ao comprimento. A afirmação significa que, se passarmos um índice que é maior que o comprimento para dividir a fatia, a função entrará em pânico antes de tentar usar esse índice.

Então, retornamos duas fatias mutáveis em uma tupla: uma do início da fatia original até o índice mid e outra de mid até o final da fatia.

Quando tentamos compilar o código na Listagem 19-5, obteremos um erro:

error[E0499]: cannot borrow `*values` as mutable more than once at a time
 --> src/main.rs:9:31
  |
2 |     values: &mut [i32],
  |             - let's call the lifetime of this reference `'1`
...
9 |     (&mut values[..mid], &mut values[mid..])
  |     --------------------------^^^^^^--------
  |     |     |                   |
  |     |     |                   second mutable borrow occurs here
  |     |     first mutable borrow occurs here
  |     returning this value requires that `*values` is borrowed for `'1`

O verificador de empréstimos (borrow checker) do Rust não consegue entender que estamos emprestando diferentes partes da fatia; ele só sabe que estamos emprestando da mesma fatia duas vezes. Emprestar diferentes partes de uma fatia é fundamentalmente aceitável porque as duas fatias não se sobrepõem, mas o Rust não é inteligente o suficiente para saber disso. Quando sabemos que o código está correto, mas o Rust não sabe, é hora de recorrer ao código unsafe.

A Listagem 19-6 mostra como usar um bloco unsafe, um ponteiro bruto e algumas chamadas para funções unsafe para fazer a implementação de split_at_mut funcionar.

use std::slice;

fn split_at_mut(
    values: &mut [i32],
    mid: usize,
) -> (&mut [i32], &mut [i32]) {
  1 let len = values.len();
  2 let ptr = values.as_mut_ptr();

  3 assert!(mid <= len);

  4 unsafe {
        (
          5 slice::from_raw_parts_mut(ptr, mid),
          6 slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

Listagem 19-6: Usando código unsafe na implementação da função split_at_mut

Lembre-se de "O Tipo Slice" que uma fatia é um ponteiro para alguns dados e o comprimento da fatia. Usamos o método len para obter o comprimento de uma fatia [1] e o método as_mut_ptr para acessar o ponteiro bruto de uma fatia [2]. Neste caso, como temos uma fatia mutável para valores i32, as_mut_ptr retorna um ponteiro bruto com o tipo *mut i32, que armazenamos na variável ptr.

Mantemos a afirmação de que o índice mid está dentro da fatia [3]. Então, chegamos ao código unsafe [4]: a função slice::from_raw_parts_mut recebe um ponteiro bruto e um comprimento, e cria uma fatia. Usamos isso para criar uma fatia que começa de ptr e tem mid itens de comprimento [5]. Em seguida, chamamos o método add em ptr com mid como um argumento para obter um ponteiro bruto que começa em mid, e criamos uma fatia usando esse ponteiro e o número restante de itens após mid como o comprimento [6].

A função slice::from_raw_parts_mut é unsafe porque recebe um ponteiro bruto e deve confiar que este ponteiro é válido. O método add em ponteiros brutos também é unsafe porque deve confiar que o local de deslocamento também é um ponteiro válido. Portanto, tivemos que colocar um bloco unsafe em torno de nossas chamadas para slice::from_raw_parts_mut e add para que pudéssemos chamá-los. Ao olhar para o código e adicionar a afirmação de que mid deve ser menor ou igual a len, podemos dizer que todos os ponteiros brutos usados dentro do bloco unsafe serão ponteiros válidos para dados dentro da fatia. Este é um uso aceitável e apropriado de unsafe.

Observe que não precisamos marcar a função split_at_mut resultante como unsafe, e podemos chamar esta função do Rust seguro. Criamos uma abstração segura para o código unsafe com uma implementação da função que usa código unsafe de uma maneira segura, porque ela cria apenas ponteiros válidos a partir dos dados que esta função tem acesso.

Em contraste, o uso de slice::from_raw_parts_mut na Listagem 19-7 provavelmente travará quando a fatia for usada. Este código pega um local de memória arbitrário e cria uma fatia com 10.000 itens de comprimento.

use std::slice;

let address = 0x01234usize;
let r = address as *mut i32;

let values: &[i32] = unsafe {
    slice::from_raw_parts_mut(r, 10000)
};

Listagem 19-7: Criando uma fatia a partir de um local de memória arbitrário

Não somos proprietários da memória neste local arbitrário, e não há garantia de que a fatia que este código cria contenha valores i32 válidos. Tentar usar values como se fosse uma fatia válida resulta em comportamento indefinido.

Usando Funções extern para Chamar Código Externo

Às vezes, seu código Rust pode precisar interagir com código escrito em outra linguagem. Para isso, o Rust tem a palavra-chave extern que facilita a criação e o uso de uma Interface de Função Estrangeira (FFI), que é uma maneira de uma linguagem de programação definir funções e permitir que uma linguagem de programação diferente (estrangeira) chame essas funções.

A Listagem 19-8 demonstra como configurar uma integração com a função abs da biblioteca padrão C. Funções declaradas dentro de blocos extern são sempre unsafe para serem chamadas a partir do código Rust. A razão é que outras linguagens não impõem as regras e garantias do Rust, e o Rust não pode verificá-las, então a responsabilidade recai sobre o programador para garantir a segurança.

Nome do arquivo: src/main.rs

extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!(
            "Valor absoluto de -3 de acordo com C: {}",
            abs(-3)
        );
    }
}

Listagem 19-8: Declarando e chamando uma função extern definida em outra linguagem

Dentro do bloco extern "C", listamos os nomes e assinaturas de funções externas de outra linguagem que queremos chamar. A parte "C" define qual Interface Binária de Aplicação (ABI) a função externa usa: a ABI define como chamar a função no nível da assembly. A ABI "C" é a mais comum e segue a ABI da linguagem de programação C.

Chamando Funções Rust de Outras Linguagens

Também podemos usar extern para criar uma interface que permite que outras linguagens chamem funções Rust. Em vez de criar um bloco extern inteiro, adicionamos a palavra-chave extern e especificamos a ABI a ser usada logo antes da palavra-chave fn para a função relevante. Também precisamos adicionar uma anotação #[no_mangle] para dizer ao compilador Rust para não "mutilar" o nome desta função. Mutilar (Mangling) é quando um compilador altera o nome que demos a uma função para um nome diferente que contém mais informações para outras partes do processo de compilação consumirem, mas é menos legível para humanos. Cada compilador de linguagem de programação mutila os nomes de forma ligeiramente diferente, então, para que uma função Rust possa ser nomeada por outras linguagens, devemos desabilitar a mutilação de nomes do compilador Rust.

No exemplo a seguir, tornamos a função call_from_c acessível a partir do código C, depois que ela é compilada para uma biblioteca compartilhada e vinculada a partir de C:

#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Acabei de chamar uma função Rust de C!");
}

Este uso de extern não requer unsafe.

Acessando ou Modificando uma Variável Estática Mutável

Neste livro, ainda não falamos sobre variáveis globais, que o Rust suporta, mas podem ser problemáticas com as regras de propriedade do Rust. Se duas threads estiverem acessando a mesma variável global mutável, isso pode causar uma condição de corrida de dados (data race).

No Rust, as variáveis globais são chamadas de variáveis estáticas. A Listagem 19-9 mostra um exemplo de declaração e uso de uma variável estática com uma fatia de string como valor.

Nome do arquivo: src/main.rs

static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("value is: {HELLO_WORLD}");
}

Listagem 19-9: Definindo e usando uma variável estática imutável

Variáveis estáticas são semelhantes a constantes, que discutimos em "Constantes". Os nomes das variáveis estáticas estão em SCREAMING_SNAKE_CASE por convenção. Variáveis estáticas só podem armazenar referências com o tempo de vida 'static, o que significa que o compilador Rust pode descobrir o tempo de vida e não somos obrigados a anotá-lo explicitamente. Acessar uma variável estática imutável é seguro.

Uma diferença sutil entre constantes e variáveis estáticas imutáveis é que os valores em uma variável estática têm um endereço fixo na memória. Usar o valor sempre acessará os mesmos dados. As constantes, por outro lado, podem duplicar seus dados sempre que são usadas. Outra diferença é que as variáveis estáticas podem ser mutáveis. Acessar e modificar variáveis estáticas mutáveis é unsafe. A Listagem 19-10 mostra como declarar, acessar e modificar uma variável estática mutável chamada COUNTER.

Nome do arquivo: src/main.rs

static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_count(3);

    unsafe {
        println!("COUNTER: {COUNTER}");
    }
}

Listagem 19-10: Ler ou escrever em uma variável estática mutável é unsafe.

Assim como com variáveis regulares, especificamos a mutabilidade usando a palavra-chave mut. Qualquer código que lê ou escreve de COUNTER deve estar dentro de um bloco unsafe. Este código compila e imprime COUNTER: 3 como esperaríamos porque é single threaded. Ter várias threads acessando COUNTER provavelmente resultaria em condições de corrida de dados.

Com dados mutáveis que são globalmente acessíveis, é difícil garantir que não haja condições de corrida de dados, e é por isso que o Rust considera variáveis estáticas mutáveis como unsafe. Sempre que possível, é preferível usar as técnicas de concorrência e ponteiros inteligentes thread-safe que discutimos no Capítulo 16 para que o compilador verifique se o acesso aos dados de diferentes threads é feito com segurança.

Implementando uma Trait Unsafe

Podemos usar unsafe para implementar uma trait unsafe. Uma trait é unsafe quando pelo menos um de seus métodos tem algum invariante que o compilador não pode verificar. Declaramos que uma trait é unsafe adicionando a palavra-chave unsafe antes de trait e marcando a implementação da trait como unsafe também, conforme mostrado na Listagem 19-11.

unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implementations go here
}

Listagem 19-11: Definindo e implementando uma trait unsafe

Ao usar unsafe impl, estamos prometendo que manteremos os invariantes que o compilador não pode verificar.

Como exemplo, lembre-se das traits marcadoras Send e Sync que discutimos em "Concorrência Extensível com as Traits Send e Sync": o compilador implementa essas traits automaticamente se nossos tipos forem compostos inteiramente de tipos Send e Sync. Se implementarmos um tipo que contém um tipo que não é Send ou Sync, como ponteiros brutos, e quisermos marcar esse tipo como Send ou Sync, devemos usar unsafe. O Rust não pode verificar se nosso tipo mantém as garantias de que ele pode ser enviado com segurança entre threads ou acessado de várias threads; portanto, precisamos fazer essas verificações manualmente e indicar isso com unsafe.

Acessando Campos de uma Union

A ação final que funciona apenas com unsafe é acessar campos de uma union. Uma union é semelhante a uma struct, mas apenas um campo declarado é usado em uma instância específica de cada vez. Unions são usados principalmente para interagir com unions em código C. Acessar campos de union é unsafe porque o Rust não pode garantir o tipo de dados atualmente armazenados na instância da union. Você pode aprender mais sobre unions na Referência do Rust em *https://doc.rust-lang.org/reference/items/unions.html\*\*.

Quando Usar Código Unsafe

Usar unsafe para usar uma das cinco superpotências que acabamos de discutir não é errado ou mesmo mal visto, mas é mais complicado obter código unsafe correto porque o compilador não pode ajudar a manter a segurança da memória. Quando você tem um motivo para usar código unsafe, você pode fazê-lo, e ter a anotação unsafe explícita torna mais fácil rastrear a origem dos problemas quando eles ocorrem.

Resumo

Parabéns! Você completou o laboratório de Unsafe Rust. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.