- Acho que as pessoas não têm noção suficiente de quão incompleta é a documentação da API do kernel Linux e de como Rust resolve parte desse problema
- Escrevi abstrações Rust para vários subsistemas e, em quase todos os casos, precisei ler o código-fonte em C para entender completamente como usar a API com segurança
- Só com a assinatura da função e os comentários de documentação relacionados, ou mesmo com documentação explícita, é difícil entender totalmente como usar a API com segurança
- se é preciso manter locks ativos
- se um argumento de contagem de referências transfere uma referência ou assume uma referência própria
- se o lock é mantido ao chamar callbacks ou se é preciso adquiri-lo manualmente
- se há algo especial no callback de free
- qual é a ordem de locking pretendida
- se existem situações especiais em que algumas operações podem usar locking dependendo do caso
- se argumentos
NULLsão permitidos e qual é o uso válido - o que acontece com a contagem de referências em caso de erro
- se o ponteiro com contagem de referências retornado já vem incrementado ou se é um empréstimo implícito da referência do argumento passado
- se o valor retornado é sempre um ponteiro válido, se pode ser
NULLou atéERR_PTR - se o ponteiro retornado via argumento indireto é limpo para
NULLem caso de erro ou permanece inalterado - se passar
NULL **é válido quando o ponteiro de retorno não é necessário
- Às vezes, os requisitos eram razoáveis, mas não estavam documentados
- às vezes, os requisitos eram flexíveis ou complexos demais, então foi preciso tomar decisões subjetivas ao escrever abstrações Rust para restringi-los a um uso seguro
- às vezes, foi preciso introduzir locking adicional dentro da abstração para garantir segurança
- às vezes, foi necessário fazer pequenas mudanças no código C para torná-lo mais ortogonal, lógico e fácil de usar (por exemplo, expor uma função de unlock para uso enquanto o lock está mantido)
- às vezes, o locking era sutil o bastante para que desse para escrever uma abstração Rust segura, mas ainda assim fosse necessário um grande comentário de documentação avisando para tomar cuidado com a forma de uso e a ordem de liberação para evitar deadlocks (Rust não previne deadlocks por si só)
- às vezes, era impossível resolver sem tornar o código C mais razoável (no caso de
drm_sched)
- Porém, na maioria dos casos, os compromissos feitos ao escrever abstrações Rust indicam problemas no design do código C e caminhos para melhorá-lo
- a abordagem geral é "primeiro escrever código Rust mudando o mínimo possível do código C para evitar conflitos e, depois, com base nas lições aprendidas, propor mudanças no código C" (ainda não consegui chegar à segunda parte)
- Como resultado, na maioria dos casos dá para saber o uso correto olhando apenas para a API Rust
- não é preciso se preocupar com contagem de referências, ponteiros
NULL, esquecer de checar resultados ou liberar referências em caso de erro - não é preciso se preocupar com uso correto de locking, esquecer de obter referências ou dar free duplo
- não é preciso ficar se perguntando como os valores de retorno de erro são codificados
- porque, se você errar essas coisas, o código simplesmente não compila
- claro, ainda dá para usar a API de forma errada, mas no pior caso isso só causa retorno de erro ou deadlock (
deadlocké fácil de depurar comlockdep, e a integração comArc<>pode capturar erros de locking relacionados a free/desreferência)
- não é preciso se preocupar com contagem de referências, ponteiros
- Até mesmo a API de OpenFirmware/DeviceTree, que é relativamente bem documentada, é tediosa e propensa a erro em C quando é preciso seguir todas as regras
- ao olhar o código OF de drivers, a chance de vazamento de referência é alta
- a maioria dos sistemas não compila o kernel com
OF_DYNAMIC, então a contagem de referências é ignorada e isso não é detectado nem corrigido - porém, a abstração Rust de OF que escrevi lida automaticamente com a contagem de referências, então não é preciso se preocupar com isso
- Vantagens de programar para o kernel em Rust em comparação com C
- ao programar para o kernel em C, só existem duas opções
- tentar fazer e torcer para que o revisor pegue os problemas, ou sofrer com depuração
- passar horas entendendo tudo antes mesmo de se atrever a usar o código, esperando encontrar todos os detalhes
- isso também aumenta a carga de trabalho de revisores e mantenedores
- eles precisam revisar envios para verificar se todas as regras ocultas e não documentadas estão sendo seguidas
- às vezes deixam passar problemas, e às vezes os problemas são tão grandes que o código precisa ser amplamente refatorado
- Em Rust, tudo isso desaparece. Se compila, é seguro e não há mau funcionamento nem vazamento de referências (código
unsafeé a exceção, mas só ele precisa ser revisado, e há a regra de que deve ser bem documentado)- claro, ainda é necessária revisão de código e ajuda de especialistas no subsistema. Rust não deixa o código perfeito por mágica
- porém, ele elimina todos os problemas e erros idiotas de baixo nível, então é possível focar nos problemas de alto nível
- Posição sobre os desenvolvedores de Linux
- não culpo os desenvolvedores de Linux pela documentação incompleta
- o kernel Linux é muito complexo e precisa lidar com muitas sutilezas
- a maioria das APIs de espaço de usuário tem regras muito mais simples para uso seguro
- o kernel é difícil
- até desenvolvedores experientes do kernel erram essas coisas o tempo todo
- não é um problema de habilidade técnica, mas sim do fato de que é impossível para seres humanos manter todas essas regras complexas na cabeça e executá-las corretamente todas as vezes
- Solução
- precisamos de ferramentas
- a solução é Rust. Depois de codificar todas as regras uma vez no código e no sistema de tipos, não é mais preciso se preocupar com isso
- é como a solução para discussões sobre estilo de código: codificar todas as regras em um formatador automático
- assim, podemos parar de nos preocupar com toda a segurança de baixo nível, ownership e problemas de sincronização, e passar a nos preocupar com coisas mais importantes, como o design de drivers e subsistemas em alto nível
- Formatação de código no projeto Rust for Linux
- o projeto Rust for Linux realmente aplica
rustfmtaos envios - ao escrever Rust para o kernel, não é preciso se preocupar com formatação de código nem com reclamações em revisão por causa disso
- basta rodar
make rustfmt
- o projeto Rust for Linux realmente aplica
Opinião do GN⁺
- Este texto aponta bem os problemas de documentação de API e segurança no desenvolvimento do kernel Linux. Mostra claramente as limitações de C e as vantagens de Rust
- Porém, a expressão "Rust é a única solução" parece um pouco exagerada. Parte das melhorias também poderia vir de outras abordagens, como ferramentas de análise estática
- Rust resolve muitos problemas, como segurança de memória, mas revisão cuidadosa de código e testes ainda continuam necessários. Não é uma bala de prata
- Migrar para Rust pode trazer várias dificuldades, como compatibilidade com código C existente e curva de aprendizado dos desenvolvedores. Uma adoção gradual parece mais desejável
- Para melhorar práticas e a cultura antiga do kernel Linux, além de Rust também serão necessários esforços em várias frentes, como documentação, mentoria e comunicação
- No geral, este texto mostra bem o potencial e as vantagens de Rust no desenvolvimento do kernel Linux, ao mesmo tempo em que alerta contra expectativas excessivas ou fé cega, apresentando uma visão equilibrada. A adoção de Rust terá dificuldades técnicas e culturais, mas, no longo prazo, pode contribuir para melhorar a segurança e a manutenibilidade do código do kernel.
2 comentários
Rust... pessoalmente até tentei estudar, mas ainda não estamos adotando aqui na empresa. Já temos uma montanha de coisas escritas em C++, e ainda existe a questão de a equipe atual ter que aprender Rust de novo... Ouvi dizer que já existem empresas na Coreia que estão usando Rust em produção; acho que seria ótimo se experiências desse tipo pudessem ser compartilhadas.
Comentários do Hacker News
Linguagens como Rust e Swift têm alta expressividade, então o compilador informa a segurança de threads de tipos de dados ou métodos
Muitas bibliotecas Rust têm documentação insuficiente
Ao tentar usar Rust como se fosse C, há dificuldades por causa do borrow checker
&selfou&mut self&mut self, é preciso usar um mutex para compartilhar a instância entre threadsAo olhar uma API Rust, na maioria dos casos dá para entender como usá-la corretamente
Como exemplo concreto em Rust, há a forma de usar locks para proteger dados
Em outras linguagens também, implementar APIs de forma redundante pode aumentar a clareza do código e da documentação
Ao usar C em extensões de Python, há o problema de precisar conhecer a convenção de chamada
Essas pessoas são heróis e fazem um excelente trabalho
Estamos um passo mais perto de implementar código totalmente autoexplicativo