36 puntos por GN⁺ 2025-08-29 | 1 comentarios | Compartir por WhatsApp
  • Los patrones de diseño orientado a objetos también permiten implementar polimorfismo y modularidad en kernels escritos en C, lo que hace posible diseñar sistemas más flexibles
  • Mediante vtable (tabla de funciones virtuales) se estandariza la interfaz de dispositivos y servicios, y el cambio dinámico en tiempo de ejecución permite soportar distintos comportamientos
  • Los servicios del kernel y el scheduler ofrecen interfaces consistentes para iniciar, detener y reiniciar a través de vtable, encapsulando los detalles de implementación
  • En combinación con los módulos del kernel, se soporta la carga dinámica de drivers, lo que permite expandir el sistema sin recompilar
  • Este enfoque ofrece flexibilidad y libertad para experimentar, aunque tiene la desventaja de una sintaxis compleja y de la verbosidad causada por el paso explícito de objetos

Libertad y patrones orientados a objetos en el desarrollo de OS

  • Desarrollar tu propio OS permite experimentar libremente sin las restricciones de la colaboración o de aplicaciones reales
    • Libera de vulnerabilidades de seguridad, mantenimiento de código y la carga de hacer lanzamientos
    • Ese es parte del atractivo del desarrollo de OS: permite explorar patrones de programación no estándar
  • El artículo de LWN “Object-oriented design patterns in the kernel” presenta casos en los que el kernel de Linux implementa principios orientados a objetos en C
    • El polimorfismo se implementa con structs que incluyen punteros a función
    • Gracias a la encapsulación, modularidad y extensibilidad, incluso un kernel de bajo nivel puede aprovechar las ventajas del diseño orientado a objetos

Concepto básico de vtable

  • Una vtable es una struct con punteros a función que define la interfaz de un objeto
    • Ejemplo: una struct para operaciones de dispositivo
      struct device_ops {  
          void (*start)(void);  
          void (*stop)(void);  
      };  
      struct device {  
          const char *name;  
          const struct device_ops *ops;  
      };  
      
  • Distintos dispositivos (por ejemplo, netdev, disk) usan la misma API, pero con implementaciones diferentes
    • netdev.ops->start() invoca la operación del dispositivo de red, mientras que disk.ops->start() llama la del dispositivo de disco
  • Cambio en tiempo de ejecución: se puede reemplazar dinámicamente la vtable para modificar el comportamiento sin cambiar el código llamador
    • Con una sincronización adecuada, esto permite una evolución dinámica del comportamiento de forma limpia

Casos de uso en un OS

Gestión de servicios

  • Los servicios del kernel (administrador de red, worker pool, servidor de ventanas, etc.) pueden gestionarse con una interfaz consistente
    • Struct de servicio:
      struct service_ops {  
          void (*start)(void);  
          void (*stop)(void);  
          void (*restart)(void);  
      };  
      struct service {  
          pid_t pid;  
          const struct service_ops *ops;  
      };  
      
  • Cada servicio implementa su propio comportamiento, pero desde la terminal puede iniciarse/detenerse/reiniciarse de manera estandarizada
  • Se reduce el acoplamiento entre el código y los servicios, simplificando la administración

Scheduler

  • El scheduler puede soportar distintas estrategias como round robin, shortest job first, FIFO y scheduling por prioridad
    • La interfaz se simplifica a yield, block, add y next
    • Definido mediante vtable, permite cambiar la política de scheduling en tiempo de ejecución
    • Se puede cambiar toda la política sin modificar el resto del kernel

Abstracción de archivos

  • La struct file_operations de Linux implementa la filosofía de “todo es un archivo”
  • Sockets, dispositivos y archivos de texto ofrecen la misma interfaz de read/write
  • El código en espacio de usuario puede operar de manera uniforme sin necesidad de conocer los detalles de implementación

Integración con módulos del kernel

  • Los módulos del kernel permiten cargar dinámicamente drivers o hooks mediante el reemplazo de vtables
    • Como en los módulos de Linux, es posible extender el kernel sin recompilar ni reiniciar
    • Al agregar nuevas funciones, basta con actualizar la vtable de la struct existente

Desventajas

  • Complejidad sintáctica:
    • Se debe pasar explícitamente el objeto, como en object->ops->start(object)
    • Es más verboso que el paso implícito de C++
    • Las firmas de función también son más largas:
      static void object_start(struct object* this) {  
          this->id = ...  
      }  
      
  • Ventaja: el paso explícito deja claras las dependencias de la función y hace transparente el acoplamiento entre objeto y comportamiento
    • En código de kernel, es un tradeoff razonable entre complejidad y claridad

