
A benefit of working on your own operating system is that you’re free from the usual "restraints" of collaboration and real applications. That has always been a major factor in my interest in osdev. You don’t have to worry about releasing your program, or about critical security vulnerabilities, or about hundreds of people having to maintain your code.
In the OSDev world you’re usually alone, and that solitude gives you the freedom to experiment with unusual programming patterns.
While developing a kernel module for my master’s thesis I came across an article on LWN: “Object-oriented design patterns in the kernel.” The article describes how the Linux kernel, despite being written in C, embraces object-oriented principles by using function pointers in structures to achieve polymorphism.
It was fascinating to see how something as low-level as the kernel can still borrow the benefits of object orientation "encapsulation", modularity, and extensibility. This lead me to experimenting with implementing all my kernels services with this approach.
The basic idea is to have a "vtable" as a struct with function pointers. Describing the interface for the object.
/* "Interface" with function pointers */ struct device_ops { void (*start)(void); void (*stop)(void); };
The device in this case will hold a reference to this vtable.
/* Device struct holding a pointer to its ops */ struct device { const char *name; const struct device_ops *ops; };
Different type of devices can now utilize the same 'api', while calling very different functions.
struct device netdev = { "eth0", &net_ops }; struct device disk = { "sda", &disk_ops }; netdev.ops->start(); // net: up disk.ops->start(); // disk: spinning netdev.ops->stop(); // net: down disk.ops->stop(); // disk: stopped
What makes vtables especially powerful is that they can be swapped out at runtime. The caller doesn’t need to change anything, once the vtable is updated, every call is automatically redirected to the new function. With proper synchronization, this provides a very clean way of evolving behavior on the fly.
A service in my operating system consists of a set of operations, start, restart and stop and a PID referencing the running thread. The operations will vary a lot between the different types of services, but as the interface stays the same, the code interacting with the services will have it very easy.
/* "Interface" with function pointers */ struct service_ops { void (*start)(void); void (*stop)(void); void (*restart)(void); }; struct service { pid_t pid; const struct service_ops *ops; ... };
Another place where I’ve leaned on vtables in my OS is the scheduler. There are many different strategies for scheduling processes, round robin, shortest job next, FIFO, priority scheduling, and so on. But the interface really only needs a handful of operations: yield, block, add, and next. By defining that interface through a vtable, I can swap out the entire scheduling policy at runtime without touching the rest of the kernel.
Example from Linux: include/linux/fs.h
struct file_operations { struct module *owner; fop_flags_t fop_flags; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ... } __randomize_layout;
A classic example of this pattern is the file abstraction. Systems like Unix and Plan 9 embrace the philosophy that “everything is a file.” Whether you’re dealing with sockets, devices, or plain text files, they all expose the same simple interface: read and write. Behind the scenes the implementation varies enormously, but the uniform contract makes the complexity disappear. You can read from a pipe just as easily as you can read from a disk file, and user-space code doesn’t have to care which one it’s talking to. That consistency is what makes the abstraction so powerful.
Combination with Kernel Modules
object->ops->start(object)
Having to pass the object explicitly every time feels clunky, especially compared to C++ where this is implicit. The function signatures also become more verbose:
static void object_start(struct object* this){ this->id = ... ... }
That said, I’ve come to see a benefit here as well. By requiring this explicitly, the function’s dependencies are clearer! It’s obvious which context is being operated on. It makes the coupling between the object and its operations more transparent, which in kernel code is often a good trade-off.
In the end, vtables give me a simple way to keep my kernel code flexible without adding a lot of extra complexity. They let me swap out behavior at runtime, keep a consistent interface across very different subsystems, and add new features without rewriting everything.
Most importantly, it has made me use C in a new way and given me a playground for experimentation, and that’s what makes OS development so much fun!
Further reading:
The xine project discusses an interesting approach to private variables with the same vtable approach as discussed here.