Rust Evangelism

C

Pedro Mendes

C

O que é Rust?

Rust é uma systems programming language, ou seja, permite ter um controlo de baixo nível do hardware e recursos da máquina.
As duas linguagens mais conhecidas neste campo são C e C++.

Porquê fazer uma linguagem nova?

C, C++ são linguagens que dão muito poder e controlo a quem as usa, deixam controlar todos os detalhes. Mas...
With great power, comes great responsibility

– Spider Man probably

Qual é o principal dificuldade de trabalhar com estas linguagens? Gestão de memória.
 Segmentation fault (core dumped) 

Ownership

Ownership é um dos mental models que podemos utilizar para conseguir evitar um conjunto de bugs como "double free" ou "use after free".
Podemos ver ownership como uma forma de responder à pergunta: "Quem é responsável por libertar esta memória?"
Começando por fazer um array dinâmico simples em C

typedef struct {
    int* values;
    size_t capacity;
    size_t used;
} IntVec;
            
  • values: Tem um pointer para os valores que o array guarda.
  • used: É quantos elementos tem.
  • capacity: Quanto espaço tem o array de values
              
typedef struct {
    int* values;
    size_t capacity;
    size_t used;
} IntVec;

IntVec int_vec_make(size_t initial_cap) {
  return (IntVec) {.capacity = initial_cap, .used = 0,
                   .values = malloc(sizeof(int) * initial_cap)};
}

void int_vec_drop(IntVec a) { free(a.values); }

int main(void) {
    // a owns the memory
    IntVec a = int_vec_make(5);
    // a's memory is dropped/freed
    int_array_drop(a);
}
            
              
IntVec int_vec_make(size_t initial_cap) {
  return (IntVec) {.capacity = initial_cap, .used = 0,
                     .values = malloc(sizeof(int) * initial_cap)};
}

void int_vec_drop(IntVec a) { free(a.values); }

int main(void) {
    // a owns the memory
    IntVec a = int_vec_make(5);
    // oh no, now both a and b own the memory
    IntVec b = a;
    // a's memory is dropped/freed
    int_vec_drop(a);
    // use after free because there were two "owners"
    printf("%d\n", b.values[1]);
}
            
              
IntVec int_vec_make(size_t initial_cap) {
  return (IntVec) {.capacity = initial_cap, .used = 0,
                     .values = malloc(sizeof(int) * initial_cap)};
}

void int_vec_drop(IntVec a) { free(a.values); }

int main(void) {
    // a owns the memory
    IntVec a = int_vec_make(5);
    // oh no, now both a and b own the memory
    IntVec b = a;
    // a's memory is dropped/freed
    int_vec_drop(a);
    // use after free because there were two "owners"
    printf("%d\n", b.values[1]); // Undefined behaviour
}
            
Vamos partir do mesmo ponto.
Um array dinâmico, em Rust chama-se vector e já está implementado.
              
Vec<i32>
            
Este tipo é equivalente ao IntVec de C
              
                fn main() {
                  // a owns the memory
                  let a: Vec<i32> = Vec::with_capacity(5);
                  // a's memory is dropped
                  drop(a);
                }
              
            
Agora vamos tentar criar o mesmo erro.
              
                fn main() {
                  // a owns the memory
                  let a: Vec<i32> = Vec::with_capacity(5);
                  // Move a --to--> b
                  let b = a;
                  // a's memory is dropped
                  drop(a);
                  // try to use b
                  println!("{}", b[1]);
                }
              
            
              
                fn main() {
                  // a owns the memory
                  let a: Vec<i32> = Vec::with_capacity(5);
                  // Move a --to--> b
                  let b = a;
                  // a's memory is dropped
                  drop(a);
                  // try to use b
                  println!("{}", b[1]);
                }
              
              
              
                fn main() {
                  // a owns the memory
                  let a: Vec<i32> = Vec::with_capacity(5);
                  // Move a --to--> b
                  let b = a;
                  // a doesn't own the memory anymore and is 'uninitialized'
                  drop(a);
                  // try to use b
                  println!("{}", b[1]);
                }
              
            

Como é que Rust previne estes bugs ?

Double free

  • Para cada bloco de memória, existe apenas um dono.
  • Apenas o dono de um bloco memória pode libertar o mesmo.

Use after free

Sempre que a ownership de um bloco de memória é passada de uma variável para outra, a antiga fica num estado inutilizável, e o compilador garante que não será mais acedida.

Lifetimes

Isto é um pouco restritivo
E se quisermos partilhar um objecto por varias partes do codigo?

Pointers?

Mas assim introduzimos "use after free" bugs outra vez...

Lifetime: Região do programa em que um dado objecto existe.


Portanto isto é """simples""" um pointer/referencia nunca pode existir mais "tempo" do que o objecto para que aponta.
Podemos pensar que as {} delimitam o lifetime de um objecto
              
              
              
              
              
              
              
              
              
              
              
              

Undefined Behaviour

Algumas "construções" são consideradas UB pelo standard de C e o compilador tem a liberdade fazer o que quiser com o programa assim que encontra UB.

