36 pontos por GN⁺ 2025-08-29 | 1 comentários | Compartilhar no WhatsApp
  • Padrões de design orientados a objetos também permitem implementar polimorfismo e modularidade em kernels escritos em C, possibilitando um design de sistema mais flexível
  • Usando vtable (tabela de funções virtuais), é possível padronizar interfaces de dispositivos e serviços e oferecer vários comportamentos por meio de mudanças dinâmicas em tempo de execução
  • Serviços do kernel e o escalonador fornecem interfaces consistentes para ações como iniciar, parar e reiniciar via vtable, encapsulando os detalhes de implementação
  • Em conjunto com módulos de kernel, isso permite carregamento dinâmico de drivers e expansão do sistema sem recompilação
  • Essa abordagem oferece flexibilidade e liberdade para experimentação, mas tem como desvantagem a verbosidade causada por sintaxe complexa e passagem explícita de objetos

Liberdade e padrões orientados a objetos no desenvolvimento de OS

  • Desenvolver seu próprio OS permite experimentação livre, sem as restrições de colaboração ou de aplicações reais
    • Liberdade em relação a vulnerabilidades de segurança, manutenção de código e pressão de release
    • Esse é um dos atrativos do desenvolvimento de OS, pois permite explorar padrões de programação não convencionais
  • O artigo da LWN “Object-oriented design patterns in the kernel” apresenta casos em que o kernel Linux implementa princípios orientados a objetos em C
    • O polimorfismo é implementado com structs que incluem ponteiros de função
    • Encapsulamento, modularidade e extensibilidade permitem aproveitar benefícios de orientação a objetos mesmo em kernels de baixo nível

Conceito básico de vtable

  • vtable é uma struct com ponteiros de função que define a interface de um objeto
    • Exemplo: struct para operações de dispositivo
      struct device_ops {  
          void (*start)(void);  
          void (*stop)(void);  
      };  
      struct device {  
          const char *name;  
          const struct device_ops *ops;  
      };  
      
  • Dispositivos diferentes (por exemplo, netdev e disk) usam a mesma API, mas com implementações distintas
    • netdev.ops->start() chama o comportamento do dispositivo de rede, enquanto disk.ops->start() chama o do dispositivo de disco
  • Mudança em tempo de execução: é possível trocar a vtable dinamicamente para alterar o comportamento sem mudar o código chamador
    • Com sincronização adequada, isso permite uma evolução dinâmica de comportamento de forma limpa

Casos de uso no OS

Gerenciamento de serviços

  • Serviços do kernel (gerenciador de rede, pool de workers, servidor de janelas etc.) podem ser gerenciados com uma interface consistente
    • Struct de serviço:
      struct service_ops {  
          void (*start)(void);  
          void (*stop)(void);  
          void (*restart)(void);  
      };  
      struct service {  
          pid_t pid;  
          const struct service_ops *ops;  
      };  
      
  • Cada serviço implementa seu comportamento específico, mas pode executar iniciar/parar/reiniciar de forma padronizada a partir do terminal
  • Reduz o acoplamento entre código e serviços, simplificando a administração

Escalonador

  • O escalonador pode suportar várias estratégias, como round-robin, shortest-job-first, FIFO e escalonamento por prioridade
    • A interface é simplificada em yield, block, add e next
    • Definida por vtable, permitindo trocar a política de escalonamento em tempo de execução
    • É possível mudar toda a política sem modificar o restante do kernel

Abstração de arquivos

  • A struct file_operations do Linux implementa a filosofia de que “tudo é arquivo”
  • Sockets, dispositivos e arquivos de texto fornecem a mesma interface de read/write
  • O código em espaço de usuário pode operar de maneira consistente sem precisar conhecer os detalhes de implementação

Integração com módulos de kernel

  • Módulos de kernel permitem carregar dinamicamente drivers ou hooks por meio da troca de vtable
    • Como nos módulos do Linux, o kernel pode ser expandido sem recompilação ou reboot
    • Ao adicionar novas funcionalidades, basta atualizar a vtable da struct existente

Desvantagens

  • Complexidade sintática:
    • É preciso passar explicitamente o objeto, como em object->ops->start(object)
    • Isso é mais verboso em comparação com a passagem implícita do C++
    • As assinaturas de função também ficam longas:
      static void object_start(struct object* this) {  
          this->id = ...  
      }  
      
  • Vantagem: a passagem explícita deixa claras as dependências da função, tornando transparente o acoplamento entre objeto e comportamento
    • Um tradeoff adequado entre complexidade e clareza no código de kernel

