Note that this is using interfaces (i.e. vtables, records of function pointers), not full object-orientation. Other OO characteristics, like classes and inheritance, have much more baggage, and are often not worth the associated pain.
> Having to pass the object explicitly every time feels clunky, especially compared to C++ where this is implicit.
I personally don't like implicit this. You are very much passing a this instance around, as opposed to a class method. Also explicit this eliminates the problem, that you don't know if the variable is an instance variable or a global/from somewhere else.
> 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.
This technique predates object oriented programming. It is called an abstract data type or data abstraction. A key difference between data abstraction and object oriented programming is that you can leave functions unimplemented in your abstract data type while OOP requires that the functions always be implemented.
The sanest way to have optional functions in object oriented programming that occurs to me would be to have an additional class for each optional function and inherit each one you implement alongside your base class via multiple inheritance. Then you would need to check at runtime whether the object is an instance of the additional class before using an optional function. With an abstract data type, you would just be do a simple NULL check to see if the function pointer is present before using it.
Where not only is it explicit, but you need to specify the object twice (once to resolve the Vtable, and a second time to pass the object to the stateless C method implementation).
> Also explicit this eliminates the problem, that you don't know if the variable is an instance variable or a global/from somewhere else.
People typically use some kind of naming convention for their member variables, e.g. mFoo, m_Foo, m_foo, foo_, etc., so that's not an issue. I find `foo_` much more concise than `this->foo`. Also note that you can use explicity this in C++ if you really want to.
Actually I would say that it is sad that developers learn a specific way how a technology is done in language XYZ and then use it as template everywhere else, what happened to the curiosity of learning?
I largely agree, and use these patterns in C, but you’re neglecting the usual approach of having a default or stub implementation in the base for classic OOP. There’s also the option of using interfaces in more modern OOP or concept-style languages where you can cast to an interface type to only require the subset of the API you actually need to call. Go is a good example of this, in fact doing the lookup at runtime from effectively a table of function pointers like this.
The concept of abstract data type is a real idea in the days of compiler design. You might as well say "compiler design predates object oriented programming". The technique described in the lead is used to implement object-oriented programming structures, just as it says. So are lots of compiler design features under the hood.
source- I wrote a windowing framework for MacOS using this pattern and others, in C with MetroWerks at the time.
My point is that this pattern is not object oriented programming. As for a default behavior with it, you usually would do that by either always adding the default pointer when creating the structure or calling the default whenever the pointer is NULL.
In the Linux VFS for example, there are optimized functions for reading and writing, but if those are not implemented, a fallback to unoptimized functions is done at the call sites. Both sets are function pointers and you only need to implement one if I recall correctly.
To be fair, OOP is not 100% absolutely perfectly defined. Strustrup swears C++ is OOP, Alan Key, at least at some point laughed at C++, and people using CLOS have yet another definition
In this paper they are known as plexes, eventually ML and CLU will show similar approaches as well.
Only much latter would Lisps evolve from plain lists and cons cells.
Listen to this article (with local TTS)
My scheduler operations implementation
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.
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.
Examples from my OS
I’ve used this pattern to implement the idea of “services” in my operating system. Services are the key kernel threads that keep the system going: the networking manager, worker pools, the window server, and so on. I wanted a consistent way to start, stop, and restart these threads interactively from the terminal, without having to hard-code special logic for each one.
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);
};
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.
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
**
This approach also pairs nicely with kernel modules. Much like how Linux modules work, custom drivers or hooks can be loaded dynamically in my system by replacing the vtables of certain structures. It’s a neat way to extend the kernel while it’s running, without recompiling or rebooting.
DrawbacksOf course, there are drawbacks. The biggest one for me is the syntax. Invoking operations through these structs often ends up looking like:
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:
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.