Compilador de Rust para C atinge 95,9% de aprovação nos testes
(fractalfir.github.io)rustc_codegen_clré um projeto que gera código Rust para C e .NET, e está prestes a apresentar na Rust Week junto com a melhora na taxa de aprovação dos testes- A taxa de aprovação dos testes do Rust core subiu de 92% há dois meses para 95,9%, e os cerca de 65 testes restantes parecem ter causas bastante parecidas entre si
- As correções recentes se concentraram em intrínsecos de inteiros de 128 bits, checked arithmetic e subslicing, e o lado .NET também passou a conseguir executar 96,3% dos testes do Rust core
- O objetivo é gerar saída mais próxima de C99 ou ANSI C e usar APIs POSIX padrão para atingir mais compiladores C, e alguns compiladores ANSI C já conseguiram executar programas Rust muito simples
- Também estão em andamento otimizações para reduzir o tamanho dos arquivos em geração de código C em larga escala e uma refatoração do IR interno, formando a base para ampliar o alcance do Rust em plataformas antigas ou especiais
Apresentação na Rust Week e andamento do projeto
- A apresentação do projeto
rustc_codegen_clrestá prevista para a Rust Week, realizada em Utrecht, nos Países Baixos - A apresentação busca equilibrar acessibilidade para iniciantes com o tema avançado de compilação de Rust para C
- Nos últimos meses, avançaram em paralelo correções de testes, compatibilidade com compiladores C, melhorias de desempenho e refatoração interna
Aumento da taxa de aprovação dos testes do Rust core
- A taxa de aprovação dos testes do Rust core aumentou de 92% há dois meses para 95,9%
- Ainda restam cerca de 65 testes, mas como as causas parecem semelhantes entre si, o tipo de correção necessário está relativamente mais delimitado
- O lado .NET também foi beneficiado pelas mesmas correções e agora consegue executar 96,3% dos testes do Rust core
Correções em inteiros de 128 bits e checked arithmetic
- Boa parte da melhora veio de correções em intrínsecos de 128 bits, checked arithmetic e subslicing
- O intrínseco
popcountem C tem três variantes:__builtin_popcount,__builtin_popcountle__builtin_popcountll- Apesar do nome,
__builtin_popcountllnão opera sobre__int128_t, e sim sobreunsigned long long - Em x86_64 Linux com GCC, tanto
unsigned longquantounsigned long longtêm 64 bits
- Apesar do nome,
- A implementação anterior truncava silenciosamente inteiros de 128 bits para 64 bits em intrínsecos de contagem de bits, produzindo resultados incorretos
- O
popcountde 128 bits foi emulado calculando separadamente a quantidade de bits ativados nos 64 bits inferiores e superiores, e somando os resultados - A verificação de overflow na multiplicação de 128 bits foi tratada com uma abordagem simples por não terem encontrado um método eficiente
- Se
bnão for 0 e(a * b) / b == a, considera-se que não houve overflow - Não é uma técnica revolucionária, mas foi suficiente para fazer mais alguns testes passarem
- Se
Bug de subslicing e intrínsecos de fallback
- O bug de subslicing era causado pela ausência de
sizeof, fazendo com que o ponteiro de dados do slice fosse deslocado em bytes, e não em unidades de elemento - Esse bug só quebrava ao fazer subslicing a partir do fim do slice, e como esse formato é usado principalmente em pattern matching, passou despercebido por muito tempo
- Em byte slices e string slices, como o tamanho do byte e da unidade UTF-8 é de 1 byte, os testes próprios passavam, e o problema só apareceu no conjunto completo de testes do compilador Rust
- Alguns intrínsecos podem usar a implementação de fallback do compilador Rust em vez de uma implementação direta
carrying_mul_addprecisa realizar a multiplicação em um inteiro com tamanho 2x maior que o da entrada- Até 64 bits isso é viável, mas entradas de 128 bits exigem inteiros de 256 bits
- O LLVM suporta inteiros de 256 bits, mas C e .NET não
- A implementação de fallback do compilador Rust realiza multiplicação e soma de 256 bits com inteiros de 128 bits, servindo como referência para emular operações de 128 bits usando apenas inteiros de 64 bits
Suporte a mais compiladores C
- O projeto quer aumentar as chances de código Rust rodar em mais compiladores C especiais
- Já houve casos de código Rust rodando em Game Boy, sendo compilado apenas com o comando move, ou executado no Holly C do Temple OS
- Não é possível dar suporte direto a compiladores C proprietários inacessíveis, mas ampliar a cobertura de compiladores C obscuros aumenta a chance de código Rust rodar nesses ambientes
- Muitas plataformas deixam de ser suportadas por falta de documentação e dificuldade de acesso
- Em discussões sobre escrever partes do Git em Rust, plataformas proprietárias como NonStop não suportam Rust, LLVM nem GCC, o que pode levar à degradação ou interrupção do suporte ao Git
- Ao compilar Rust para C, em teoria torna-se possível executar Rust em qualquer lugar onde exista C
- Ainda não está claro se será possível contornar todos os problemas em todas as plataformas
- Para obter legalmente um compilador para essa plataforma, seria necessário comprar um servidor, algo muito além do orçamento
- A chance de Rust rodar logo nessas plataformas parece baixa
Estratégia atual de geração de C
- O plano atual é gerar código o mais próximo possível de C99 compatível com o padrão ou de ANSI C
- Como a inicialização thread-local exige algum suporte a threads, a direção escolhida é usar apenas a API POSIX padrão
- O projeto mantém implementações de fallback próprias para certos intrínsecos, e essa lista vem sendo expandida gradualmente
- Já foi possível executar programas Rust muito simples em alguns compiladores ANSI C
- A meta é deixar até plataformas hoje difíceis de suportar em um estado em que possam ser adicionadas com relativa facilidade quando surgir necessidade
Pequenas melhorias de desempenho e tamanho de arquivo
- Na saída de literais inteiros, passou-se a considerar que inteiros menores que 2^32 ficam do mesmo tamanho ou menores em decimal do que em hexadecimal
255é 1 byte menor que0xFF65536também é menor que0xFFFF- Por causa do prefixo
0x, a representação hexadecimal não fica menor antes de 2^32
- No caso extremo de converter o compilador Rust inteiro para C, já chegaram a gerar um arquivo-fonte C de até 1 GB, então até pequenas reduções percentuais no tamanho têm impacto
- A diretiva
#line, usada para inserir informações de debug, também ficou mais inteligente- O nome do arquivo-fonte só é incluído quando ele muda
- Isso pode reduzir bastante o tamanho do arquivo ao usar informações de debug
Refatoração interna e organização do IR
- Parte das funcionalidades internas de
rustc_codegen_clrfoi separada em crates distintos para acelerar o incremental build - Também está em andamento a migração para um IR internado mais eficiente em memória
- O IR antigo tinha rvalues/lvalues peculiares que não se mapeavam bem para C, e os problemas ficavam maiores em recursos do Rust como tipos de tamanho dinâmico
- Por exemplo, em um tipo dinâmico customizado como
MyStr,&self.sparece simples, mas na prática exige lidar com metadados de fat pointers- Em C99, dá para usar compound literals e criar
struct FatPtr_strem uma linha - Em ANSI C, é preciso colocar
dataemetaseparadamente em variáveis temporárias e depois retornar
- Em C99, dá para usar compound literals e criar
- Como uma linha de Rust e MIR pode se expandir para várias linhas de C, o IR antigo tratava isso com escopos internos contendo temporários locais e substatements
- O novo modelo tem uma etapa de preparação mais complexa, mas simplifica o IR como um todo, e quando os últimos casos difíceis forem resolvidos, será possível remover essa parte do IR antigo
Próximos trabalhos
- O período de trabalho no projeto já chegou a cerca de 1,5 ano, e quanto menos bugs restam, mais tempo passa a ser necessário para encontrar os que faltam
- Está em produção a parte 2 de “Rust panics under the hood”, que pretende explicar passo a passo o processo de panic em Rust
- Só a explicação da geração da mensagem de panic já rende cerca de 10 minutos, então existe a possibilidade de dividir o texto em duas partes
- Também está em andamento um profilador de memória pequeno e preciso para Rust
- O código tem cerca de 2K LOC
- Apesar do cronograma apertado, a intenção é escrever sobre isso nas próximas semanas
1 comentários
Opiniões no Hacker News
Originalmente, o projeto rustc_codegen_gcc prometeu isso, mas acabou não cumprindo
[0] https://github.com/rust-lang/compiler-team/issues/458
No lado GCC/Clang, é “apenas” 80%
| .NET Core tests | 1764 | 48 | 20 | 96.29% |
| C Core tests | 1712 | 71 | 8 | 95.59% |
Esse hack é um back-end do compilador Rust. Como o back-end recebe instruções específicas da plataforma como entrada, o código C não trivial gerado provavelmente não será portável
O usuário teria de receber código-fonte pré-gerado específico da plataforma, ou instalar o compilador Rust e esse back-end para gerar por conta própria
extern "c"do Rust?“Minha representação IR para .NET se mapeia bem para C e, por isso, consegui adicionar suporte para compilar Rust para C com 2 a 3 mil linhas de código. Quase toda a base de código é reutilizada, e o código específico de C e .NET só existe na etapa final da compilação”
Começou como um back-end .NET, mas perceberam que a abordagem também permitia dar suporte facilmente à geração de código C e então adicionaram isso. Ele transforma o que o rustc entrega em uma representação intermediária (IR) própria e processa a partir daí
Garantias dinâmicas, como verificações de limites, também podem ser implementadas sem problema no runtime em C
unsafeC não carrega todas as informações que precisariam ser conhecidas explicitamente para compilar em Rust
https://www.youtube.com/watch?v=1VgptLwP588
unsafee, em geral, o código resultante não parece idiomático em RustPor exemplo, transformar uma máquina de estados baseada em variáveis globais em uma máquina de estados mais idiomática em Rust, usando algo como enums nomeados, seria muito difícil
Talvez seja possível com a ajuda de uma IA suficientemente poderosa, mas a IA ainda está bem aquém de realmente fazer o que se pretende, então ainda não parece pronta. Também seria preciso memória suficiente para colocar bases inteiras de código C e Rust dentro da janela de contexto; quando o código passa de certo tamanho, isso rapidamente exigiria hardware muito caro. Caso contrário, como muitos LLMs assistentes de código, ela acabaria gerando de forma independente códigos que não são compatíveis entre si
Ainda assim, se você quiser estender um projeto em C com Rust ou reescrevê-lo aos poucos, https://c2rust.com/ está pronto para uso
Rust é uma linguagem moderna, com gerenciamento de pacotes, ferramentas integradas de build/teste simplificadas, muito menos legado, além de recursos e sintaxe de alto nível de que as pessoas realmente gostam
C também é elegante, mas bases de código complexas se beneficiam de uma linguagem moderna que ajuda a criar abstrações robustas mantendo a velocidade de C. Sem esquecer, é claro, do borrow checker e da segurança de memória
Se houver um conversor Rust→C que gere código C suficientemente padrão, isso pode preencher essa lacuna