Implicaciones

  • Las vtables ofrecen una forma simple de reducir complejidad manteniendo la flexibilidad
    • Facilitan reemplazar comportamientos en tiempo de ejecución, mantener interfaces consistentes y agregar nuevas funciones
  • Ofrecen una nueva manera de implementar diseño orientado a objetos en C, subrayando la diversión experimental del desarrollo de OS
  • Material adicional: el proyecto xine (https://xine.sourceforge.net/hackersguide#id324430) muestra cómo gestionar variables privadas con vtables
  • El desarrollo de OS es un espacio para la experimentación creativa, y demuestra que los patrones orientados a objetos son herramientas poderosas incluso en sistemas de bajo nivel

1 comentarios

 
GN⁺ 2025-08-29
Opiniones de Hacker News
  • Se comenta un artículo sobre cómo, aunque el kernel de Linux está escrito en C, adopta principios de orientación a objetos al usar punteros a función dentro de structs para implementar polimorfismo. Se señala que estas técnicas existían mucho antes de la programación orientada a objetos y que se conocen como "tipos de datos abstractos (ADT)" o abstracción de datos. La diferencia clave entre ADT y OOP es que en un ADT se puede omitir la implementación de funciones, mientras que en OOP siempre se requiere una implementación. Si en OOP se necesitan funciones opcionales, hay que crear clases adicionales para cada función opcional, heredarlas juntas mediante herencia múltiple cada vez que se implementen y verificar en tiempo de ejecución si el objeto es instancia de esa clase adicional. En cambio, con ADT basta con comprobar si el puntero a función es NULL
    • En Smalltalk y Objective-C, la forma tradicional de OOP es comprobar fácilmente en tiempo de ejecución si un objeto puede responder a un mensaje. Es una lástima que la esencia de OOP se haya distorsionado por patrones de diseño excesivamente centrados en clases en C++ y Java
    • En general están de acuerdo y mencionan que también usan este patrón en C, pero que en la OOP tradicional es común poner una implementación base predeterminada o de relleno en la clase base. En OOP moderna o en lenguajes orientados a conceptos, también existe la opción de hacer cast a interfaces que usan solo un subconjunto de la API necesaria. Go es un buen ejemplo
    • Sobre la afirmación de que esta técnica es anterior a la programación orientada a objetos, alguien dice que preferiría describir OOP como una formalización de patrones y paradigmas ya existentes
    • En la mayoría de los lenguajes OOP como Java y C#, ahora también se pueden usar lambdas, así que se puede implementar exactamente igual que en C. Una lambda no es más que un puntero a función, por lo que puede asignarse directamente a una variable de instancia. (También se menciona la anécdota de que Java tardó más de 10 años en introducir lambdas y que Sun Microsystems incluso llegó a demandar a Microsoft por un intento de agregar lambdas a Java en el pasado)
    • La herencia no es obligatoria. Se puede usar el patrón composite. Python también se parece a esto porque hay que pasar explícitamente el puntero self/this/object, así que se asemeja a la abstracción de datos al estilo de C
  • Hace algunos años Peterpaul desarrolló un sistema ligero de orientación a objetos que puede usarse cómodamente sobre C (repo). No hace falta pasar los objetos explícitamente y, aunque la documentación es escasa, sí tiene una suite de pruebas completa (prueba 1, prueba 2)
    • Si da curiosidad cómo se ve sin el azúcar sintáctica de carbon, se puede ver aquí. Parece que no soporta polimorfismo paramétrico
    • También opinan que Vala está haciendo un intento adecuado para este nicho
  • La persona comenta que no domina bien esta parte, pero le parece que el OP está haciendo algo distinto a lo que hicieron los desarrolladores del kernel. Al leer el artículo enlazado por el OP, las vtable tienen punteros a funciones tipadas, mientras que el OP da la impresión de usar punteros void. Además, la ventaja principal mencionada por el desarrollador del kernel es ahorrar memoria al poner solo un puntero a vtable por instancia de struct, en vez de varios punteros a función. Es decir, el punto principal es el ahorro de memoria, pero el OP está usando esa vtable como indirección para reemplazar métodos en tiempo de ejecución e implementar polimorfismo. Ese patrón es distinto de lo que describía el desarrollador del kernel
    • El OP no se refería a un puntero void, sino a void como tipo de retorno de una función sin argumentos. La vtable sí se usa para implementar polimorfismo. Si no hubiera polimorfismo, ni siquiera se usaría una vtable, así que se ahorraría todavía más memoria
  • Ante la idea de que es incómodo tener que pasar el objeto explícitamente cada vez, alguien dice que en realidad le desagrada el uso implícito de this. En la práctica, de todos modos se sigue pasando la instancia this, y tenerla explícita evita confusiones sobre si una variable pertenece a la instancia o viene de un ámbito global o de otro lugar
    • Considera que uno de los grandes errores de la sintaxis OOP de C++ (y Java) es no exigir this al referirse a miembros de instancia
    • Cree que el autor se refiere a la parte donde en object->ops->start(object) hay que mencionar el objeto dos veces: una para resolver la vtable y otra para pasarlo a la implementación de la función en C
    • Para dejar clara la pertenencia de las variables, suele usar convenciones de nombres como mFoo, m_Foo, foo_, etc. Prefiere foo_ porque es más conciso que this->foo. Claro que en C++ también se puede usar this explícitamente
    • El this implícito hace que el código sea más conciso y, al usar métodos reales, ya no hace falta repetir el prefijo del struct en cada función. Por ejemplo, s->dosmth(); se siente más natural que mystruct_dosmth(s);
    • También se puede manejar de forma más ingeniosa usando macros
  • Aprendió este patrón por primera vez en una presentación de Tmux (material) sobre este tipo de enfoque en C. También tiene un texto propio sobre el tema (artículo sobre comandos orientados a objetos en tmux)
  • En la universidad implementó este enfoque en algunos proyectos pequeños. Le pareció divertido darle a C una sensación similar a OOP, pero si no se tiene cuidado los problemas pueden crecer muy rápido
  • Hay que notar que este patrón usa interfaces (es decir, vtable, tablas de punteros a función), no el objeto completo. Otras funciones de orientación a objetos, como clases o herencia, más bien son costosas y difíciles de seguir
    • La herencia al final no es más que una forma de composición de vtables. Una clase tampoco es más que la combinación de una vtable con variables de ámbito
    • En C, hacer cast usando un struct como primer miembro hace que la herencia de campos resulte más natural de lo que parece
    • Una vtable normalmente contiene funciones que reciben un puntero this. El ejemplo de struct file_operations tiene punteros a funciones que no reciben this, así que cuesta verlo como una vtable real
  • Hay quien crea wrappers inline para las funciones de la vtable, de modo que en vez de thing->vtable->foo(thing, ...) se pueda escribir foo(thing, ...)
  • Siempre se ha preguntado por qué este patrón no se incluye en el nuevo estándar de C. Claramente mucha gente está implementando una y otra vez el mismo patrón
    • Si se agrega azúcar sintáctica, entonces tendría que coexistir una forma oficialmente permitida con algún fallback que se sienta incompleto. Una ventaja de C es que no oculta la complejidad dinámica. Cuando ocurre despacho dinámico, siempre queda claro. Ya hay muchos lenguajes que formalizan esto, pero una fortaleza propia de C es precisamente que la complejidad queda expuesta. Por eso se termina usando solo cuando de verdad hace falta despacho dinámico. Además, la sintaxis tampoco es tan difícil
    • Al parecer, High C Compiler intentó algo en esa dirección hasta cierto punto
  • Desde una experiencia muy fuerte, alguien aconseja no usar nunca este patrón. Tuvo la pesadilla de mantener código grande estructurado de esta manera. La legibilidad era terrible, el compilador no podía optimizar llamadas hechas por punteros, las herramientas no daban ningún soporte, la sintaxis era incómoda y la gente nueva tenía que dominar casi por completo el interior de un compilador de C++ solo para poder leer el código. Sobre todo, frente a los beneficios dudosos de introducir OOP, esto puede arruinar el mantenimiento a largo plazo. Si de verdad hace falta, mejor usar C++
    • Cuando le preguntan qué fue exactamente lo tan terrible, responde que le parece que menos azúcar sintáctica incluso mejora la legibilidad porque deja claro cuándo una llamada es despacho dinámico. Así se puede limitar su uso solo a los casos necesarios. Además, recuerda haber visto un blog que decía que el código dinámico en C es más fácil de optimizar porque tiene menos punteros a función. No se trata de reimplementar un compilador de C++ completo, sino simplemente de entender la esencia de OOP para poder implementarlo de forma natural. Y sobre la idea de "no convertir C en un C++ improvisado", dice que esto precisamente le parece una forma muy propia de C y una manera sencilla de introducir dinamismo justo donde se necesita.