Por exemplo, em gcc version 10.2.0 compilar o programa anterior, com -O2, resulta no seguinte assembly para a função foo.

            
            xor %eax,%eax
            retq
              
              
              
              
            
              
              
              
            
              
              <foo>:
                xor %eax,%eax
                retq
              
            
              
              
              
            
              
              <foo>:
                xor %eax,%eax
                retq
              <main>:
                mov 0x0,%eax
                ud2
              
            
? ud2 ?
              
              
              
            
              
              
              
            

Borrowing

Em Rust um pointer ou referência é normalmente chamado de um borrow.


Tal como temos variáveis que são "donas" (owners) de coisas estas coisas também podem ser "emprestadas" (borrowed).

Há dois tipos de borrows em Rust:

  • shared (ou immutable)
  • exclusive (ou mutable)


E há duas regras para estes:

Em qualquer ponto do programa...

  • ... podem existir qualquer número de shared borrows para um determinado objecto e nenhum exclusive borrow.
  • ... se existe um exclusive borrow para um objecto, não pode haver mais nenhum borrow de qualquer tipo.
Vamos continuar a utilizar o array dinâmico da secção anterior.

typedef struct {
    int* values;
    size_t capacity;
    size_t used;
} IntVec;
            
            
typedef struct {
    int* values;
    size_t capacity;
    size_t used;
} IntVec;

void int_vec_push(IntVec* self, int value) {
  // Caso o vector esteja cheio
  if(self->capacity == self->used) {
    // Duplicar a capacidade do vector
    self->capacity = self->capacity ? self->capacity * 2 : 1;
    // Realocar o array, usando agora o dobro da capacidade
    self->values = realloc(self->values, sizeof(int) * self->capacity);
  }
  // Adicionar ao vector
  self->values[self->used++] = value;
}
            

A linha mais importante deste código e a 13. Nesta linha podemos ver que o pointer self->values muda!

            
typedef struct { int* values; size_t capacity, used; } IntVec;

void int_vec_push(IntVec* self, int value);

int main(void) {
  IntVec v = int_vec_make(0);

  // Fazemos push de um valor
  int_vec_push(&v, 1); // isto realoca porque capacity(0) == used(0)

  // Criamos um pointer para o primeiro valor do vector
  int* first_value_ptr = &v.values[0];

  // Fazemos push de mais um valor
  int_vec_push(&v, 2); // capacity(1) == used(1) então realocamos

  // Tentamos alterar o primeiro valor do vector
  *first_value_ptr = 42;
}
            
            
typedef struct { int* values; size_t capacity, used; } IntVec;

void int_vec_push(IntVec* self, int value);

int main(void) {
  IntVec v = int_vec_make(0);

  // Fazemos push de um valor
  int_vec_push(&v, 1); // isto realoca porque capacity(0) == used(0)

  // Criamos um pointer para o primeiro valor do vector
  int* first_value_ptr = &v.values[0];

  // Fazemos push de mais um valor
  int_vec_push(&v, 2); // capacity(1) == used(1) então realocamos

  // Tentamos alterar o primeiro valor do vector
  *first_value_ptr = 42; // Undefined behaviour
}
            
            
            fn main() {
              let mut v = Vec::new();

              // Fazemos push de um valor
              v.push(1);

              // Criamos um pointer para o primeiro valor do vector
              let first_value_ptr = &mut v[0];

              // Fazemos push de mais um valor
              v.push(2);

              // Tentamos alterar o primeiro valor do vector
              *first_value_ptr = 42;
            }
            
              
              fn main() {
                let mut v = Vec::new();

                // Fazemos push de um valor
                v.push(1);

                // Criamos um pointer para o primeiro valor do vector
                let first_value_ptr = &mut v[0];

                // Fazemos push de mais um valor
                v.push(2);

                // Tentamos alterar o primeiro valor do vector
                *first_value_ptr = 42;
              }
              
              
                  
                  fn main() {
                    let mut v = Vec::new();
                    // Fazemos push de um valor
                    v.push(1);
                    // Criamos um pointer para o primeiro valor do vector
                    let first_value_ptr = &mut v[0];
                    // Fazemos push de mais um valor
                    v.push(2);
                    // Tentamos alterar o primeiro valor do vector
                    *first_value_ptr = 42;
                  }
                  
                
                  
                  fn main() {
                    let mut v = Vec::new();
                    // Fazemos push de um valor
                    Vec::push(&mut v, 1); // equivalente a v.push(1);
                    // Criamos um pointer para o primeiro valor do vector
                    let first_value_ptr = &mut v[0];
                    // Fazemos push de mais um valor
                    Vec::push(&mut v, 2); // equivalente a v.push(2);
                    // Tentamos alterar o primeiro valor do vector
                    *first_value_ptr = 42;
                  }
                  
                

E muito mais...

Zero cost abstractions

Iterators

Safe concurrency

Zero Sized Types

Traits

Tagged unions/Sum types/Enums

Pattern Matching

Unsafe

E muito mais...

Zero cost abstractions

Iterators

Safe concurrency

Zero Sized Types

Traits

Tagged unions/Sum types/Enums

Pattern Matching

Unsafe

The Rust Book