Implicações

  • vtable oferece uma forma simples de reduzir complexidade enquanto preserva a flexibilidade
    • Facilita trocar comportamentos em tempo de execução, manter interfaces consistentes e adicionar novas funcionalidades
  • Também apresenta uma nova forma de implementar design orientado a objetos em C, destacando o lado experimental e divertido do desenvolvimento de OS
  • Material adicional: o projeto xine (https://xine.sourceforge.net/hackersguide#id324430) mostra como gerenciar variáveis privadas com vtable
  • O desenvolvimento de OS é um campo de experimentação criativa, e padrões orientados a objetos provam ser ferramentas poderosas até mesmo em sistemas de baixo nível

1 comentários

 
GN⁺ 2025-08-29
Comentários do Hacker News
  • Discute-se um texto sobre como o kernel Linux, apesar de ter sido escrito em C, adotou princípios de orientação a objetos, como implementar polimorfismo usando ponteiros de função em structs. Essa técnica existe desde muito antes da programação orientada a objetos e costuma ser chamada de "tipo de dado abstrato (ADT)" ou abstração de dados. A principal diferença entre ADT e OOP é que, em ADT, a implementação de funções pode ser omitida, enquanto em OOP sempre há necessidade de implementação. Se forem necessárias funções opcionais em OOP, é preciso criar classes adicionais para cada função opcional, herdá-las juntas por meio de herança múltipla sempre que isso for implementado e verificar em tempo de execução se o objeto é instância dessa classe adicional. Já em ADT, basta verificar se o ponteiro de função é NULL
    • Em Smalltalk e Objective-C, a abordagem tradicional de OOP é simplesmente verificar em tempo de execução se o objeto pode responder à mensagem. É lamentável que a essência de OOP tenha sido distorcida por causa dos padrões de projeto excessivamente centrados em classes de C++ e Java
    • A maioria concorda e menciona que também usa esse padrão em C, enquanto em OOP tradicional uma abordagem comum é colocar implementações padrão (default) ou stubs na base. Em OOP mais moderno ou em linguagens orientadas a conceitos, também há a forma de fazer cast para interfaces que usam apenas subconjuntos da API necessária. Go é um bom exemplo
    • Sobre a afirmação de que essa técnica veio antes da programação orientada a objetos, alguém diz que preferiria expressar isso como OOP sendo uma formalização de padrões e paradigmas que já existiam
    • Mesmo em linguagens OOP como Java e C#, hoje em dia dá para implementar isso exatamente como em C usando lambdas. Lambdas nada mais são do que ponteiros de função, então podem ser atribuídos diretamente a variáveis de instância. (Há também a anedota curiosa de que o Java levou mais de 10 anos para introduzir lambdas, e no passado a Sun Microsystems chegou a processar a Microsoft por uma tentativa de adicionar lambdas ao Java)
    • Herança não é obrigatória. Dá para usar o padrão composite. Python também é parecido nesse aspecto, pois é preciso passar explicitamente o ponteiro self/this/object, então se assemelha à abstração de dados no estilo de C
  • Alguns anos atrás, Peterpaul chegou a desenvolver um sistema leve de orientação a objetos que pode ser usado de forma confortável sobre C (repo). Não é necessário passar o objeto explicitamente, e embora a documentação seja escassa, há uma suíte completa de testes (teste1, teste2)
    • Se quiser ver como isso fica sem o açúcar sintático do carbon, dá para ver aqui. Parece não haver suporte a polimorfismo paramétrico
    • Também parece que Vala está tentando algo apropriado para esse nicho
  • A pessoa diz não dominar muito esse ponto, mas acha que o OP está fazendo algo diferente do que os desenvolvedores do kernel fizeram. Lendo o texto linkado pelo OP, a impressão é que a vtable contém ponteiros de função tipados, enquanto o OP estaria usando ponteiros void. Além disso, a principal vantagem mencionada no texto dos desenvolvedores do kernel é economizar memória, deixando apenas um ponteiro para vtable em cada instância da struct, em vez de vários ponteiros de função. Ou seja, o ponto principal é a economia de memória, mas o OP está usando essa vtable como uma indireção para trocar métodos em tempo de execução e implementar polimorfismo. Esse padrão é diferente do que os desenvolvedores do kernel estavam descrevendo
    • O OP quis dizer void, e não ponteiro void. Vtable é usada para implementar polimorfismo. Sem polimorfismo, nem se usaria vtable, então a economia de memória seria ainda maior
  • Sobre a opinião de que é inconveniente ter de passar o objeto explicitamente toda vez, alguém diz que, na verdade, não gosta do uso implícito de this. Na prática, você está sempre passando a instância this, e o this explícito evita confusão sobre se uma variável pertence à instância, é global ou veio de outro lugar
    • Em C++ (e Java), acha que um dos grandes erros da sintaxe OOP é não exigir o uso obrigatório de this ao referenciar membros de instância
    • A pessoa entende que o autor está apontando o fato de que, em object->ops->start(object), o objeto precisa ser explicitado duas vezes: uma para resolver a vtable e outra para passá-lo à implementação da função em C
    • Para deixar clara a origem das variáveis, costuma-se usar convenções de nomenclatura para membros como mFoo, m_Foo, foo_ etc. foo_ é preferido por ser mais conciso que this->foo. Claro, em C++ também é possível usar this explicitamente
    • O this implícito deixa o código mais conciso, e quando se usa um método de verdade não é preciso repetir o prefixo da struct em cada função. Por exemplo, fica mais natural escrever s->dosmth(); em vez de mystruct_dosmth(s);
    • Também dá para tratar isso de forma mais esperta usando macros
  • Alguém diz que aprendeu esse padrão em C pela primeira vez em uma apresentação do Tmux (material). Também tem um texto próprio organizando essas ideias (post sobre comandos orientados a objetos no tmux)
  • Na época da faculdade, a pessoa implementou isso em alguns projetos pequenos. Foi divertido dar a C uma sensação parecida com OOP, mas, se não tomar cuidado, os problemas podem crescer muito rápido
  • É preciso notar que isso é um padrão que utiliza interfaces (ou seja, vtable, tabela de ponteiros de função), não o objeto inteiro. Outros recursos de orientação a objetos, como classes e herança, acabam sendo mais custosos e difíceis de seguir
    • Herança, no fim das contas, é uma forma de composição de vtable. Classe também não passa de uma combinação de vtable com variáveis de escopo
    • Em C, se fizer cast de uma struct colocada como primeiro membro, herança de campos acaba sendo mais natural do que parece
    • Normalmente, a vtable contém funções que recebem o ponteiro this. O exemplo de struct file_operations tem ponteiros de função que não recebem this, então é difícil considerá-lo uma vtable de verdade
  • Há quem crie wrappers inline para as funções da vtable, para poder escrever foo(thing, ...) em vez de thing->vtable->foo(thing, ...)
  • Alguém sempre se perguntou por que esse padrão não foi incorporado ao novo padrão da linguagem C. Claramente muita gente acaba implementando a mesma coisa repetidas vezes
    • Se for adicionado açúcar sintático, teria de coexistir tanto uma forma oficialmente permitida quanto um fallback que parece estar faltando algo. Uma das vantagens de C é não esconder a complexidade dinâmica. Quando ocorre despacho dinâmico, isso sempre fica explícito. Muitas linguagens já fornecem essa formalização, mas a vantagem própria de C é justamente deixar a complexidade aparente. Assim, isso só é usado quando realmente há necessidade de despacho dinâmico. Além disso, a sintaxe nem é tão difícil
    • Pelo visto, houve alguma tentativa nessa direção no lado do High C Compiler
  • Surge um conselho forte, vindo de experiência prática, para nunca usar esse padrão. A pessoa sofreu com o pesadelo de manter código grande estruturado dessa forma. A legibilidade é horrível, o compilador não consegue otimizar chamadas por causa dos ponteiros, e não há qualquer suporte de tooling. A sintaxe também é estranha, e novos integrantes só conseguem ler o código depois de praticamente dominar os internos de um compilador de C++. Acima de tudo, em comparação com os benefícios duvidosos de introduzir OOP, isso pode arruinar a manutenção no longo prazo. Se realmente for necessário, o certo seria simplesmente usar C++
    • Em resposta à pergunta sobre o que exatamente foi tão ruim, alguém diz achar que menos açúcar sintático na verdade melhora a legibilidade, porque deixa claro quando uma chamada de função é despacho dinâmico. Assim, dá para limitar o uso só aos lugares que realmente precisam disso. E a pessoa também viu um post dizendo que código dinâmico em C é mais fácil de otimizar por usar menos ponteiros de função. A ideia não é reimplementar um compilador de C++ por completo, mas simplesmente que, entendendo a essência de OOP, isso se torna natural de implementar. Por fim, quanto ao argumento de “não transformar C em um C++ malfeito”, a defesa é que esse é justamente um jeito bem C de fazer as coisas, além de facilitar inserir dinamismo exatamente onde se quer.