Na última década, Rust emergiu como a linguagem preferida para pessoas que desejam escrever software rápido e nativo de máquina, que também tenha fortes garantias de segurança de memória.
Outras linguagens, como C, podem ser executadas de forma rápida e próxima do metal, mas carecem dos recursos de linguagem para garantir que a memória do programa seja alocada e descartada adequadamente. Conforme observado recentemente pelo Gabinete do Diretor Nacional Cibernético da Casa Branca, essas deficiências permitem inseguranças e explorações de software com consequências dispendiosas no mundo real. Linguagens como Rust, que colocam a segurança da memória em primeiro lugar, estão recebendo mais atenção.
Como o Rust garante a segurança da memória de uma forma que outras linguagens não fazem? Vamos descobrir.
Segurança da memória Rust: um recurso de idioma nativo
A primeira coisa a entender sobre os recursos de segurança de memória do Rust é que eles não são fornecidos por meio de uma biblioteca ou ferramentas de análise externas, sendo que qualquer uma delas seria opcional. Os recursos de segurança de memória do Rust estão integrados na linguagem. Eles não são apenas obrigatórios, mas também aplicados antes da execução do código.
No Rust, comportamentos que não são seguros para a memória não são tratados como tempo de execução erros, mas como compilador erros. Classes inteiras de problemas, como erros de uso após liberação, são sintaticamente errado em Rust. Esse código inválido nunca é compilado e nunca chega à produção. Em muitas outras linguagens, incluindo C ou C++, erros de segurança de memória muitas vezes só são descobertos em tempo de execução.
Isso não significa que o código escrito em Rust seja totalmente à prova de balas ou infalível. Alguns problemas de tempo de execução, como condições de corrida, ainda são de responsabilidade do desenvolvedor. Mas Rust elimina muitas oportunidades comuns de exploração de software.
Linguagens gerenciadas por memória, como C#, Java ou Python, dispensam quase inteiramente o desenvolvedor de fazer qualquer gerenciamento manual de memória. Os desenvolvedores podem se concentrar em escrever código e realizar trabalhos. Mas essa conveniência tem outro custo, normalmente a velocidade ou a necessidade de um tempo de execução maior. Os binários Rust podem ser altamente compactos, rodar na velocidade nativa da máquina por padrão e permanecer seguros para a memória.
Variáveis de ferrugem: imutáveis por padrão
Uma das primeiras coisas que os desenvolvedores novatos do Rust aprendem é que todas as variáveis são imutável por padrão– o que significa que eles não podem ser reatribuídos ou modificados. Eles devem ser especificamente declarados como mutáveis para serem alterados.
Isso pode parecer trivial, mas tem o efeito de forçar o desenvolvedor a estar totalmente consciente de quais valores precisam ser mutáveis em um programa e quando. O código resultante é mais fácil de raciocinar porque informa o que pode mudar e onde.
Imutável por padrão é distinto do conceito de constante. Uma variável imutável pode ser calculada e armazenada como imutável em tempo de execução – ou seja, pode ser calculada, armazenada e depois não alterada. Uma constante, entretanto, deve ser computável em compilar tempo, antes que o programa seja executado. Muitos tipos de valores – entrada do usuário, por exemplo – não podem ser armazenados como constantes dessa maneira.
C++ assume o oposto de Rust: por padrão, tudo é mutável. Você deve usar o const
palavra-chave para declarar coisas imutáveis. Você poderia adote um estilo de codificação C++ de uso const
por padrão, mas isso cobriria apenas o código você escrever. A ferrugem garante todos programas escritos na linguagem, agora e no futuro, assumem imutabilidade por padrão.
Propriedade, empréstimo e referências em Rust
Cada valor em Rust tem um “proprietário”, o que significa que apenas uma coisa por vez, em qualquer ponto do código, pode ter controle total de leitura/gravação sobre um valor. A propriedade pode ser cedida ou “emprestada” temporariamente, mas esse comportamento é estritamente rastreado pelo compilador do Rust. Qualquer código que viole as regras de propriedade de um determinado objeto simplesmente não é compilado.
Compare essa abordagem com o que vemos em outras línguas. Em C não há propriedade: qualquer coisa pode ser acessada por qualquer outra coisa a qualquer momento. Toda a responsabilidade sobre como as coisas são modificadas é do programador. Em linguagens gerenciadas como Python, Java ou C#, as regras de propriedade não existem, mas apenas porque não são necessárias. O acesso a objetos e, portanto, a segurança da memória, é controlado pelo tempo de execução. Novamente, isso tem o custo da velocidade ou do tamanho e da presença de um tempo de execução.
Vidas em ferrugem
Referências a valores em Rust não têm apenas proprietários, mas vidas—significando um escopo para o qual uma determinada referência é válida. Na maioria dos códigos Rust, os tempos de vida podem ser deixados implícitos, pois o compilador os rastreia. Mas os tempos de vida também podem ser anotados explicitamente para casos de uso mais complexos. Independentemente disso, tentar acessar ou modificar algo fora de seu tempo de vida ou depois de “sair do escopo” resulta em um erro do compilador. Mais uma vez, isso evita que classes inteiras de bugs perigosos cheguem à produção com código Rust.
Erros de uso após liberação ou “ponteiros pendentes” surgem quando você tenta acessar algo que, em teoria, foi desalocado ou saiu do escopo. Eles são deprimentemente comuns em C e C++. C não tem aplicação oficial em tempo de compilação para a vida útil dos objetos. C++ possui conceitos como “ponteiros inteligentes” para evitar isso, mas eles não são implementados por padrão; você deve optar por usá-los. A segurança da linguagem torna-se uma questão de estilo de codificação individual ou de um requisito institucional, e não algo que a linguagem garante completamente.
Com linguagens gerenciadas como Java, C# ou Python, o gerenciamento de memória é de responsabilidade do tempo de execução da linguagem. Isso tem o custo de exigir um tempo de execução considerável e, às vezes, reduz a velocidade de execução. Rust impõe regras vitalícias antes que o código seja executado.
A segurança da memória do Rust tem custos
A segurança da memória do Rust também tem custos. A primeira e maior é a necessidade de aprender e usar a própria língua.
Mudar para uma nova linguagem nunca é fácil, e uma das críticas comuns ao Rust é sua curva de aprendizado inicial, mesmo para programadores experientes. É preciso tempo e trabalho para compreender o modelo de gerenciamento de memória do Rust. A curva de aprendizado do Rust é um ponto constante de discussão mesmo entre os adeptos da linguagem.
C, C++ e todo o resto têm uma base de usuários grande e consolidada, o que é um argumento frequente a seu favor. Eles também possuem bastante código existente que pode ser aproveitado, incluindo bibliotecas e aplicativos completos. Não é difícil entender por que os desenvolvedores optam por usar linguagens C: existem muitas ferramentas e outros recursos ao seu redor.
Dito isso, na década de existência do Rust, ele ganhou ferramentas, documentação e uma comunidade de usuários que facilitam a atualização. E a coleção de “caixas” de terceiros, ou bibliotecas Rust, já é extensa e cresce diariamente. O uso do Rust pode exigir um período de reciclagem e reequipamento, mas os usuários raramente carecem de recursos ou suporte de biblioteca para uma determinada tarefa.
Aplicando as lições de Rust a outras linguagens
O crescimento do Rust estimulou conversas sobre a transformação de linguagens existentes que carecem de segurança de memória para adotar recursos de proteção de memória semelhantes aos do Rust.
Existem algumas ideias ambiciosas, mas, na melhor das hipóteses, são difíceis de implementar. Por um lado, eles quase certamente custariam a compatibilidade com versões anteriores. Os comportamentos do Rust são difíceis de introduzir em linguagens onde não estão em uso, sem forçar uma divisão rígida entre o código legado existente e o novo código com novos comportamentos.
Nada disso impediu as pessoas de tentar. Vários projetos tentaram criar extensões para C ou C++ com regras sobre segurança e propriedade de memória. Os projetos Carbon e Cppfront exploram ideias nesse sentido. Carbon é uma linguagem totalmente nova com ferramentas de migração para código C++ existente, e Cppfront propõe uma sintaxe alternativa ao C++ como forma de escrevê-lo de forma mais segura e conveniente. Mas ambos os projetos permanecem prototípicos; Cppfront lançou sua primeira versão completa apenas em março de 2024.
O que dá ao Rust seu lugar distinto no mundo da programação é que seus recursos mais poderosos e notáveis – segurança da memória e os comportamentos de tempo de compilação que a garantem – são indivisivelmente parte da linguagem; eles foram incorporados e não adicionados após o fato. O acesso a esses recursos pode exigir mais do desenvolvedor inicialmente, mas os dividendos compensam mais tarde.