Há uns anos atrás, em 2017, quando eu cheguei ao meu ápice na linguagem Java e estava escrevendo meu próprio compilador e prototipando minha própria linguagem para a JVM, eu comecei a despertar novamente um desejo de aprender uma nova linguagem de programação, mas que não rodasse em uma VM, como Java ou C#, e que não fosse dinâmica nem interpretada, como Javascript ou Python, eu queria algo mais próximo de C/C++, com performance sendo o principal forte, foi ai que dei de cara com, não só duas, mas três linguagens: Go, Rust e Nim.
Go
A primeira vista, Go parecia bastante interessante, com duck-typing, que era algo que eu curtia bastante (e ainda curto), uma ótima performance, gerenciamento automático de memória com GC (por mais que eu queria me afastar do GC), closures e as incríveis Goroutines, no entanto, minha animação acabou na ausência dos genéricos, inclusive acompanhei por bastante tempo a discussão sobre genéricos em Go, que viriam provavelmente na versão 2, mas não era o que eu esperava para aquele momento, nem mesmo para os dias de hoje.
A ausência dos genéricos é um grande passo para trás na programação. Implementar sua própria estrutura de dados sem genéricos implica necessariamente em utilizar tipagem dinâmica, totalmente contra intuitivo em uma linguagem de tipagem majoritariamente estática, como o Go, inclusive, as próprias listas em Go utilizam tipagem dinâmica, apenas o map é implementado com uma espécie de polimorfismo paramétrico, o que eu também acho totalmente contra intuitivo, um único tipo na linguagem ser capaz de usar polimorfismo paramétrico, mas nenhum outro poder utilizar também.
Nim
Uma linguagem interessante que eu gostaria de ter descoberto antes, bastante poderosa e também com gerenciamento automático de memória com GC, era uma ótima candidata, mas a falta de suporte para a IDE que utilizo no dia-a-dia, uma sintaxe que não me agradou tanto assim, e o fato de ter GC, reforçou minha desconsideração em usar o Nim, e deixar apenas como uma opção para o futuro.
Rust
Depois de ter conhecido Go e programado durante um bom tempo, conheci o Rust, quando eu vi a linguagem pela primeira vez eu fiquei extremamente atraído pela ideia de gerenciamento automático de memória, em tempo de compilação, era exatamente o que eu procurava: alta performance, compilando para um binário executável e estaticamente linkado, sem Garbage Collection e sem Runtime, e permitindo a utilização para implementação de sistemas operacionais, com grande segurança contra memory leaks e bugs podendo ser encontrados precocemente.
A minha escolha por uma linguagem sem GC, sem Runtime, compilada para um binário "nativo" (explico as aspas no nativo no final do artigo) foi justamente por uma vontade de programar em C/C++ desde pequeno. Eu programo desde os 9/10 anos, apesar da idade ser difícil de lembrar com exatidão, o que eu mais lembro é que programei praticamente minha vida toda (risos), publiquei meu primeiro projeto em Java em 2014 (aos meus 13 anos), e já havia programado em C/C++, Delphi, Visual Basic (VB.NET), C#, Lua, PHP, Javascript e Pawn antes de chegar no Java, então já carrego certa experiência em outras linguagens, mas queria me aprofundar num "tipo" de linguagem que sempre despertou meu interesse.
Inclusive, C/C++ foi minha segunda linguagem de programação, a primeira foi Lua, programando usando a API do ambiente não tão conhecido chamado Autoplay Media Studio, usando dele para um propósito totalmente diferente do que era a plataforma, eu programava sistemas em Lua usando o ambiente do Autoplay (algoritmos de criptografia, sistemas de chat, controle de computadores em LAN Houses), enquanto o mesmo foi feito para a criação de menus e multimídia.
A minha relação com C/C++ foi muito curta, devido a curva de aprendizado, principalmente do C++, em um momento onde eu estava me acostumando com linguagens de alto nível, e mesmo eu tendo programado muito em Pawn, e Pawn sendo parecido com C, ela é uma linguagem sem tipos, sem alocação de memória na Heap, sem gerenciamento manual de memória, que roda em uma Runtime/VM, focada em callbacks, destoando bastante de C, apesar de ser C-like, então não consegui aproveitar muito desse conhecimento.
Mas mesmo com a dificuldade, sempre tentei voltar a essas linguagens e aprende-las, mas sempre me debatia com gerenciamento de dependências, linkagem, headers, construtores, destrutores, Heap, stack, gerenciamento de memória, funções com nomes totalmente contra intuitivos (o que é cout cara?), eu me via brigando com a linguagem em conceitos básicos, enquanto linguagens como Delphi, entregavam muito mais para o Desktop com muito menos código, e o Java permitia uma facilidade extrema em incluir bibliotecas, e eu nem usava Maven na época, eu configurava o Classpath por meio do Eclipse.
Rust apresentou uma solução que resolvia todas as dores que eu tive em C/C++, Cargo para dependências e configuração da compilação, trait Drop para destrutor (isso quando precisar fazer), gerenciamento de memória automático em tempo de compilação, macros e funções com nomes intuitivos, construtores parecidos com outras linguagens, entre outros recursos.
Claro que, quando eu fui tentar novamente mergulhar nessas linguagens mais "nativas" e "baixo nível", eu já conhecia todo o conceito de Heap e Stack, já entendia toda estrutura de execução da máquina virtual Java e a mecânica e engenharia do Bytecode, já tinha uma ideia muito mais bem formada de como uma máquina virtual operava, que é bem próximo do que uma máquina física opera, apesar de muitas diferenças, e isso me deu muitas vantagens, mas ainda assim me bati muito com borrowing, ownership, RAII, macros, closures, Box, Rc, Arc, alocações, reference cycles, move, traits, e por ai vai (iremos falar sobre tudo isso nessa série), mas todas essas batalhas foram prazerosas, pois pude extrair um conhecimento único e novo.
Agora chega de historinha e vamos conhecer o Rust.
O que é Rust
Não é aquele jogo que você talvez já tenha visto ou jogado, o que estamos falando aqui é da linguagem de programação Rust, mas você já deve ter percebido isso.
Rust é uma linguagem de programação multiparadigma, desenvolvida pela Mozilla, focada em segurança, performance e produtividade. A linguagem fornece abstração sem overhead, gerenciamento de memória automático sem Garbage Collector, e concorrência sem data-race.
Ela é uma linguagem estática fortemente "tipada" e inspirada por C++, C#, Haskell, entre outras, e como dito anteriormente, ela é multiparadigma, os principais são o: paradigma funcional, imperativo, concorrente, estruturado e genérico. Apesar de alguns considerarem Rust como uma linguagem orientada a objetos, devemos lembrar que os 4 pilares da linguagem orientada a objetos são:
- Encapsulamento
- Abstração
- Herança
- Polimorfismo
Por mais que Rust tenha, de certa forma, encapsulamento, abstração (por meio de Traits), e polimorfismo, Rust não tem um dos pilares da linguagem orientada a objetos, que é a herança, e nesse caso, sem um pilar, eu não sustentaria a ideia de Rust ser orientado a objetos, e acredito que a falta de um pilar também não sustenta uma estrutura.
Rust também pode ser utilizado na programação de sistemas operacionais (como o Redox) e produz binários tanto dinamicamente como estaticamente linkados (com musl por exemplo). Bem como pode ser usado em sistemas embarcados, em aplicações web, no frontend (pois suporta WebAssembly nativamente e pode ser utilizado com Emscripten), em redes. Rust é uma linguagem de propósito geral, e pode ser usada em quase todo tipo de aplicação.
Instalação e configuração
A instalação e configuração costuma ser a parte mais fácil de uma linguagem, na maioria das vezes é só baixar um executável e rodar, ou apenas rodar um comando no terminal para baixar os binários e instalar, como nos sistemas *nix. Mas no caso do Rust, o processo é bem interessante, o ecossistema do Rust tem um instalador interativo chamado Rustup, ele te guia por todo o processo de instalação do Rust, permitindo escolher o canal da linguagem, set de recursos disponíveis (minimal, default, complete), e até plataformas alvo (target), como WebAssembly, ARM, PowerPC, Fuchsia, e até escolhendo entre glibc ou musl.
Iniciar um projeto
Um projeto novo pode ser criado com um simples comando: cargo init [nome], por exemplo:
cargo init medium
Você também pode usar a flag lib para criar uma biblioteca:
cargo init --lib medium
Suporte a IDE
Plugins para diferentes IDEs podem ser encontrados no site oficial do Rust, inclusive, diversas ferramentas como o Rustfmt, para formatação, o Clippy que é um linter e o RLS como frontend para auto completar estão disponíveis.
Eu particularmente utilizo o plugin de Rust para o CLion com o Rustfmt habilitado no lugar do formatador do plugin do Rust, e o Clippy no lugar do cargo check com a opção de habilitar o linter para analisar o código on-the-fly.
Gerenciamento de dependências
Para o gerenciamento de dependências, o Rust tem o Cargo (utilizando o registro crates.io), que no meu ponto de vista, é o melhor gerenciador de pacotes de todos, de longe. Eu tinha sim havia curtido bastante o go get, mesmo sofrendo com o GOPATH, já era melhor do que o que usamos no Java, e concordo que o Cargo é equiparável ao Npm e ao Pip, mas ao meu ver, e pela minha experiência, o Cargo se integra muito melhor com a linguagem Rust, e isso é meio óbvio, já que ele é construido e mantido pelos próprios mantenedores da linguagem, o que dá bastante vantagens a ele.
Adicionar uma dependência em um projeto no Rust é simples como é, ou pelo menos deveria ser, em qualquer gerenciador de pacotes. Adicionar a dependência na biblioteca rand, por exemplo, consiste em apenas adicionar a seguinte linha ao Cargo.toml do projeto na seção dependencies:
E agora magicamente a dependência já pode ser usada.
Simples Olá Mundo
Vamos ver então como é um simples olá mundo no Rust:
Notas
Nativo
Nativo, no contexto do artigo, refere-se a linguagens onde o resultado da compilação, mais precisamente o executável produzido, pode ser executado diretamente sem a necessidade de intermediários, como uma máquina virtual. No entanto, isso não quer dizer necessariamente que o executável produzido não tenha uma Runtime embarcada, como acontece com Golang e com o Java quando se produz uma Imagem Nativa.
Quando a frase "linguagens nativas" é incluída, estou falando exatamente de linguagens que produzem binários que podem ser executados sem intermediários, diretamente pelo Sistema Operacional.
Baixo Nível
A própria ideia de linguagem de baixo nível é dificil de exprimir, dado que hoje em dia, a maioria das linguagens que eram "de baixo nível", hoje implementam conceitos de alto nível. Ao mesmo tempo, que a expressão "linguagem de baixo nível" é utilizada de forma errônea por muitos, onde confundem abstração do hardware e do sistema operacional, com abstração de funcionalidades, como na programação orientada a objetos.
Linguagens de baixo nível são aquelas que abstraem pouco ou nada das funcionalidades relacionadas a arquitetura, ou seja, não abstraem o acesso ao sistema de arquivos nem a comunicação com o hardware, ou quando o fazem, fazem minimamente e muito ligadas a plataforma em que irão executar, isso quer dizer que o binário final é extremamente dependente da arquitetura e sistema operacional que irão rodar, mas não quer dizer que não possam ser compiladas para outras arquiteturas, apenas que o resultado final é extremamente dependente da plataforma.
As linguagens de baixo nível, muitas vezes podem abstrair o acesso ao sistema de arquivos por exemplo, ou ao networking, mas ao código ser compilado, as flags e a plataforma destino irá dizer ao compilador quais funções equivalentes na plataforma deverão ser executadas, mantendo ainda assim que o binário final seja pouco portável e muito dependente. Diferente de linguagens de alto nível, como Java, onde o binário produzido pode ser executado em qualquer sistema que tenha uma Runtime compatível (a JVM) disponível.