|
|
Subscribe / Log in / New account

Object-oriented design patterns in the kernel, part 1

LWN.net needs you!

Without subscribers, LWN would simply not exist. Please consider signing up for a subscription and helping to keep LWN publishing

June 1, 2011

This article was contributed by Neil Brown

Despite the fact that the Linux Kernel is mostly written in C, it makes broad use of some techniques from the field of object-oriented programming. Developers wanting to use these object-oriented techniques receive little support or guidance from the language and so are left to fend for themselves. As is often the case, this is a double-edged sword. The developer has enough flexibility to do really cool things, and equally the flexibility to do really stupid things, and it isn't always clear at first glance which is which, or more accurately: where on the spectrum a particular approach sits.

Instead of looking to the language to provide guidance, a software engineer must look to established practice to find out what works well and what is best avoided. Interpreting established practice is not always as easy as one might like and the effort, once made, is worth preserving. To preserve that effort on your author's part, this article brings another installment in an occasional series on Linux Kernel Design Patterns and attempts to set out - with examples - the design patterns in the Linux Kernel which effect an object-oriented style of programming.

Rather than providing a brief introduction to the object-oriented style, tempting though that is, we will assume the reader has a basic knowledge of objects, classes, methods, inheritance, and similar terms. For those as yet unfamiliar with these, there are plenty of resources to be found elsewhere on the web.

Over two weeks we will look for patterns in just two areas: method dispatch and data inheritance. Despite their apparent simplicity they lead to some rich veins for investigation. This first article will focus on method dispatch.

Method Dispatch

The large variety of styles of inheritance and rules for its usage in languages today seems to suggest that there is no uniform understanding of what "object-oriented" really means. The term is a bit like "love": everyone thinks they know what it means but when you get down to details people can find they have very different ideas. While what it means to be "oriented" might not be clear, what we mean by an "object" does seem to be uniformly agreed upon. It is simply an abstraction comprising both state and behavior. An object is like a record (Pascal) or struct (C), except that some of the names of members refer to functions which act on the other fields in the object. These function members are sometimes referred to a "methods".

The most obvious way to implement objects in C is to declare a "struct" where some fields are pointers to functions which take a pointer to the struct itself as their first argument. The calling convention for method "foo" in object "bar" would simply be: bar->foo(bar, ...args); While this pattern is used in the Linux kernel it is not the dominant pattern so we will leave discussion of it until a little later.

As methods (unlike state) are not normally changed on a per-object basis, a more common and only slightly less obvious approach is to collect all the methods for a particular class of objects into a separate structure, sometimes known as a "virtual function table" or vtable. The object then has a single pointer to this table rather than a separate pointer for each method, and consequently uses less memory.

This then leads to our first pattern - a pure vtable being a structure which contains only function pointers where the first argument of each is a pointer to some other structure (the object type) which itself contains a pointer to this vtable. Some simple examples of this in the Linux kernel are the file_lock_operations structure which contains two function pointers each of which take a pointer to a struct file_lock, and the seq_operations vtable which contains four function pointers which each operate on a struct seq_file. These two examples display an obvious naming pattern - the structure holding a vtable is named for the structure holding the object (possibly abbreviated) followed by "_operations". While this pattern is common it is by no means universal. Around the time of 2.6.39 there are approximately 30 "*_operations" structures along with well over 100 "*_ops" structures, most if not all of which are vtables of some sort. There are also several structs such as struct mdk_personality which are essentially vtables but do not have particularly helpful names.

Among these nearly 200 vtable structures there is plenty of variability and so plenty of scope to look for interesting patterns. In particular we can look for common variations from the "pure vtable" pattern described above and determine how these variations contribute to our understanding of object use in Linux.

NULL function pointers

The first observation is that some function pointers in some vtables are allowed to be NULL. Clearly trying to call such a function would be futile, so the code that calls into these methods generally contains an explicit test for the pointer being NULL. There are a few different reasons for these NULL pointers. Probably easiest to justify is the incremental development reason. Because of the way vtable structures are initialized, adding a new function pointer to the structure definition causes all existing table declarations to initialise that pointer to NULL. Thus it is possible to add a caller of the new method before any instance supports that method, and have it check for NULL and perform a default behavior. Then as incremental development continues those vtable instances which need it can get non-default methods.

A recent example is commit 77af1b2641faf4 adding set_voltage_time_sel() to struct regulator_ops which acts on struct regulator_dev. Subsequent commit 42ab616afe8844 defines that method for a particular device. This is simply the most recent example of a very common theme.

Another common reason is that certain methods are not particularly meaningful in certain cases so the calling code simply tests for NULL and returns an appropriate error when found. There are multiple examples of this in the virtual filesystem (VFS) layer. For instance, the create() function in inode_operations is only meaningful if the inode in question is a directory. So inode_operations structures for non-directories typically have NULL for the create() function (and many others) and the calling code in vfs_create() checks for NULL and returns -EACCES.

A final reason that vtables sometimes contain NULL is that an element of functionality might be being transitioned from one interface to another. A good example of this is the ioctl() operation in file_operations. In 2.6.11, a new method, unlocked_ioctl() was added which was called without the big kernel lock held. In 2.6.36, when all drivers and filesystems had been converted to use unlocked_ioctl(), the original ioctl() was finally removed. During this transition a file system would typically define only one of two, leaving the other defaulting to NULL.

A slightly more subtle example of this is read() and aio_read(), also in file_operations, and the corresponding write() and aio_write(). aio_read() was introduced to support asynchronous IO, and if it is provided the regular synchronous read() is not needed (it is effected using do_sync_read() which calls the aio_read() method). In this case there appears to be no intention of ever removing read() - it will remain for cases where async IO is not relevant such as special filesystems like procfs and sysfs. So it is still the case that only one of each pair need be defined by a filesystem, but it is not simply a transition, it is a long-term state.

Though there seem to be several different reasons for a NULL function pointer, almost every case is an example of one simple pattern - that of providing a default implementation for the method. In the "incremental development" examples and the non-meaningful method case, this is fairly straightforward. e.g. the default for inode->create() is simply to return an error. In the interface transition case it is only slightly less obvious. The default for unlocked_ioctl() would be to take the kernel lock and then call the ioctl() method. The default for read() is exactly do_sync_read() and some filesystems such as ext3 actually provide this value explicitly rather than using "NULL" to indicate a default.

With that in mind, a little reflection suggests that if the real goal is to provide a default, then maybe the best approach would be to explicitly give a default rather than using the circuitous route of using a default of NULL and interpreting it specially.

While NULL is certainly the easiest value to provide as a default - as the C standard assures us that uninitialized members of a structure do get set to NULL - it is not very much harder to set a more meaningful default. I am indebted to LWN reader wahern for the observation that C99 allows fields in a structure to be initialized multiple times with only the final value taking effect and that this allows easy setting of default values such as by following the simple model:

    #define FOO_DEFAULTS  .bar = default_bar, .baz = default_baz
    struct foo_operations my_foo = { FOO_DEFAULTS,
	.bar = my_bar,
    };

This will declare my_foo with a predefined default value for baz and a localized value for bar. Thus for the small cost of defining a few "default" functions and including a "_DEFAULTS" entry to each declaration, the default value for any field can easily be chosen when the field is first created, and automatically included in every use of the structure.

Not only are meaningful defaults easy to implement, they can lead to a more efficient implementation. In those cases where the function pointer actually is NULL it is probably faster to test and branch rather than to make an indirect function call. However the NULL case is very often the exception rather than the rule, and optimizing for an exception is not normal practice. In the more common case when the function pointer is not NULL, the test for NULL is simply a waste of code space and a waste of execution time. If we disallow NULLs we can make all call sites a little bit smaller and simpler.

In general, any testing performed by the caller before calling a method can be seen as an instance of the "mid-layer mistake" discussed in a previous article. It shows that the mid-layer is making assumptions about the behavior of the lower level driver rather than simply giving the driver freedom to behave in whatever way is most suitable. This may not always be an expensive mistake, but it is still best avoided where possible. Nevertheless there is a clear pattern in the Linux kernel that pointers in vtables can sometimes be NULLable, typically though not always to enable a transition, and the call sites should in these cases test for NULL before proceeding with the call.

The observant reader will have noticed a hole in the above logic denouncing the use NULL pointers for defaults. In the case where the default is the common case and where performance is paramount, the reasoning does not hold and a NULL pointer could well be justified. Naturally the Linux kernel provides an example of such a case for our examination.

One of the data structures used by the VFS for caching filesystem information is the "dentry". A "dentry" represents a name in the filesystem, and so each "dentry" has a parent, being the directory containing it, and an "inode" representing the named file. The dentry is separate from the inode because a single file can have multiple names (so an "inode" can have multiple "dentry"s). There is a dentry_operations vtable with a number of operations including, for example, "d_compare" which will compare two names and "d_hash" which will generate a hash for the name to guide the storage of the "dentry" in a hash table. Most filesystems do not need this flexibility. They treat names as uninterpreted strings of bytes so the default compare and hash functions are the common case. A few filesystems define these to handle case-insensitive names but that is not the norm.

Further, filename lookup is a common operation in Linux and so optimizing it is a priority. Thus these two operations appear to be good candidates where a test for NULL and an inlined default operation might be appropriate. What we find though is that when such an optimization is warranted it is not by itself enough. The code that calls d_compare() and d_hash() (and a couple of other dentry operations) does not test these functions for NULL directly. Rather they require that a few flag bits (DCACHE_OP_HASH, DCACHE_OP_COMPARE) in the "dentry" are set up to indicate whether the common default should be used, or whether the function should be called. As the flag field is likely to be in cache anyway, and the dentry_operations structure will often be not needed at all, this avoids a memory fetch in a hot path.

So we find that the one case where using a NULL function pointer to indicate a default could be justified, it is not actually used; instead, a different, more efficient, mechanism is used to indicate that the default method is requested.

Members other than function pointers

While most vtable-like structures in the kernel contain exclusively function pointers, there are a significant minority that have non-function-pointer fields. Many of these appear on the surface quite arbitrary and a few closer inspections suggest that some of them result of poor design or bit-rot and their removal would only improve the code.

There is one exception to the "functions only" pattern that occurs repeatedly and provides real value, and so is worth exploring. This pattern is seen in its most general form in struct mdk_personality which provides operations for a particular software RAID level. In particular this structure contains an "owner", a "name", and a "list". The "owner" is the module that provides the implementation. The "name" is a simple identifier: some vtables have string names, some have numeric names, and it is often called something different like "version", "family", "drvname", or "level". But conceptually it is still a name. In the present example there are two names, a string and a numeric "level".

The "list", while part of the same functionality, is less common. The mdk_personality structure has a struct list_head, as does struct ts_ops. struct file_system_type has a simple pointer to the next struct file_system_type. The underlying idea here is that for any particular implementation of an interface (or "final" definition of a class) to be usable, it must be registered in some way so that it can be found. Further, once it has been found it must be possible to ensure that the module holding the implementation is not removed while it is in use.

There seem to be nearly as many styles of registration against an interface in Linux as there are interfaces to register against, so finding strong patterns there would be a difficult task. However it is fairly common for a "vtable" to be treated as the primary handle on a particular implementation of an interface and to have an "owner" pointer which can be used to get a reference on the module which provides the implementation.

So the pattern we find here is that a structure of function pointers used as a "vtable" for object method dispatch should normally contain only function pointers. Exceptions require clear justification. A common exception allows a module pointer and possible other fields such as a name and a list pointer. These fields are used to support the registration protocol for the particular interface. When there is no list pointer it is very likely that the entire vtable will be treated as read-only. In this case the vtable will often be declared as a const structure and so could even be stored in read-only memory.

Combining Methods for different objects

A final common deviation from the "pure vtable" pattern that we see in the Linux kernel occurs when the first argument to the function is not always the same object type. In a pure vtable which is referenced by a pointer in a particular data structure, the first argument of each function is exactly that data structure. What reason could there be for deviating from that pattern? It turns out that there are few, some more interesting than others.

The simplest and least interesting explanation is that, for no apparent reason, the target data structure is listed elsewhere in the argument list. For example all functions in struct fb_ops take a struct fb_info. While in 18 cases that structure is the first argument, in five cases it is the last. There is nothing obviously wrong with this choice and it is unlikely to confuse developers. It is only a problem for data miners like your author who need to filter it out as an irrelevant pattern.

A slight deviation on this pattern is seen in struct rfkill_ops where two functions take a struct rkfill but the third - set_block() - takes a void *data. Further investigation shows that this opaque data is exactly that which is stored in rfkill->data, so set_block() could easily be defined to take a struct rfkill and simply to follow the ->data link itself. This deviation is sufficiently non-obvious that it could conceivably confuse developers as well as data miners and so should be avoided.

The next deviation in seen for example in platform_suspend_ops, oprofile_operations, security_operations and a few others. These take an odd assortment of arguments with no obvious pattern. However these are really very different sorts of vtable structures in that the object they belong to are singletons. There is only one active platform, only one profiler, only one security policy. Thus the "object" on which these operations act is part of the global state and so does not need to be included in the arguments of any functions.

Having filtered these two patterns out as not being very interesting we are left with two that do serve to tell us something about object use in the kernel.

quota_format_ops and export_operations are two different operations structures that operate on a variety of different data structures. In each case the apparent primary object (e.g. a struct super_block or a struct dentry) already has a vtable structure dedicated to it (such as super_operations or dentry_operations) and these new structures add new operations. In each case the new operations form a cohesive unit providing a related set of functionality - whether supporting disk quotas or NFS export. They don't all act on the same object simply because the functionality in question depends on a variety of objects.

The best term from the language of object-oriented programming for this is probably the "mixin". Though the fit may not be perfect - depending on what your exact understanding of mixin is - the idea of bringing in a collection of functionality without using strict hierarchical inheritance is very close to the purpose of quota_format_ops and export_operations.

Once we know to be on the lookout for mixins like these we can find quite a few more examples. The pattern to be alert for is not the one that led us here - an operations structure that operates on a variety of different objects - but rather the one we found where the functions in an "operations" structure operate on objects that already have their own "operations" structure. When an object has a large number of operations that are relevant and these operations naturally group into subsets, it makes a lot of sense to divide them into separate vtable-like structures. There are several examples of this in the networking code where for instance both tcp_congestion_ops and inet_connection_sock_af_ops operate (primarily) on a struct sock, which itself has already got a small set of dedicated operations.

So the pattern of a "mixin" - at least as defined as a set of operations which apply to one or more objects without being the primary operations for those objects - is a pattern that is often found in the kernel and appears to be quite valuable in allowing better modularization of code.

The last pattern which explains non-uniform function targets is probably the most interesting, particularly in its contrast to the obvious application of object-oriented programming style. Examples of this pattern abound with ata_port_operations, tty_operations, nfs_rpc_ops and atmdev_ops all appearing as useful examples. However we will focus primarily on some examples from the filesystem layer, particularly super_operations and inode_operations.

There is a strong hierarchy of objects in the implementation of a filesystem where the filesystem - represented by a "super_block" - has a number of files (struct inode) which may have a number of names or links (struct dentry). Further each file might store data in the page cache (struct address_space) which comprises a number of individual pages (struct page). There is a sense in which all of these different objects belong to the filesystem as a whole. If a page needs to be loaded with data from a file, the filesystem knows how to do that, and it is probably the same mechanism for every page in every file. Where it isn't always the same, the filesystem knows that too. So we could conceivably store every operation on every one of these objects in the struct super_block, as it represents the filesystem and could know what to do in each case.

In practice that extreme is not really helpful. It is quite likely that while there are similarities between the storage of a regular file and a directory, there are also important differences and being able to encode those differences in separate vtables can be helpful. Sometimes small symbolic links are stored directly in the inode while larger links are stored like the contents of a regular file. Having different readlink() operations for the two cases can make the code a lot more readable.

While the extreme of every operation attached to the one central structure is not ideal, it is equally true that the opposite extreme is not ideal either. The struct page in Linux does not have a vtable pointer at all - in part because we want to keep the structure as small as possible because it is so populous. Rather the address_space_operations structure contains the operations that act on a page. Similarly the super_operations structure contains some operations that apply to inodes, and inode_operations contains some operations that apply to dentries.

It is clearly possible to have operations structures attached to a parent of the target object - providing the target holds a reference to the parent, which it normally does - though it is not quite so clear that it is always beneficial. In the case of struct page which avoids having a vtable pointer altogether the benefit is clear. In the case of struct inode which has its own vtable pointer, the benefit of having some operations (such as destroy_inode() or write_inode()) attached to the super_block is less clear.

As there are several vtable structures where any given function pointer could be stored, the actual choice is in many cases little more than historical accident. Certainly the proliferation of struct dentry operations in inode_operations seems to be largely due to the fact that some of them used to act directly on the inode, but changes in the VFS eventually required this to change. For example in 2.1.78-pre1, each of link(), readlink(), followlink() (and some others which are now defunct) were changed from taking a struct inode to take a struct dentry instead. This set the scene for "dentry" operations to be in inode_operations, so when setattr and getattr were added for 2.3.48, it probably seemed completely natural to include them in inode_operations despite the fact that they acted primarily on a dentry.

Possibly we could simplify things by getting rid of dentry_operations altogether. Some operations that act on dentries are already in inode_operations and super_operations - why not move them all there? While dentries are not as populous as struct page there are still a lot of them and removing the "d_op" field could save 5% of the memory used by that structure (on x86-64).

With two exceptions, every active filesystem only has a single dentry operations structure in effect. Some filesystem implementations like "vfat" define two - e.g. one with case-sensitive matching and one with case-insensitive matching - but there is only one active per super-block. So it would seem that the operations in dentry_operations could be moved to super_operations, or at least accessed through "s_d_op". The two exceptions are ceph and procfs. These filesystems use different d_revalidate() operations in different parts of the filesystem and - in the case of procfs - different d_release() operations. The necessary distinctions could easily be made in per-superblock versions of these operations. Do these cases justify the 5% space cost? Arguably not.

Directly embedded function pointers

Finally it is appropriate to reflect on the alternate pattern mentioned at the start, where function pointers are stored directly in the object rather than in a separate vtable structure. This pattern can be seen in struct request_queue which has nine function pointers, struct efi which has ten function pointers, and struct sock which has six function pointers.

The cost of embedded pointers is obviously space. When vtables are used, there is only one copy of the vtable and multiple copies of an object (in most cases) so if more than one function pointer is needed, a vtable would save space. The cost of a vtable is an extra memory reference, though cache might reduce much of this cost in some cases. A vtable also has a cost of flexibility. When each object needs exactly the same set of operations a vtable is good, but if there is a need to individually tailor some of the operations for each object, then embedded function pointer can provide that flexibility. This is illustrated quite nicely by the comment with "zoom_video" in struct pcmcia_socket

	/* Zoom video behaviour is so chip specific its not worth adding
	   this to _ops */

So where objects are not very populous, where the list of function pointers is small, and where multiple mixins are needed, embedded function pointers are used instead of a separate vtable.

Method Dispatch Summary

If we combine all the pattern elements that we have found in Linux we find that:

Method pointers that operate on a particular type of object are normally collected in a vtable associated directly with that object, though they can also appear:

  • In a mixin vtable that collects related functionality which may be selectable independently of the base type of the object.

  • In the vtable for a "parent" object when doing so avoids the need for a vtable pointer in a populous object

  • Directly in the object when there are few method pointers, or they need to be individually tailored to the particular object.

These vtables rarely contain anything other than function pointers, though fields needed to register the object class can be appropriate. Allowing these function pointers to be NULL is a common but not necessarily ideal technique for handling defaults.

So in exploring the Linux Kernel code we have found that even though it is not written in an object-oriented language, it certainly contains objects, classes (represented as vtables), and even mixins. It also contains concepts not normally found in object-oriented languages such as delegating object methods to a "parent" object.

Hopefully understanding these different patterns and the reasons for choosing between them can lead to more uniform application of the patterns across the kernel, and hence make it easier for a newcomer to understand which pattern is being followed. In the second part of our examination of object oriented patterns we will explore the various ways that data inheritance is achieved in the Linux kernel and discuss the strengths and weaknesses of each approach so as to see where each is most appropriate.


Index entries for this article
KernelDevelopment model/Patterns
GuestArticlesBrown, Neil


(Log in to post comments)

Object-oriented design patterns in the kernel, part 1

Posted Jun 1, 2011 15:35 UTC (Wed) by mb (subscriber, #50428) [Link]

> as the C standard assures us that uninitialized members of a structure do get set to NULL

Is that really true? Does the _standard_ assure it, or is it just an implementation detail that all platforms we care about do?
Also, does the C standard say something about .bss (uninitialized global variables)?

Object-oriented design patterns in the kernel, part 1

Posted Jun 1, 2011 15:58 UTC (Wed) by JoeBuck (subscriber, #2330) [Link]

C guarantees that static and global pointers are initialized to null, but local (stack) pointers are uninitialized, and pointers allocated from the heap aren't initialized unless the allocation function explicitly does it.

Object-oriented design patterns in the kernel, part 1

Posted Jun 1, 2011 16:01 UTC (Wed) by JohnLenz (guest, #42089) [Link]

Expanding on Joe Buck's comment, the C99 standard says the following in section 6.7.8

Except where explicitly stated otherwise, for the purposes of this subclause unnamed members of objects of structure and union type do not participate in initialization. Unnamed members of structure objects have indeterminate value even after initialization.

If an object that has automatic storage duration is not initialized explicitly, its value is indeterminate. If an object that has static storage duration is not initialized explicitly, then:

  • if it has pointer type, it is initialized to a null pointer;
  • if it has arithmetic type, it is initialized to (positive or unsigned) zero;
  • if it is an aggregate, every member is initialized (recursively) according to these rules;
  • if it is a union, the first named member is initialized (recursively) according to these rules.

Object-oriented design patterns in the kernel, part 1

Posted Jun 1, 2011 16:12 UTC (Wed) by nbourdau (guest, #68532) [Link]

Actually the standard guarantees that if an automatic variable is initialized by an initializer list, any subobjects (for example members for a structure) not explicitly initialized "shall be initialized implicitly the same as objects that have static storage duration".

So assuming you have some declaration like:
struct dummy {
int a;
char* p;
long b;
};

if you have in a function :
struct dummy smth = {.b = 0};

You are ensured by the standard that smth.p == NULL and smth.a == 0.

Object-oriented design patterns in the kernel, part 1

Posted Jun 1, 2011 16:58 UTC (Wed) by hummassa (guest, #307) [Link]

[citation needed]

Object-oriented design patterns in the kernel, part 1

Posted Jun 1, 2011 18:31 UTC (Wed) by juliank (guest, #45896) [Link]

WG14/N1256 Committee Draft — Septermber 7, 2007 ISO/IEC 9899:TC3

6.7.8 Initialization
19 The initialization shall occur in initializer list order,each initializer provided for a particular subobject overriding any previously listed initializer for the same subobject; all subobjects that are not initialized explicitly shall be initialized implicitly the same as objects that have static storage duration.

Object-oriented design patterns in the kernel, part 1

Posted Jun 1, 2011 20:15 UTC (Wed) by arjan (subscriber, #36785) [Link]

one gotcha; if your structure contains padding (due to alignment requirements of variable types).... it's not uncommon that this padding does not get initialized. In userspace this is often not a big deal.. but if you get this in the kernel you may accidentally leak kernel stack data to userspace....

Object-oriented design patterns in the kernel, part 1

Posted Jun 1, 2011 21:15 UTC (Wed) by pr1268 (subscriber, #24648) [Link]

Interesting... Just curious, has this been observed thus far? If so, has it been addressed (patch, git commit, etc.)?

Based on the standard, I'm unsure whether the padding you mention needs to be initialized. Assuming it does, then is the leaked stack data a compiler bug? Or, if it doesn't, then does this fall into the category of "making the compiler more efficient"?

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 0:26 UTC (Thu) by elanthis (guest, #6227) [Link]

I don't have a link, but it's a real problem that's been observed in the wild.

It's not a compiler bug by any means. It's correct, normal, expected behavior. There's no reason to initialize padding space in most circumstances, and doing so is just a loss of efficiency for no gain for those overwhelmingly common cases.

The only cases where this ever comes up as a problem are in security contexts (e.g. kernel stack data leakage) and when you're trying to do "undefined" operations like hashing a chunk of memory representing a struct. In the former case, you should be more careful and explicitly zero out memory via memset or calloc (or equivalent APIs) which you need to do anything for just about every other data structure you use so it's not a big deal, and in the later case you should be beaten with a stick until you stop doing such stupid things. :)

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 0:39 UTC (Thu) by foom (subscriber, #14868) [Link]

I ran into it when writing in-memory structs to disk (to be mmap'd in later). It was a problem that there was random garbage in between the data, since then it was not possible to md5sum two files and tell if they were the same.

Furthermore, gcc emits *terrible* code when explicitly initializing a bunch of integer members to 0 (especially where some of them are bitfields). My solution was to make a parent class call bzero to clear out sizeof(*this), and not explicitly initialize any of the initially-zero integer members. Made faster code *and* didn't have any padding issues. (But now someone's probably going to tell me that doing that is undefined and that gcc 4.8's optimizer is going to decide to transform my app into nethack, because I invoked some undefined behavior).

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 13:48 UTC (Thu) by cesarb (subscriber, #6266) [Link]

I believe that, unless your class has only plain old data members (the kind you would find in a C struct), doing a bzero of *this has the potential of blowing up. The compiler can add extra hidden members (like the vtable pointer), and you would be overwriting them.

That is, I believe what you are doing is safe only if:

- All the integer and bitfield members (and only them) are in the parent class;
- The parent class is zeroing only itself (that is, it is doing the sizeof(*this) itself, instead of being passed that value by the child class);
- The parent class has no virtual member functions or anything else that could make the compiler add C++ magic to it.

That said, I am no language lawyer, and would not be surprised if several clauses scattered all over the standard combine to say that even then you are still doing something undefined.

Why not move all these variables into a plain C-style struct, add it to your object as a member, and zero it on the constructor? That way sounds much safer to me, and I doubt the C++ standard would break it (since breaking it would break compatibility with C).

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 11:20 UTC (Fri) by liljencrantz (guest, #28458) [Link]

What makes you assume the parent is talking C++ and not C? Am I missing something or are you?

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 13:51 UTC (Fri) by sethml (guest, #8471) [Link]

Presumably because *this means something special in C++, and is not a common thing to write in C.

I've written this bug in C++ (memset(this, 0, sizeof(*this))) - clobbering your vtable pointer is pretty annoying to debug. My horrible hacky fix was just to zero the data portion of the class: memset(&firstMember, 0, (char *)(&lastMember + 1) - (char *)&firstMember)

Object-oriented design patterns in the kernel, part 1

Posted Jun 4, 2011 10:11 UTC (Sat) by liljencrantz (guest, #28458) [Link]

I always use the name «this» for the object pointer when doing OOP in C. From what I've seen, this is a common convention.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 17:49 UTC (Thu) by marcH (subscriber, #57642) [Link]

> It's not a compiler bug by any means. It's correct, normal, expected behavior. There's no reason to initialize padding space in most circumstances, and doing so is just a loss of efficiency for no gain for those overwhelmingly common cases.

Performance over security: a very important design choice of C, one to keep in mind at all times. One carried over to C++. One responsible for zillions of security flaws written by thousands of programmers who should rather have used another, safer language (or stayed in bed).

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 9:25 UTC (Thu) by juliank (guest, #45896) [Link]

The standard (or my draft) says, in 6.2.6 Representations of types, 6.2.6.1 General:
6 When a value is stored in an object of structure or union type, including in a member object, the bytes of the object representation that correspond to any padding bytes take unspecified values.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 17:49 UTC (Thu) by jwakely (guest, #60262) [Link]

See http://lwn.net/Articles/417989/ and the mail it links to

> Check commit 1c40be12f7d8ca1d387510d39787b12e512a7ce8 for an example
> (net sched: fix some kernel memory leaks)

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 13:05 UTC (Thu) by pixelbeat (guest, #7440) [Link]

I think the standard says the whole object is initialized as it would be in static storage.
I.E. the padding is zeroed too.

I previously did some analysis to verify.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 13:08 UTC (Thu) by juliank (guest, #45896) [Link]

> I.E. the padding is zeroed too.
It's not, read my other comment.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 14:30 UTC (Thu) by pixelbeat (guest, #7440) [Link]

Well by my analysis in the above link, GCC does init the padding to zero. I.E. it seems GCC interprets 6.7.8 to trump 6.2.6.
I'd be very interested if there was a counter example in the wild

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 16:19 UTC (Thu) by juliank (guest, #45896) [Link]

> Well by my analysis in the above link, GCC does init the padding to zero.
Did you actually enable optimizations when testing?

> I'd be very interested if there was a counter example in the wild

https://lwn.net/Articles/417989/
https://lwn.net/Articles/417994/

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 1:59 UTC (Fri) by pixelbeat (guest, #7440) [Link]

> Did you actually enable optimizations when testing?
That is immaterial as optimizations may zero the padding.
The interesting case to test is the non optimized case.

> https://lwn.net/Articles/417989/
> https://lwn.net/Articles/417994/
They concur with my testing I think.
I.E. only in the case where all members are specified
in the initializer list, is the padding not zeroed.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 18:00 UTC (Thu) by nix (subscriber, #2304) [Link]

Well, you could check the code, or (more practically) GCC's intermediate representations, both of which make it fairly clear that GCC emits initializations for every field independently, then subjects them all to optimization just as if you'd written them as separate statements. Sometimes (especially for large structures) this may lead to initializations being merged and blatting over gaps: very often, it does not.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 16:17 UTC (Thu) by Spudd86 (guest, #51683) [Link]

Automatic variables aren't really relevant to the case of kernel vtables, I imagine they are generally statically allocated.

Object-oriented design patterns in the kernel, part 1

Posted Jun 1, 2011 16:11 UTC (Wed) by dashesy (guest, #74652) [Link]

Very useful article, thanks. I think some inline color-coded code was a definite plus for a newbie like me but was a bookmark-worthy article nevertheless.

Object-oriented design patterns in the kernel, part 1

Posted Jun 1, 2011 20:40 UTC (Wed) by mjthayer (guest, #39183) [Link]

Surely one of the most important reasons for using a real function as a default and not a NULL pointer (see [ http://en.wikipedia.org/wiki/Null_Object_pattern ] by the way) is that having every caller check for NULL adds the potential for lots of additional typing mistakes or other errors in the caller code.

Object-oriented design patterns in the kernel, part 1

Posted Jun 1, 2011 22:32 UTC (Wed) by martinfick (subscriber, #4455) [Link]

I think that would fall under Neil's generalization that:

> In general, any testing performed by the caller before calling a method can be seen as an instance of the "mid-layer mistake" discussed in a previous article.

Object-oriented design patterns in the kernel, part 1

Posted Jun 1, 2011 21:15 UTC (Wed) by daglwn (guest, #65432) [Link]

All right, I can't resist. I have to ask, "why?" Why go through all the pain of manually setting up vtables when we have a language that will do it for you?

Yeah, yeah, I know all about Linus' biases against C++. He's flat out wrong and being a stick in the mud. One does not have to use every feature of a language to get good productivity out of the language. It seems to me simple uses of the C++ inheritance model would clean up code considerably and lead to fewer bugs and more developer hair.

Object-oriented design patterns in the kernel, part 1

Posted Jun 1, 2011 21:31 UTC (Wed) by pr1268 (subscriber, #24648) [Link]

Now I can't resist. Linus' bias against C++ notwithstanding, there are some seriously good reasons to avoid using it for kernel programming:

  • Using C++'s standard library types and features generates a lot of object code bloat. Trust me, I refactored some of my own programs from C++ back to C and got ELF objects less than half as big.
  • Generally speaking, use of higher-level language tools (such as the C++ standard library) tends to move the programmer further away from the bare metal (and kernel programming should be as close to the metal as possible).
  • The use of a single programming language tends to make all participants have a similar mindset of the technical problems being solved, and
  • Use of a low-level language where it's easy to "shoot your foot off" tends to weed out those without a thorough technical knowledge of kernel programming (Okay, I'm going into generalizations, here, but I'm sure I've read Linus mention something similar to these a while back).

Bear in mind that I'm a devout C++ programmer—I really do love to code in both C and C++. It's just that I think Linus' arguments against C++ with respect to kernel programming are justified.

Please do NOT read this as an attempt to start a language war—again I love both languages!

Object-oriented design patterns in the kernel, part 1

Posted Jun 1, 2011 22:43 UTC (Wed) by oak (guest, #2786) [Link]

C is IMHO also typically easier to debug as function names are typed in full (= easier to grep/find from large code base) and their meaning doesn't mutate with inheritance / overloading.

From reading the C++ code you cannot guess what innocent looking lines do, I've seen (bad) C++ programs that malloc() all together nearly 1MB of RAM before they even enter main(), due to there being static objects and their constructors creating innocent looking other object(s) that turn out not to be integers... (And btw. C++ ABI actually defines classes to have several constructors for different purposes, although source code has only one)

Debugging problems at HW level is already hard, making it harder by selecting too complex language doesn't help.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 0:23 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]

You can write C++ code without the stdlibc++, it's not that hard - just turn off exceptions, do not use static initializers and off you go.

It was even possible to use C++ in the Linux kernel some time ago. Unfortunately, after about 2.6.16 a lot of C++ keywords ('template', 'class', etc.) started to appear in the kernel structs so porting became difficult.

I'm using C++ in Microsoft Windows drivers (via http://code.google.com/p/ontl/ ). It's actually quite nice to use Boost.ScopeExit instead of 'goto error_exit'. And exceptions! One can actually use exceptions in Windows, even in the kernel mode. Oh and real templated containers, of course.

And advantages of C++ are really visible. Good C++ code is about 30%-50% of corresponding C code in LOCs, and much easier to read.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 2:28 UTC (Thu) by nevets (subscriber, #11875) [Link]

The problem with saying we should use a subset of C++ is that it will never happen. There's just too many contributors to enforce such a thing. There's too many places that people may use side effects of C++.

You state that good C++ is easier to read and understand. I'm saying that bad C++ code is much worse to figure out than bad C code. And unfortunately, there just happens to be a lot of bad C code in the kernel. It does get cleaned up over time. But with the huge number of contributors, crap just keeps slipping in.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 9:37 UTC (Thu) by juliank (guest, #45896) [Link]

> Good C++ code is about 30%-50% of corresponding
> C code in LOCs, and much easier to read.

Not really know. You always need to work around the C++ language with things like shared_ptr and named constructor idioms, and prevent default constructors and more to produce something useful. In C, you add a reference count field to you struct, create a new, a ref, and an unref function and you're done. Much easier.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 9:44 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]

LOL.

In C you _have_ to manually ref/unref things (and get hard-to-detect errors when you unbalance them). In C++ one just uses shared_ptr (or one of other smart pointers) to do it _automatically_ - it's not possible to unbalance shared_ptr (at least without trying hard to do it).

That's not a workaround - that's a feature, and a wonderfully nice feature at that. A couple of lines in C++ can easily replace 5-10 lines in C.

Also, why would you suppress default constructors? They work just fine, even with shared_ptrs.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 9:58 UTC (Thu) by juliank (guest, #45896) [Link]

> Also, why would you suppress default constructors?
> They work just fine, even with shared_ptrs.
You may want to restrict users to use only shared_ptr, in order to have a controlled way to reference things. Reference counting just feels completely unnatural in C++.

Another problem is ABI stability. It might be possible to get some more stability by suppressing anything default and allowing only pointers to objects to be taken. The safer option is to use d-pointers in the classes, but this is all very complicated compared to declaring an incomplete type in C.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 10:08 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]

>You may want to restrict users to use only shared_ptr, in order to have a controlled way to reference things. Reference counting just feels completely unnatural in C++.

Why? A class should not be constricting its users, unless it's necessary. If your class should be used ONLY with refcounting, then intrusive_ptr is a better solution. C++ is just fine with refcounting, just learn to use C++ correctly.

ABI stability is about on the same level as in C. Just use incomplete classes, they work EXACTLY like in C.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 10:16 UTC (Thu) by juliank (guest, #45896) [Link]

> Why? A class should not be constricting its users, unless it's necessary.
A user should never know the size of an object, unless it's necessary. C++ just does not allow me to do this in a sane way, so it's already disqualified for long-term library projects.

> If your class should be used ONLY with refcounting,
> then intrusive_ptr is a better solution
Not part of C++.

> C++ is just fine with refcounting, just learn to use C++ correctly.
If you like std::shared_ptr<MyObject> everywhere, then yes, it works. But it looks like an ugly peace of shit when compared to a reference counting C library.

> ABI stability is about on the same level as in C.
> Just use incomplete classes, they work EXACTLY like in C.
Call a method on an incomplete class.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 10:26 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]

>A user should never know the size of an object, unless it's necessary.
Why?? Opaque object pointers are nice for some parts of ABI, but internally they are rarely required.

>C++ just does not allow me to do this in a sane way, so it's already disqualified for long-term library projects.
C++ allows to do it EXACTLY like in C. You just use forward declarations. C++ won't allow you to use copy constructor unless you have full class definition, so no difference here as well.

Please, stop spreading FUD.

>> If your class should be used ONLY with refcounting,
>> then intrusive_ptr is a better solution
>Not part of C++.

Can you point me where reference counting is described in the C99 Standard?

>If you like std::shared_ptr<MyObject> everywhere, then yes, it works. But it looks like an ugly peace of shit when compared to a reference counting C library.

Why would you need it? If you want an opaque interface, then do it exactly like in C. If you want a _sane_ interface then provide full type definitions and let user to decide what to use (shared_ptr, auto_ptr, etc.) and/or provide built-in refcounters and generic AddRef/Release functions.

>Call a method on an incomplete class.
Pass an incomplete structure by value in C.

You can't call a method of an incomplete class (duh, it's _INCOMPLETE_). But you can pass it as a parameter to a function/method. Just like in C.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 11:12 UTC (Thu) by juliank (guest, #45896) [Link]

> Why?? Opaque object pointers are nice for some parts of ABI, > but internally they are rarely required. In user space code, at least 99% of all ABI should be opaque. There should be no inline code (and thus no templates), no complete types, the only things there should be are functions and incomplete types. Everything else is going to fail. Even in Qt, where they try harder to avoid such problems, we still have an ABI that only lasts 4 years (Qt3) or is expected to last 6 years (Qt4). In other parts of the world, we have ABI breaks in every minor release (V8), or all few months (APT). C ABIs in contrast can last much longer, as evident by the fact that GLib 2 was released in 2002, and did not have any ABI break yet. Of course, there are also bad C ABIs, such as Python's, which are allowed to break with every release, but C makes it much easier to create a consistent long-term-stable ABI than C++. > You can't call a method of an incomplete class > (duh, it's _INCOMPLETE_). But you can pass it > as a parameter to a function/method. Just like > in C. But then you're in C world again, and have no advantage of using C++. In C, the struct can be incomplete, yet I can call functions on it which can be virtual (methods) and other functions, all using one common interface, via functions, such as the following two, defined in the my_object.c.



struct MyObject {
   int some_internal_int_field;
   void (*method_a)(MyObject *o);
};

int my_object_function_a(MyObject *o)
{
   return o->some_internal_int_field;
}

void my_object_method_a(MyObject *o)
{
   o->method_a(o);
}
The external world only sees the header file:
typedef struct MyObject MyObject;

int my_object_function_a(MyObject *o)
void my_object_method_a(MyObject *o)
The language coming closest to the optimum is Vala. Private fields are not part of an objects ABI, neither is (for callees) whether a method is virtual or not.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 13:19 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]

And you can do this in C++ just as well. What's the problem? Quite a lot of C++ libraries have pure-C interface (often for the reasons of compatibility with another languages).

Then there is COM. There are COM-interfaces in Windows which are stable since _1993_ (the ones that deal with OLE) and that's kinda hard to beat. And COM maps directly into C++, COM-interfaces are just pure C++ classes. AddRef/Release can be managed using smart pointers and QueryInterface is dynamic_cast<> reimplemented manually.

Yes, designing stable C++ interfaces requires some forethought. But pure C interfaces require no less forethought.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 20:14 UTC (Thu) by pr1268 (subscriber, #24648) [Link]

I had asked that my earlier post not be construed as an attempt to start a language war, and, guess what? We now have a language war.

Sigh.

At least this one has been fairly tame. If there's anything nice about the above discussion, it's that I've learned a lot about exception-handling programming. Thanks! ;-)

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 22:24 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]

Well, I actually deeply hate C++ :) But I also can't tolerate FUD for the languages I really hate.

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 8:10 UTC (Fri) by marcH (subscriber, #57642) [Link]

Agreed, such FUD and lies are really annoying. I have very little love for C++ but anyone who claims that C++ cannot do something while C can is just making pure noise and calling to be filtered out.

Object-oriented design patterns in the kernel, part 1

Posted Jun 9, 2011 16:29 UTC (Thu) by jd (subscriber, #26381) [Link]

If we're going to have a language flame-war anyway, I propose rewriting the kernel in Occam. :)

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 21:46 UTC (Thu) by cmccabe (guest, #60281) [Link]

> The language coming closest to the optimum is Vala. Private fields are not
> part of an objects ABI, neither is (for callees) whether a method is
> virtual or not.

I'm pretty sure private fields are not part of the ABI in Java, either. Your jar files will continue to work when someone changes private fields in a different jar. It's a nice feature and a lot of modern programming languages have it.

And as you noted, C has it as well. Private stuff stays private in C (as opposed to C++.)

C.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 23:45 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]

Java has very loose ABI. It's possible to add new public methods without breaking it or even modify existing methods to some degree.

There's a nice description here: http://wiki.eclipse.org/Evolving_Java-based_APIs_2

The fact that Java bytecode is essentially a lightly-parsed source code helps immensely.

Object-oriented design patterns in the kernel, part 1

Posted Jun 8, 2011 19:22 UTC (Wed) by marcH (subscriber, #57642) [Link]

Linking at start time makes Java very flexible indeed. It is also what makes every Java program insanely slow to start up.

You cannot have your cake and eat it.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 13:16 UTC (Thu) by jond (subscriber, #37669) [Link]

Aren't exceptions one of the best reasons *not* to use C++ for such a thing? The possibility of exceptions make this pattern impossible:

void somefunction() {
do_something();
do_something_else();
important_cleanup_code();
}

More in the "Dark side of C++" talk:
http://www.fefe.de/c++/c%2B%2B-talk.pdf

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 13:37 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]

This kind of code is BAD.

For this reason:
====================
void somefunction() {
do_something();
if (do_something_else())
return; //Whoopsie!
important_cleanup_code();
}
====================

In good C++ one would write something like:
====================
void somefunction() {
ON_SCOPE_EXIT(&important_cleanup_code);

do_something();
if (do_something_else())
return; //No whoopses.
do_something_really_else();
}
====================

If you use smart pointers it's even nicer:
====================
void shared_ptr<File> somefunction() {
const std::string &name=compute_file_name();
if (some_condition(name))
return shared_ptr(new File(name, FOR_READ));
if (anotherCondition())
throw my_exception(F("Something bad happened with the file ")<<name);
return shared_ptr(new File(name, FOR_WRITE));
}

//Call site
shared_ptr<File> fl=somefunction(); //that's it!
//One can even do
somefunction(); //For side effects, for example - returned file won't be lost
====================

In pure C you'll have to do something like:
====================
void int somefunction(file_object_ptr *result_place)
{
some_string name;
int res;
name=compute_file_name();

if (some_condition(name))
{
//And pray that your numeric error codes do not collide.
res=file_open_file(name, FOR_READ, result_place);
//What if we want to pass a verbose message
//like 'file <NAME> is locked by another process'?
goto cleanup;
}
if (anotherCondition())
{
res=E_SOMETHINGBAD;
//Verbose errors? Forget about it.
goto cleanup;
}

res=file_open_file(name, FOR_WRITE, result_place);

cleanup:
free_string(name);

return res;
}

//Call site
file_object_ptr obj=0; //Don't forget to initialize!
int res=somefunction(&obj);
if (res<0) return res; //Hope...
cleanup:
delete_file(obj);
====================

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 14:20 UTC (Thu) by Trelane (subscriber, #56877) [Link]

Pardon my ignorance, but where does ON_SCOPE_EXIT come from? I'd really like it in some places. :)
Thanks!

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 15:48 UTC (Thu) by nye (guest, #51576) [Link]

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 15:56 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]

Originally, from Alexandrescu's article. I've been using it since 2001: http://drdobbs.com/cpp/184403758 . It's simple and it works everywhere (even on VisualStudio 6).

Then there's Boost.ScopeExit: http://www.boost.org/doc/libs/1_45_0/libs/scope_exit/doc/... which is a bit nicer (it even lists alternatives here: http://www.boost.org/doc/libs/1_45_0/libs/scope_exit/doc/... ).

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 19:04 UTC (Thu) by Trelane (subscriber, #56877) [Link]

Thanks for the pointers! (heh) It will make my code suck much less. :)

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 22:42 UTC (Thu) by cmccabe (guest, #60281) [Link]

Golang has the "defer" statement, which sets up some statements which will be executed when the current scope exits. So you might do something like this
> func foo() {
>     defer fmt.Println("running cleanup");
>    ...
> }
Then the printout will happen when you return from the function, whenever that is.

C, of course, has the single exit point idiom, which is used a lot in the kernel. It looks like this:

> int foo() {
>     setup_bar();
>     ...
>     if (ret)
>         goto cleanup;
>     ...
> cleanup:
>     shutdown_bar();
>     return ret;
> }
Java, Python, and Ruby have try...finally { }

I have to be honest; I don't think dumping that pile of macros and templates into your code will make it "suck less." I think you just need to use the mechanisms C++ gives you, namely try and catch, and RAII. It may be an extra line or two, but the programmers trying to read your code after you've moved on will thank you.

Especially when you're debugging something, flow control macros are not your friends. Alexandrescu may be able to figure it out in gdb, but he probably doesn't work with you.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 22:53 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]

To be fair, ON_SCOPE_EXIT is quite nice. Its functionality is very simple and restricted and it's indispensable when one needs to do something simple, usually with non-C++ resources (like calling fclose on FILE*).

It's also easy to debug. Though I tend to spend much less time in a debugger when working with C++ code, all this strict typing pays off.

C++0x has lambdas so it's possible to write analog of 'defer' from Golang (indeed, Boost already has it).

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 1:16 UTC (Fri) by Trelane (subscriber, #56877) [Link]

Thanks to both of your for the info; I am learning a lot. Yet again LWN proves to be the only place where the comments have value. :)

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 9:46 UTC (Fri) by cortana (subscriber, #24596) [Link]

FYI, you can still use shared_ptr with FILE* (and probably any other C API that follows the same pattern.

shared_ptr<FILE> f (fopen ("whatever", "r"), fclose);

Now, fclose will be called whenever the last copy of that shared_ptr is destructed. No more single exit points, goto tricks, or leaning towers of if statements!

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 12:30 UTC (Fri) by cmccabe (guest, #60281) [Link]

That's pretty good, and it relies on things that are actually in the standard library. Thanks.

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 14:19 UTC (Fri) by cortana (subscriber, #24596) [Link]

This and std::vector are the best things in C++! :)

Object-oriented design patterns in the kernel, part 1

Posted Jun 6, 2011 8:58 UTC (Mon) by mjthayer (guest, #39183) [Link]

> This kind of code is BAD.
>
> For this reason:
> ====================
> void somefunction() {
> do_something();
> if (do_something_else())
> return; //Whoopsie!
> important_cleanup_code();
> }
> ====================

In C you can get round that with the following (you don't have to like it if you don't want to!)

void somefunction_wrapper() {
somefunction_core();
important_cleanup_code();
}

void somefunction_core() {
do_something();
if (do_something_else())
return; //No whoopses.
do_something_really_else();> return; //Whoopsie!
}

> //And pray that your numeric error codes do not collide.
> res=file_open_file(name, FOR_READ, result_place);
> //What if we want to pass a verbose message
> //like 'file <NAME> is locked by another process'?

The devil's advocate in me says: my (admittedly limited) experience is that you can't do that even in C++ without thinking very carefully about error handling before you implement it - including memory management issues, particularly if you may end up handling out of memory conditions using that mechanism. And if you are going to think about it beforehand anyway you can also return a pointer to an error structure (which can potentially be statically or dynamically allocated) in C rather than just an integer error code. (That was just an example of course as there are lots of other ways to achieve the same thing.)

Object-oriented design patterns in the kernel, part 1

Posted Jun 6, 2011 14:47 UTC (Mon) by Cyberax (✭ supporter ✭, #52523) [Link]

>In C you can get round that with the following (you don't have to like it if you don't want to!)
>void somefunction_wrapper() {
>somefunction_core();
>important_cleanup_code();
>}

That's even worse. You'll have either put resources into global variables (yikes!) or pass around some kind of 'local frame' structure. Essentially reinventing exceptions.

>The devil's advocate in me says: my (admittedly limited) experience is that you can't do that even in C++ without thinking very carefully about error handling before you implement it - including memory management issues, particularly if you may end up handling out of memory conditions using that mechanism.
OOM conditions are not really worth it to handle in userspace applications.

In kernel level handling OOM would be mandatory, but not so much different from the current situation. Kernel would just set aside a small pool of RAM and use it during OOM allocations.

>And if you are going to think about it beforehand anyway you can also return a pointer to an error structure (which can potentially be statically or dynamically allocated) in C rather than just an integer error code. (That was just an example of course as there are lots of other ways to achieve the same thing.)
The problem is that it quickly becomes very cumbersome as each and every function has to pass around pointers to error structures.

Object-oriented design patterns in the kernel, part 1

Posted Jun 6, 2011 15:00 UTC (Mon) by mjthayer (guest, #39183) [Link]

>> In C you can get round that with the following (you don't have to like it if you don't want to!)
>> void somefunction_wrapper() {
>> somefunction_core();
>> important_cleanup_code();
>> }
>
> That's even worse. You'll have either put resources into global variables > (yikes!) or pass around some kind of 'local frame' structure. Essentially > reinventing exceptions.

Not quite sure what you mean there.

[...]

>> And if you are going to think about it beforehand anyway you can also return a pointer to an error structure (which can potentially be statically or dynamically allocated) in C rather than just an integer error code. (That was just an example of course as there are lots of other ways to achieve the same thing.)
> The problem is that it quickly becomes very cumbersome as each and every function has to pass around pointers to error structures.

Not really - you just return a pointer where C traditionally returns an integer error code, and pass that on if a function you call returns a non-NULL error pointer.

Object-oriented design patterns in the kernel, part 1

Posted Jun 10, 2011 13:08 UTC (Fri) by jond (subscriber, #37669) [Link]

> This kind of code is BAD.
> For this reason:
> ====================
> void somefunction() {
> do_something();
> if (do_something_else())
> return; //Whoopsie!
&#9986; &#9986; &#9986; &#9986; &#9986;

That isn't the example code I provided, at all. You've injected a return which I did not have in my code. My code, as written, will (in C) guarantee the last line to be executed if the process has not been terminated. Exceptions in C++ prevent you from making the same assertion, which is why you need RAII and/or…

> If you use smart pointers it's even nicer:

…stuff like smart pointers.

There's a good overview of using smart pointers and other techniques to ensure deterministic resource management here:

http://www.slideshare.net/eplawless/exception-safety-and-...

Object-oriented design patterns in the kernel, part 1

Posted Jun 10, 2011 13:22 UTC (Fri) by paulj (subscriber, #341) [Link]

You can use single-loops for ultra-light-weight exception handling in any C-syntax-like language:

http://bit.ly/j69nwV

Obviously the do_final_stuff pattern can be used within the do_exceptional_stuff pattern (inc via helper funcs).

Object-oriented design patterns in the kernel, part 1

Posted Jun 16, 2011 8:11 UTC (Thu) by gowen (guest, #23914) [Link]

void somefunction() {
  do_something();
  do_something_else();
  important_cleanup_code();
}

Can do_something() fail for some reason? If so, why don't you check for it? If not, why do you think you need to worry about it throwing an exception?

Under what circumstances do you imagine that C++ do_something_cpp() will throw an exception but do_something_c() silently succeed?

Object-oriented design patterns in the kernel, part 1

Posted Jun 16, 2011 9:12 UTC (Thu) by elanthis (guest, #6227) [Link]

There's big differences.

A C-style error handler can fail without bringing down the rest of the system. An exception cannot. There's also different definitions of "fail" with different levels of consequences, while an exception is always a fatal error unless you explicitly check for it.

There's the matter of exception handling logic not being able to be placed where you might want it, too. With a simplistic C error handling facility I can store an error code and check it later when it matters or is most convenient. With exceptions, I need to wrap each individual function call (and operator, etc.) in order to ensure that nothing slips past.

Operator overloading, virtual functions, etc. are all massively useful features that help an unbelievable amount in writing good code. Exceptions just don't bring any meaningful benefit to large, complex programs that need to keep operating, don't bring any readability/maintainability benefit, and pretty much only appear to be beneficial when you look at tiny little trivial error handling examples compared to very poorly written C APIs. (Operator overloading is often panned, but anyone who's ever written a game in C with C-style math libraries can tell you that operator loading opponents are full of doody; and anyone who's ever tried a math library in Python/Lua/JavaScript/Ruby/Java/C#/etc. can tell you with five times the conviction that dynamic typing or static typing without local types that C++ critics are full of doody.)

Object-oriented design patterns in the kernel, part 1

Posted Jun 16, 2011 13:46 UTC (Thu) by gowen (guest, #23914) [Link]

There's the matter of exception handling logic not being able to be placed where you might want it, too. With a simplistic C error handling facility I can store an error code and check it later when it matters or is most convenient.
Wow. I am truly staggered ... just utterly lost for words. Exceptions/RAII automatically propogate non-recoverable errors to the correct level of abstraction, error return values have to be propogated by hand.
With exceptions, I need to wrap each individual function call (and operator, etc.) in order to ensure that nothing slips past.
Holy living Christ on a bicycle... Do you really think that's how to write code that uses exceptions for error handling? If you do, please stop writing about it, because you don't know what you're talking about.

Object-oriented design patterns in the kernel, part 1

Posted Jun 1, 2011 22:57 UTC (Wed) by brianomahoney (guest, #6206) [Link]

I could not disagree with you more, the biggest problem with C++ is it is just too complicates for most programmers, and fosters cult programming styles

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 8:51 UTC (Thu) by dajoli66 (guest, #61914) [Link]

LOL, this article is an analysis of (some of) the "cult programming styles" in the current kernel! Bear in mind that some cult styles can be helpful in hiding some necessary complexity and making algorithm simplicity more clear, e.g. macros for lock;call;unlock sequences, which can lead to cluttered code (or worse, coding errors) when spelled out in full.

FWIW, the stats over at langpop.com seem to suggest that "most programmers" find C++ perfectly usable, although C still gets as much or more discussion/committers/work etc.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 12:19 UTC (Thu) by nix (subscriber, #2304) [Link]

Well, among other things the very article you're responding to provides a number of examples of doing things with vtables which C++ will *not* do automatically for you (e.g. having several different vtables referring to the same object without paying any space cost in that object).

So you'd either have to take an efficiency hit, or do a lot of this stuff manually in C++ as well, which would lead to an ugly mix of inconsistent coding styles.

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 14:58 UTC (Fri) by daglwn (guest, #65432) [Link]

I believe the variants described could be implemented in C++ via virtual functions (overriding operations for specific types), inheritance-style mixins or the Visitor pattern (both used to add operations to an existing type, the latter being more dynamic than the former).

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 17:54 UTC (Thu) by chad.netzer (subscriber, #4257) [Link]

Which C++ keywords/features, in your opinion, should one be able to use in kernel code (since we are explicitly talking about using a subset of C++). Also, how do I, as a C contributor, deal with things like someone accidentally overloading my foo(int) function with foo(char), and other such issues? Ie. Who is in charge of enforcing the correct subset of C++ on a project with such a wide range of contributors, since compiling in C++ typically gives you access to *all* of C++, not just the subset you prefer?

Btw, the inheritance model that you tout as a feature (for "simple uses") is arguably one of C++'s *worst* additions to the C language; there is a reason that no one else ever copied it (AFAIK). Where do you draw the line on how much of it to use?

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 21:01 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]

Well, other projects manage to do it just fine. For example, see: LLVM, QT, OpenOffice, FireFox, Chrome and so on.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 22:08 UTC (Thu) by chad.netzer (subscriber, #4257) [Link]

Which of those projects started as a large C codebase, and migrated to C++? I mean, that is what we are discussing, unless you are proposing redesigning the Linux kernel from scratch, using C++. Do we allow device drivers to be C++, but still use the core C kernel code? Or do we require the device drivers to be C, but redesign the core with C++? What subsystems would benefit *immediately* from being rewritten in C++? These are questions without obvious answers, imo.

I'd bet a kernel project that was started from scratch using C++, could probably *not* sustain the rate of development that Linux has seen, given the nature of it's contributor base. Those projects you mentioned all started in C++ (AFAIK), typically with a small focused team of programmers, all commercially funded (except LLVM, which had research funding initially), whereas Linux had an explosion of early volunteer contributors. And I'm not completely sure, but it seems that Linux still has the widest range of commercial and volunteer contributors compared to any of the other projects you mentioned. It's just not comparable; a C++ codebase *would* drastically affect the set of contributors.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 22:23 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]

>Do we allow device drivers to be C++, but still use the core C kernel code? Or do we require the device drivers to be C, but redesign the core with C++? What subsystems would benefit *immediately* from being rewritten in C++? These are questions without obvious answers, imo.

Any real refactoring of Linux-sized codebases MUST be gradual. Hypothetically, I'd first clean up kernel source to allow C++ drivers, introduce kernel-side C++ library and then slowly convert subsystems to it.

Though by now it might not get us much benefit, I admit. A lot of kernel code is fairly conservative and rewriting it just for the sake of rewriting won't get us anything useful.

>I'd bet a kernel project that was started from scratch using C++, could probably *not* sustain the rate of development that Linux has seen, given the nature of it's contributor base. Those projects you mentioned all started in C++ (AFAIK), typically with a small focused team of programmers, all commercially funded (except LLVM, which had research funding initially), whereas Linux had an explosion of early volunteer contributors.

KDE started the same way - an explosion of contributors working on the same goal. By now it's comparable in size with the Linux kernel. Ditto for Haiku OS (though it can't compare with Linux).

At smaller scales, we're seeing addition of C++ to the Mesa project right now (new shader compiler and optimizer is written in C++) and it seems to be working out fine. Though they use it in C-with-classes fashion, mainly for the 'Visitor' pattern.

There are some case studies on transitioning large codebases from C to C++: http://www.bleading-edge.com/Publications/C++Report/v9507...

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 23:07 UTC (Thu) by chad.netzer (subscriber, #4257) [Link]

> KDE started the same way - an explosion of contributors working on the same goal.

But KDE was also a collection of many independent, even orthogonal, applications such that developers could work on their piece without any affect on another (ie. less code merging across larger groups, etc.) Even the dependent pieces were often linked together by abstractions at a higher level than the language (object brokers, interprocess communication protocols, etc.) right? So the real core C++ libraries and such, that were linked into programs directly were a much smaller group of developers and contributors (I'm guessing). And furthermore, a significant portion of that core was based around the commercially developed QT, which started completely as C++ (and hell, even extended it) and was a small tight team. KDE is definitely a great example of a large and active C++ based project, but still, I think, a *very* different contribution model than a community driven C++ monolithic kernel would be. It all comes down to merging, and merging lots of separate contributed C++ code and requires a lot more pre-planning, design, discipline, coordination, and review *just at the language level* than C (imo).

However, my summary of KDE could be wrong, so someone please correct me if so. I've not used it much personally.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 23:20 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]

Kernel is not exactly monolithic as well. It has various loosely-linked subsystems and a lot of code is in drivers. Also, I'd like to point out that the core in Linux initially was quite small as well.

Ah, and I've forgotten about another large C project switching to C++ - http://gcc.gnu.org/wiki/gcc-in-cxx

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 2:34 UTC (Fri) by chad.netzer (subscriber, #4257) [Link]

> Kernel is not exactly monolithic as well.

Sure it is. Certainly in the classic sense of not having memory access isolation between all services, drivers, etc. The Linux kernel may be modular, and have some kernel threads, but it is "exactly monolithic" by the standard definition, is it not?

Thus, it isn't paranoia on the part of the developers to use a language that allows one to fairly easily see what memory accesses are occurring on a roughly line per line basis (such as C).

> Ah, and I've forgotten about another large C project switching to C++ (gcc)

That's interesting, because the gcc development seems so different from Linux's; things like the copyright assignment provisions may (or may not, I'm speculating) affect the contributor pool in a way that makes the transition more practical. In any case, compilers work at a different level, so that the memory access abstractions of C++ aren't so objectionable. In fact, I suspect hey have more data structures and inheritance possibilities that would benefit directly from it, and it's probably a good choice for them. But I don't think that necessarily translates into a reason for a kernel project to do the same.

In any case, while I disagree with daglwn's assertion that Linus was "flat out wrong" about C++, it's exciting to see projects that implement a kernel in a language like C++ (or D, or Go, etc.) to know how the language features influence design decisions and ease of implementation, to see if it really matters.

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 14:46 UTC (Fri) by Cyberax (✭ supporter ✭, #52523) [Link]

>Sure it is. Certainly in the classic sense of not having memory access isolation between all services, drivers, etc. The Linux kernel may be modular, and have some kernel threads, but it is "exactly monolithic" by the standard definition, is it not?

I don't mean 'monolithic' in the sense of 'monolithic vs. microkernels'. I meant it in the sense of 'one giant C file vs. modular code'.

Linux Kernel is divided into subsystems which are pretty independent: network layer doesn't really care about DRI, for example.

>In any case, while I disagree with daglwn's assertion that Linus was "flat out wrong" about C++, it's exciting to see projects that implement a kernel in a language like C++ (or D, or Go, etc.) to know how the language features influence design decisions and ease of implementation, to see if it really matters.

It doesn't look like that C vs. C++ matter much in kernel development (look at L4 kernel, for example).

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 17:49 UTC (Fri) by chad.netzer (subscriber, #4257) [Link]

> I don't mean 'monolithic' in the sense of 'monolithic vs. microkernels'.

Well, that's confusing then. The term already has meaning in kernel discussions, and you were responding to *my* usage of the term. But, ok.

The concerns I mentioned still apply for pretty independent codebases: if you intend to build the whole kernel with C++, you have do deal with all the legacy C code and interaction issues (function namespace, type incompabilities, etc.) so as you said it must be gradual. But, if it's gradual, you now have a complicated mixed build system and have to worry about how to interact across the C/C++ layers (since there is no agnostic "message passing" layer for the components, like a microkernel would have). It could be done, I'm sure, it just a matter of what is motivating it.

It might make sense if there already existed some well tested code bases that were worth integrating; let's say hypothetically that ZFS had been released as GPL years ago, but it's implementation was in C++. Then I could see dealing with the pain (or attempting it), rather than a rewrite.

> It doesn't look like that C vs. C++ matter much in kernel development (look at L4 kernel, for example).

Well, L4/Fiasco *is* a microkernel, with a well defined message passing ABI, built by a small team, and is *tiny*. But it demonstrates that design of the OS is the much bigger issue than language implementation for the most part. The language issue really matters more (imo) from a community and potential contributor perspective.

Haiku

Posted Jun 3, 2011 8:48 UTC (Fri) by tialaramex (subscriber, #21167) [Link]

Haiku is a really terrible example.

In their kernel they're using a subset of 1990s C++ that gives them much less functionality than the C++ aficionados have been talking about in these comments.

All non-core components use only C APIs even though they may be in C++. So many of the examples mentioned for Linux would still have to be done the same way since they're available in blessed module APIs for Haiku.

And despite a decade's work what they have is basically a BeOS clone. Nasty shortcuts to rush BeOS to market before Be Inc. ran out of money, faithfully reproduced. That goes from big picture things like no privilege separation and no power management to little annoyances like no real hot plug (they have a hack that lets them hotplug USB devices by first loading all the drivers they might want...)

Haiku

Posted Jun 3, 2011 13:07 UTC (Fri) by cmccabe (guest, #60281) [Link]

> In their kernel they're using a subset of 1990s C++ that gives them much
> less functionality than the C++ aficionados have been talking about in
> these comments.

*Everyone* is using some kind of subset of C++.

Firefox and Chrome, as well as Webkit, are using -fnoexceptions and -fnortti. This is a pretty important design choice because it means that you can't do things that can fail in your constructors, since there is no way for them to report errors except throwing exceptions.

XNU, which later became the basis of the Mac OS kernel, uses a restricted subset of C++ that doesn't allow exceptions, multiple inheritance, or templates.

If projects do use exceptions, they all do it differently. Some old Microsoft APIs throw pointers to exceptions, which the caller must then manually call delete() on. Most projects roll their own exception hierarchy. Sometimes they inherit from std::exception; other times not. Sometimes they throw other things. I heard from a friend that his team is writing new code that throws ints! Yes, new code, written in 2010, that throws ints.

Some projects use char* almost everywhere, other projects use std::string. QT has its own string class, which is supposed to be better at internationalization, that a lot of projects use. Some projects use a mix of all of this stuff. Some projects roll their own string class.

A lot of projects rolled their own smart pointer, or used one from boost, prior to the introduction of tr1::shared_ptr. Some of them work similarly, others not. Some projects barely use smart pointers; other projects use them almost everywhere.

*Everyone* is using some kind of subset of C++. Everyone is bitterly convinced that they are right and everyone else is wrong. When someone advocates "using C++," a legitimate question is "which C++"? When you add a new person to your team, you can expect to spend quite a bit of time getting him or her up to speed.

And of course, the different subsets of C++ don't interoperate that well at the library level. So when designing APIs, everyone just uses the lowest common denominator, which is C or something that looks almost exactly like it.

Haiku

Posted Jun 3, 2011 15:43 UTC (Fri) by daglwn (guest, #65432) [Link]

> *Everyone* is using some kind of subset of C++.

Not true, and if they are, they're Doing It Wrong.

Boost, for example, places no such restrictions on the project. Instead, members use vigorous code review to ensure quality. That is the right way to go because terrible interfaces get designed in every language every day. Restricting the set of allowed language features doesn't solve that problem, it exacerbates it.

Haiku

Posted Jun 3, 2011 19:16 UTC (Fri) by cmccabe (guest, #60281) [Link]

> > *Everyone* is using some kind of subset of C++.
>
> Not true, and if they are, they're Doing It Wrong.

Really? Let me ask you: when was the last time you wrote code that used throw specifications? Or the "export" keyword for templates? Or wide character streams (wchar)? Have you ever used protected inheritance?

Wake up and smell the coffee. You're programming in a subset of C++. You are no doubt convinced that your subset is "modern" and "progressive", whereas everyone else's is "backwards" and "old-fashioned". But it's still a subset.

Haiku

Posted Jun 3, 2011 21:09 UTC (Fri) by daglwn (guest, #65432) [Link]

This makes no sense. If this is your criteria, everyone programs in a subset of their favorite language. When's the last time you used gets()?

The point is that the tools to use shouldn't be artificially restricted. If someone wants to use protected inheritance, let them as long as they can show why it's necessary or beneficial.

Haiku

Posted Jun 4, 2011 1:07 UTC (Sat) by cmccabe (guest, #60281) [Link]

You're mixing apples and oranges. gets() isn't a language feature, it's a library function.

> The point is that the tools to use shouldn't be artificially restricted.
> If someone wants to use protected inheritance, let them as long as they
> can show why it's necessary or beneficial.

You didn't answer my question. When was the last time you used those features?

You say that programmers shouldn't be "artificially restricted" from doing things that are "necessary and beneficial", but those are weasel words. The reality is, you'll just define necessary and beneficial as whatever you've been doing. So if you've been throwing exceptions as pointers, it's obviously "necessary and beneficial" for the new code to do the same. If you haven't been using throw specs, obviously the new code shouldn't have them. But you're not using a subset of the language, oh no.

As a side note, what's with the gets() obsession in these programming language debates. I don't think I was even alive the last time someone used gets() in a real program.

Haiku

Posted Jun 4, 2011 1:11 UTC (Sat) by cmccabe (guest, #60281) [Link]

As a side note, I found this highly amusing:
http://stackoverflow.com/questions/214321/what-c-features...

Literally every comment has a completely different view of which C++ features are "evil." I don't think you can find even two distinct answers that agree. I can only imagine what a novice programmer, fresh out of school, would think after reading this :)

Haiku

Posted Jun 4, 2011 5:52 UTC (Sat) by elanthis (guest, #6227) [Link]

It saddens me that it's become the norm to expect a "novice" who's had a full four years worth of a $100,000 education to have his first experience with real-world languages be a StackOverflow posting.

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 15:40 UTC (Fri) by daglwn (guest, #65432) [Link]

You're asking the wrong questions.

Which parts of C++? All of it! But people have to develop smartly, as with any language.

Specifying a language subset is a losing proposition because there will always be some feature outside of that subset that is the perfect fit for some need in the project. Artificially restricting developers is the wrong way to go. Code review, basic rules of thumb and other such things do a much better job of ensuring software quality.

As to the inheritance model, I have heard this claim many times and never once has anyone explained their objections. What is wrong with it? C++ inherited it from Simula, so your claim that nothing else uses it is incorrect.

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 18:29 UTC (Fri) by chad.netzer (subscriber, #4257) [Link]

> You're asking the wrong questions.

Enlighten us as to the right questions.

> Which parts of C++? All of it!

On new projects, sure. C++ (imo) can be quite elegant when you code in the modern style, and not all the legacy predecessors (I started before mature templates, before exceptions, before the Standard Library, etc., and many codebases are still stuck in that age since compilers took *forever* to mature).

But that isn't what we are discussing. As I said, unless you plan to rewrite Linux from scratch in C++, any proposal on the table is about using a restricted set of features in some parts of the kernel.

> C++ inherited it from Simula, so your claim that nothing else uses it is incorrect.

My claim was that no one copied it from C++, so you're re-statement of what I said is wrong. And which languages copied C++'s design in this respect? What other language after C++ uses multiple inheritance, has virtual base classes, uses "protect" or equivalent, allows slicing, etc.? I'm happy to be enlightened if there is a language out there that said "C++ inheritance is spot on, let's copy it". Python, bizarrely enough, comes kinda close in some ways (to it's detriment).

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 21:21 UTC (Fri) by daglwn (guest, #65432) [Link]

> Enlighten us as to the right questions.

Does this code or proposed design exhibit sound engineering given the project goals?

> As I said, unless you plan to rewrite Linux from scratch in C++, any
> proposal on the table is about using a restricted set of features in some
> parts of the kernel.

Why? There is nothing magical about the kernel. It's software. Even in a project transitioning from one implementation to another, there's no reason to artificial limit what people are allowed to do, especially _a_priori_.

> What other language after C++ uses multiple inheritance, has virtual
> base classes, uses "protect" or equivalent, allows slicing, etc.?

Ah, good, some concrete points.

Multiple inheritance is very useful. If a language with inheritance doesn't have it, it has a major missing feature.

Virtual base classes are trickier. They are painful, I admit, but necessary for some uses of MI. Many uses of MI don't need them at all. C++'s possible mistake was to place the decision point at the intermediate class level rather than at the "final" class level. But it's a convenience/efficiency tradeoff and I'm not totally sure the committee got it wrong..

By "protect" I assume you mean "protected." Protected methods certainly are useful. Protected inheritance, not so much but I'm sure someone has a use case for it.

Slicing is also a big pitfall. But I think it's a natural consequence of call-by-value semantics. One could argue that C++ should have made call-by-reference default but that would have broken C compatibility, a major strength of the language.

> I'm happy to be enlightened if there is a language out there that said
> "C++ inheritance is spot on, let's copy it".

I don't think anyone has said that. I have not said that. To do so would be to say that the language is perfect, an impossibility in an imperfect world.

There are valid criticisms of C++. But to take that to an extreme and proclaim the C++ inheritance model "broken" or "wrong" is, well, wrong.

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 22:26 UTC (Fri) by elanthis (guest, #6227) [Link]

> Multiple inheritance is very useful. If a language with inheritance doesn't have it, it has a major missing feature.

Sort of. Multiple inheritance in the C++ sense is useful simply because C++ doesn't differentiate between base classes, interfaces, and mixins. The need for multiple inheritance with multiple base classes is pretty small, and at least every example I've personally seen has been an example of bad interface design rather than a showcase for the need for MI (granted, my experience is not all-encompassing).

> Virtual base classes are trickier. They are painful, I admit, but necessary for some uses of MI. Many uses of MI don't need them at all. C++'s possible mistake was to place the decision point at the intermediate class level rather than at the "final" class level.

I will argue that if you start having intermediate classes very often at all, or a diamond tree, you're using inheritance wrong. Which most object-oriented developers do, unfortunately. A large part of this I fear is that it's simply taught wrong in schools. The classical examples of inheritance are things like Mammal->Dog, or Shape->Circle, or Vehicle->Car. These are attempts at modelling real-world "is-a" relationships inside of algorithmic data structures, which is really really wrong. It's a very long discussion to explain it in the necessary detail, and I believe Herb Sutter already did some fantastic talks on the topic (I'm having trouble Googling them though; maybe it wasn't Sutter that did them?).

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 23:32 UTC (Fri) by daglwn (guest, #65432) [Link]

I think it's actually a strength that C++ doesn't differentiate among types of classes. It allows more flexibility and reuse. I agree with you that there should be a good case made before using MI.

How would, for example, making "mixin" a special kind of class look in the place of MI?

I'm not sure about "intermediate classes very often" part, but you certainly are correct about the diamond hierarchy. A diamond with state in the virtual base is usually poor design and causes countless headaches in constructors. A diamond without state in the virtual base (i.e. it's a pure interface) is sometimes useful.

isa inheritance is usually good design. hasa inheritance is usually bad design. For the latter, composition is almost always the way to go. If you can find examples where "isa" is the wrong thing to do, I am very interested. Myself, I tend to prefer generic code and composition to inheritance but there are obvious cases where inheritance is useful and necessary.

Object-oriented design patterns in the kernel, part 1

Posted Jun 4, 2011 0:50 UTC (Sat) by elanthis (guest, #6227) [Link]

> How would, for example, making "mixin" a special kind of class look in the place of MI?

The closest you get to what most people would consider a mixin in C++ would be the CRTP, but it's a little gimped because you can't properly specify that the "mixin" is implementing methods in an interface that the class inherits from.

e.g., if you have class Foo that implements interface IDoStuff, a mixin SimpleDoStuff might include members and methods that implement some or all of the methods of IDoStuff. There's no direct way to do that in C++, though, and so you end up with either grotesque inheritance trees or diamond inheritance.

The only way to handle common partial implementations of interfaces in C++ is to create intermediate classes, which then results in you having a ton of intermediate classes, and you start wanting to mix different sets of then, and then you end up with either (a) an uber base object replacing the interface, making all your object bloated and over-complex, (b) a metric crapload of intermediary classes with tons of code duplication, or (c) diamond hierarchies and virtual bases and all the problems those impose.

> isa inheritance is usually good design. hasa inheritance is usually bad design. For the latter, composition is almost always the way to go. If you can find examples where "isa" is the wrong thing to do, I am very interested. Myself, I tend to prefer generic code and composition to inheritance but there are obvious cases where inheritance is useful and necessary.

We perhaps are using slightly different definitions of "is-a", as I wasn't including interface derivation in that statement. Composition is the opposite of what I consider is-a. Yay for ambiguity in computer terminology. :)

(On a related note, composition is super important. Not understanding composition is the other thing that leads to massive inheritance trees and horribly screwed up architectures. Composition isn't necessarily trivial in C++ due to a lack of language/syntax support, but it's still possible to do and do well, unlike mixins which are borderline imposssible.)

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 22:56 UTC (Fri) by chad.netzer (subscriber, #4257) [Link]

> Does this code or proposed design exhibit sound engineering given the project goals?

That is only the right question to ask in that it must always be asked, and is contentless. Clearly a good designer will start by saying, "What is a good, sound design for this task?" But knowing how to evaluate a design and implementation requires asking the kind of concrete questions that you claimed were not the right ones.

> Even in a project transitioning from one implementation to another, there's no reason to artificial limit what people are allowed to do, especially _a_priori_.

You started this whole thread by claiming "One does not have to use every feature of a language to get good productivity out of the language.", which implied a conservative approach to feature adoption. Now you are claiming that if Linux allowed for C++ development, that all features should be available for use without restriction. Change of heart?

> Ah, good, some concrete points.

You phrase that as though I haven't already been making concrete points. I disagree, and I think I've been more concrete in my points than you have. I find Linus's arguments about using C++ for kernel coding to be largely correct (for the Linux kernel), and your defense of your assertion that he is "dead wrong" to be unconvincing.

> But to take that to an extreme and proclaim the C++ inheritance model "broken" or "wrong"

Those quoted proclamations you are apparently attributing to me are also incorrect, btw.

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 23:43 UTC (Fri) by daglwn (guest, #65432) [Link]

> But knowing how to evaluate a design and implementation requires asking
> the kind of concrete questions that you claimed were not the right ones.

Your overarching question was, "what parts of C++?" All of your other questions were about dealing with consequences of using a subset, save the one about overloading and that one is equivalent to, "how do we make sure we don't introduce bugs?"

This is how I look at things. Given a tool, use it. Everyone on the project should evaluate how that tool is being used. If we start with a tool, reduce its functionality by saying we can't do this or that with it, it doesn't eliminate the need to evaluate how we're using it, but it does eliminate the some possibilities of using it better when we find certain cases of poor usage.

> You started this whole thread by claiming "One does not have to use
> every feature of a language to get good productivity out of the
> language.", which implied a conservative approach to feature adoption.
> Now you are claiming that if Linux allowed for C++ development, that all
> features should be available for use without restriction. Change of
> heart?

Not at all. The two statements are entirely consistent. All the tools should be available and we should make sure we use them correctly. Of course a project in transition isn't going to use every language feature immediately, simply because not all of the code will transition immediately. That does not mean that this or that language feature should be off-limits when transitioning any particular piece of code.

> You phrase that as though I haven't already been making concrete points.

Forums are a poor way to convey expression. Believe me, no disrespect was intended. I was genuinely happy to see examples as I have been asking for them in various places for a long time.

> Those quoted proclamations you are apparently attributing to me are also
> incorrect, btw.

Perhaps I misunderstood the intent of your statement:

Btw, the inheritance model that you tout as a feature (for "simple
uses") is arguably one of C++'s *worst* additions to the C language

If so, I apologize. It reads to me like a condemnation of the entire C++ inheritance model.

Object-oriented design patterns in the kernel, part 1

Posted Jun 4, 2011 0:21 UTC (Sat) by chad.netzer (subscriber, #4257) [Link]

> This is how I look at things. Given a tool, use it.

I agree. Projects that adopt a tool like C++, should be prepared to adopt all of it. Those that don't want to adopt all of it, should seriously consider adopting none of it. The ones that want to adopt a subset of C++ (ie. "We'll just use it as a 'better' C, or 'C with classes'") should be prepared for the burden of that, and are probably best suited to a fairly small team of developer's (ie. not Linux sized). I think Linus was right to adopt none of it.

> The two statements are entirely consistent.

The statements themselves are not inconsistent with each other, but I feel the argument you are making with them is not, imo. But let's leave it. I think it is impractical for a large active codebase with wide collaboration to transition from C only, to fully incorporating and embracing modern C++, without a rewrite essentially from scratch. I'm not sure of any such examples in the open source world (though gcc was pointed out earlier as a related example), but perhaps I'm wrong.

> Perhaps I misunderstood the intent of your statement:

I think you were just restating my intent in your own words, but using the quotes made it appear that I said those words. I'd say 20+ years of C++ inheritance experience has shown it to be flawed: it's difficult for many to understand all the ramifications and options, carelessness leads to the fragile base class syndrome despite it's encapsulation features, people have devised the ugly "pimpl idiom" to deal with other compiler and encapsulation issues, it has multiple inheritance :), and so on and so on. I think C++ has been a useful experiment, and hopefully something better will now come to prevalence that has learned from it's flaws (disregarding Java et al; I mean a language w/ pointers).

Object-oriented design patterns in the kernel, part 1

Posted Jun 4, 2011 7:14 UTC (Sat) by chad.netzer (subscriber, #4257) [Link]

> There is nothing magical about the kernel. It's software.

This got me thinking about something else. There are about 5000 uses of kmalloc() in the linux kernel, but kmalloc() takes a flag argument. So, to replace these uses with idiomatic C++ would require widespread use of placement new().

However, using placement new requires care when deleting objects. So, rather than just using delete, you have to both manually call the class destructor, and then operator delete() with the right arguments to invoke the proper delete function (I suppose the kernel C++ stdlib could kludge the delete operator so that it works for placement new; now you are writing non-conformant C++)

ie.

struct T *p = kmalloc(sizeof(struct T), GFP_KERNEL | GFP_DMA);
some_init_function(p);
kfree(p);
becomes something like:
T *p = new (GFP_KERNEL | GFP_DMA) T;
p->~T();
operator delete(p, GFP_KERNEL | GFP_DMA);  // Or whatever it takes to match the call signature
This unorthodox usage is necessary because the kernel is not just like other software. Not when it comes to things like memory allocation. And what about a std::vector of struct *T? How do you allocate those objects with the right memory flags?

Perhaps there is an elegant solution to this, most likely by eschewing bare pointers in the kernel and always using some templated smart pointer class. A fairly radical change for kernel development. What are your thoughts on it?

Object-oriented design patterns in the kernel, part 1

Posted Jun 4, 2011 7:19 UTC (Sat) by elanthis (guest, #6227) [Link]

I feel your argument is a little weak.

Video games are almost certainly "just another piece of software" and we deal with the exact same memory management types of issues. Many game engines outright ban the use of new and delete because of the problems they cause, requiring instead that specialized factories and memory managers be used, especially on console titles and mobile games. It's every last bit as complex of a set of interfaces as the kernel, if not more so. And they're still just software, and they still mostly all use C++. :)

Object-oriented design patterns in the kernel, part 1

Posted Jun 4, 2011 7:36 UTC (Sat) by chad.netzer (subscriber, #4257) [Link]

Whereas the existing kmalloc/kfree idiom is quite straightforward. So where is the gain? If the most basic memory allocation features of the language have to be banned from use, it's hardly a sales point for adopting the language. You use it *in spite* of these deficiencies, just as Linux does with C.

Object-oriented design patterns in the kernel, part 1

Posted Jun 5, 2011 19:32 UTC (Sun) by elanthis (guest, #6227) [Link]

C++ has problems. The thing is, no other language is better. Sure, C is clearer about memory allocation. But it lacks critical things like templates. Which also have problems, but without them it's literally impossible to write a sane container library, generic algorithm library, or even a simple sort that doesn't end up making logN function calls. Dynamic languages or languages like C#/Java almost manage this, but either screw up safety or just totally screw up performance and memory usage in their generics implementation.

And then there's user-defined value types. Trying to do basic graphics work in any language other than C/C++ is a nightmare, because your vectors are referece types. In games, I use vec3's and vec4's more often than I use int's and float's. Now imagine if every number in your language of choice was a reference type. That's what doing games (or just about any other interesting mathematical application) in every single popular language besides C/C++ is like. C's failing here is the lack of operator overloading for user-defined types, which just makes doing simple expressions with vec's and such a total pain. Even languages supposedly focused towards games or systems programming, like C#, utterly fail when it comes to user defined types.

And then in the C/C++ realm, C++ actually solves the very memory management problems we started talking about through it's ability to let users define their own handle types. Raw pointers in C++ present a lot of problems that also exist in C, but C lacks the ability to solve those problems at a language/API level like you can in C++.

So yeah, C++ fails in many ways, and fails hard. Every other language just fails even harder (for certain classes of application, that is). D comes closest to being a viable replacement, but it's not better enough to justify breaking compatibility with all the excellent C++ libraries out there.

Object-oriented design patterns in the kernel, part 1

Posted Jun 6, 2011 19:17 UTC (Mon) by cmccabe (guest, #60281) [Link]

In my opinion, video games are a great example of a place where you *should* use C++. Kernels and systems-level software are a great example of a place where you should *never* use C++.

Why do I say this? Well, C++ was designed to increase the speed of development and allow you to mix high-level and low-level code easily. It is very good at doing those things. At the same time, a clever programmer can get reasonable performance out of C++ by spending a little time optimizing.

When writing a video game, you don't care that much about long-term maintainability or safety. An exciting game that crashes a few times a month will sell many more copies than a boring game which is rock-solid. Stuff gets rewritten from scratch every few years to take advantage of the latest and greatest hardware. If things are not written in the absolute most efficient way, who cares? You just keep tweaking things until the game runs all right on the target hardware.

Kernels are the opposite. There are still pieces of code in the Linux kernel that go back to the early 1990s. Maintainability and stability are much more important than features or speed of development. It's important to use a simple style, in a simple programming language, that many different contributors can understand and use. Efficiency is key-- we don't have extra cycles to burn on things like std::string or std::map. In some structures, people are reluctant to enlarge the structure even by 4 bytes for performance reasons.

A lot of people claim that C++ is somehow safer than C. But this is ridiculous. C++ has all of the different flavors of undefined behavior that C does, plus many, many more. C++ has a lot of "generated code"-- default copy constructors, default destructors, and so on, that will burn you if you're not careful. Again, it's about speed of development, *not* safety.

Claiming that C++ is faster than C, or even as fast, is equally ridiculous. Even the most basic operations in C++, like iostreams, are much slower than the C equivalents like printf. Maybe some kind of theoretical perfect programmer could write equally fast, or even faster, code in C++ than C. But in the real world, programmers will take shortcuts if they exist. Let me draw an analogy-- if your wife bakes a cheesecake for you every night, you're going to get fat. Yes, in theory you could not eat the cheesecake, but it's right there.

So we're left with the real C++ advantage-- speed of development. I want to be fair here. C++ is good at doing what it does. But let's not get confused here-- it's not the right choice for a lot of projects. Use the right tool for the job.

Object-oriented design patterns in the kernel, part 1

Posted Jun 7, 2011 3:40 UTC (Tue) by elanthis (guest, #6227) [Link]

You're making a few claims I have to disagree strongly with, but in few words as ive got to walk out the door in a few. First, I don't think you are quite in tune with game development, at least not professional game development. Stability and even security are quite important - nobody wants to be Fallout New Vegas. Second, game engines are not rewritten that often. All of the popular engines are well over a decade old, and the majority of the code has not changed in architecture at all. Most crashes you see in games are actually video driver crashes, save a few particularly poorly engineered titles.

C++ is very much a safer language. You bring up hidden code and such as if that makes it unsafe, which is silly. C++ doesnt just magically do things wrong behind your back anymore than C does. Yes, a destructor runs "automatically" but never ever unexpectedly. On the other hand, C requires programmers to cut-n-paste large swaths of boilerplate code, leading to a much higher bug count in general.

Simple fact is that there are kernels written in C++ which work very very well.

Object-oriented design patterns in the kernel, part 1

Posted Jun 7, 2011 6:13 UTC (Tue) by cmccabe (guest, #60281) [Link]

> First, I don't think you are quite in tune with game development, at least
> not professional game development. Stability and even security are quite
> important - nobody wants to be Fallout New Vegas

Look, I'm not trying to trash game development or game developers. I know you guys have a difficult and challenging job.

But let's be realistic. If I'm the project manager and I have a choice of releasing with a crashing bug or two or releasing a game that is dull or outdated, I am going to release with the bugs. If I don't, I'm going to get fired. Nobody wants a game that looks like it should have come out last year.

On the other hand, the economics are different for some other kinds of software.

> C++ is very much a safer language. You bring up hidden code and
> such as if that makes it unsafe, which is silly

It's not silly at all. Every time I write class with any kind of non-trivial destructor, I have to remember to hide the copy constructor and assignment operator. If I fail to do this, a simple typo later in the program-- say typing use_foo(Foo f) instead of use_foo(Foo &f)-- could bring the program to its knees.

Or take conversion constructors or default parameters. Here's a true story of something that happened very recently. Someone on our project decided to change a function prototype from foo(int i) to foo(const MyObject &o). What he didn't realize was that there was a conversion constructor from an int to a MyObject. So when he overlooked this little gem:
> foo(0)

The compiler happily generated this:
> foo(MyObject(0))

Needless to say, the results were not what he intended at all.

Implicitly generated code hurts. Default constructors hurt. Default parameters hurt even more. You might disagree, but I've had a lot of time to come to this opinion.

> Simple fact is that there are kernels written in C++ which work very very
> well.

Well, as I already mentioned, there is XNU, which has no templates, no RTTI, and no exceptions, but is "technically C++". Apparently the Windows NT kernel also has some kind of C++ support-- again, I think exceptions are left out, as well as who knows what else.

I would say a bigger C++ success story is the success of the LLVM project. They managed to write a very capable compiler in C++, which has been gaining a lot of momentum. One of my friends who works on the project is also one of the few people who can stump me with a C++ question. I guess it's hard to out language-lawyer the language implementers!

But oh, they don't use exceptions :) And the LLVM library is 20MB when compiled, whereas /usr/bin/gcc is 262kb.

C++ has been the inspiration for a lot of other languages like Java and Golang, and I respect it for that. It got a lot of things right, and it's still the language that I know the best. But can't we have some new ideas after all these years?

Object-oriented design patterns in the kernel, part 1

Posted Jun 7, 2011 6:24 UTC (Tue) by elanthis (guest, #6227) [Link]

Sure, there's cases where C++ bites you. There are cases where C does this, too, and there are many ways in which C++ helps you.

I doubt I'll convince you to change your opinion with anything short of a small novel, though. :)

Object-oriented design patterns in the kernel, part 1

Posted Jun 7, 2011 7:46 UTC (Tue) by jezuch (subscriber, #52988) [Link]

> And the LLVM library is 20MB when compiled, whereas /usr/bin/gcc is 262kb

Are you sure you're not looking at just the GCC driver that invokes the actual compiler?

jezuch@zx-spectrum-1:~$ du -shc /usr/lib/gcc/x86_64-linux-gnu/4.6/{cc1,cc1plus,lto1}
11M /usr/lib/gcc/x86_64-linux-gnu/4.6/cc1
12M /usr/lib/gcc/x86_64-linux-gnu/4.6/cc1plus
11M /usr/lib/gcc/x86_64-linux-gnu/4.6/lto1
33M razem

Object-oriented design patterns in the kernel, part 1

Posted Jun 7, 2011 18:54 UTC (Tue) by cmccabe (guest, #60281) [Link]

I stand corrected. I thought that binary was way too small... but ldd didn't tell me anything useful.

It's been a long time since I built gcc. Even back when I did embedded stuff, we always seemed to use prebuilt toolchains.

Object-oriented design patterns in the kernel, part 1

Posted Jun 9, 2011 14:21 UTC (Thu) by renox (guest, #23785) [Link]

> Implicitly generated code hurts. Default constructors hurt. Default parameters hurt even more. You might disagree, but I've had a lot of time to come to this opinion.

Sorry but C++ poor mix of features doesn't mean that those features are always bad..
Plus C's lack of initialisation also hurt a lot, no?

Object-oriented design patterns in the kernel, part 1

Posted Jun 9, 2011 16:56 UTC (Thu) by jd (subscriber, #26381) [Link]

Well, no. Implicit code is (IMHO) always a Bad Thing because when the specs or compiler change, the resultant binaries will change behaviour when given the same source.

The source is a specification document, to all practical intents and purposes, which the compiler uses to generate a program. If the program can change in nature for the same specification, the specification is incomplete and insufficient.

In other words, by depending on something external (in this case, the compiler) you cannot do reliable testing. There are too many external parameters that you can never reliably take account of. Good engineering practice is to always work to reduce or eliminate the unknowables. If the compiler is any good, it'll ignore initializing to the compiler's defaults since the instructions generated by the compiler's default will already be present. If the compiler isn't any good, you really shouldn't be trusting it to be doing the Right Thing anyway.

If you explicitly initialize, you always know the state of a variable, no matter what new C or C++ standard is produced. This is guaranteed safe.

Compiler-tolerant software is necessarily well-engineered software. Yes, it's more work, but if you wanted to avoid work, you'd not be using C or C++, you'd be using a fourth- or fifth-generation language instead. The only reason to use anything in the C family is to be able to balance development efficiency with code efficiency. Higher levels of language offer better development efficiency, lower levels (Fortran, assembly, etc) offer better code efficiency. Neither is useful on its own for something like an OS kernel, which is why nobody writes general-purpose kernels on the scale of Linux in either Erlang or IA64 assembly.

(Kernels do exist in both those, and indeed in Occam, but because of tradeoffs are almost always much more special-purpose.)

Object-oriented design patterns in the kernel, part 1

Posted Jun 13, 2011 0:20 UTC (Mon) by cmccabe (guest, #60281) [Link]

> Plus C's lack of initialisation also hurt a lot, no?

I remember a thread here on LWN a while back where people were complaining about a performance regression in the kernel caused by zeroing struct page. So it seems that at least in some corner cases, not initializing memory is a feature. In a perfect world, non-initialization probably should be something that the programmer has to ask for specifically, rather than the default. But C was designed in the 1970s-- give it a break already.

When C++ was designed, in the mid-1980s, the decision was made to keep all the old uninitialized variable behavior and add some more. So if you create a C++ class with some primitives, and forget to initialize them in one of the constructors, they'll be uninitialized. This also applies to copy constructors. The rationale was that programmers shouldn't have to pay for what they didn't use.

Luckily there is some relief in sight, in the shape of Coverity and similar static analysis tools. These can catch most uninitialized variables.

When in doubt, add another layer of indirection

Posted Jun 1, 2011 21:25 UTC (Wed) by sethml (guest, #8471) [Link]

Curious question from a non-kernel-hacker: it seems like the kernel could save a lot of memory (particularly on 64-bit systems) by replacing vtable pointers by smaller array indices. For example, every inode in the system contains an inode_operations pointer, occupying 64 bits:
struct inode {
        /* RCU path lookup touches following: */
        umode_t                 i_mode;
        uid_t                   i_uid;
        gid_t                   i_gid;
        const struct inode_operations   *i_op;
        struct super_block      *i_sb;
        ...
}

... foo->i_op->bar(...);
It'd be possible to save 52 bits per inode, which could add up if you have a lot of inodes in memory:
struct inode {
        ...
        uint16_t i_op_idx;
        ...
}

struct inode_operations inode_ops[MAX_INODE_OPS];

... inode_ops[foo->i_op_idx].bar(...);
The same technique could be used for a lot of structure members which aren't vtable pointers - for example, i_sb seems like a potential candidate. Disadvantages: need to set an upper limit on the number of inode_operations structures, a bit slower (extra indirection), complicates the code. Given appropriate infrastructure, the code complication could be centralized in some helper functions/macros. Thoughts? Could this be worthwhile? Are the potential memory gains too minimal to care about?

When in doubt, add another layer of indirection

Posted Jun 1, 2011 23:37 UTC (Wed) by darthscsi (guest, #8111) [Link]

Doing this without language level support for appending arrays gets really ugly. That said, the kernel plays linker script magic other places to create appending arrays.

When in doubt, add another layer of indirection

Posted Jun 2, 2011 8:33 UTC (Thu) by koverstreet (subscriber, #4296) [Link]

In order to make it work with modules you'd need to generate the idx at runtime, which would actually solve both problems...

When in doubt, add another layer of indirection

Posted Jun 3, 2011 7:57 UTC (Fri) by josh (subscriber, #17465) [Link]

Most of the time, only one copy of those structures exists, so saving a few bits doesn't seem worth the extra complexity and the extra indirection.

When in doubt, add another layer of indirection

Posted Jun 9, 2011 7:44 UTC (Thu) by kevinm (guest, #69913) [Link]

The savings are per inode, not per inode_ops.

When in doubt, add another layer of indirection

Posted Jun 3, 2011 9:04 UTC (Fri) by jengelh (subscriber, #33263) [Link]

But the 52 saved bits might be spent again in extra instructions to calculate the pointer from the extra offset that you then would have.

When in doubt, add another layer of indirection

Posted Jun 3, 2011 12:15 UTC (Fri) by dlang (guest, #313) [Link]

but if you have thousands or millions of these in memory the space savings may add up

also, since the cpu runs so much faster than memory does, you can actually do quite a lot of processing in the time saved by avoiding a memory fetch.

When in doubt, add another layer of indirection

Posted Jun 9, 2011 7:43 UTC (Thu) by kevinm (guest, #69913) [Link]

This seems like a reasonable idea, although the hit to readability and simplicity is such that you'd only want to do it in places where it would make a real memory difference.

Why not try it and see?

(By the way, it'd be 48 bits that you'd save replacing a 64 bit pointer with a 16 bit index).

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 0:13 UTC (Thu) by linusw (subscriber, #40300) [Link]

Fun that my two regulator commits are used as vtable illustration, I vaguely knew the term vtable before, now I know I'm doing such.

I'm anticipating the follow-on articles to elaborate on how the outspaced macro container_of() performs the analog mechanism to super() from OOP...

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 9:07 UTC (Fri) by jengelh (subscriber, #33263) [Link]

container_of is e.g. used for linked lists, where it merely gives the surrounding actual object to the linked list head, in other words, it is used like (Java)"class mydevice implements list_head" rather than "extends list_head", so it would not be quite like "super" :-)

Object-oriented design patterns in the kernel, part 1

Posted Jun 3, 2011 13:25 UTC (Fri) by linusw (subscriber, #40300) [Link]

Yeah, extends or super or whatever. The point is that it is in the object orient.

Object-oriented design patterns in the kernel, part 1

Posted Jun 6, 2011 3:56 UTC (Mon) by jmm82 (guest, #59425) [Link]

It is also used in many kernel subsystems when the base struct will have a pointer to a private struct where the driver can hold variables Specific to that driver. The base struct is like a super in this case and if all you have is a pointer to the private struct you can use container_of() to get the base struct.

Also, container_of is used with the kref code when deleting the struct which has embedded a kref for refence counting.

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 2:19 UTC (Thu) by cmccabe (guest, #60281) [Link]

This was an interesting article. I was always curious about the use of NULL in kernel vtable structures.

The idea of associating multiple vtables with a single object-- to collect related functionality-- reminds me of Google Go's structural subtyping. Why force all the class methods to be in one .h or .java file? We should be able to group the methods in whatever way makes the most sense.

Ah, Google Go

Posted Jun 5, 2011 11:23 UTC (Sun) by khim (subscriber, #9252) [Link]

Well, the Google Go structural subtyping is crippled and limited version of ages old idiom.

Of course it does not mean Google Go is bad language: it's version is simpler and less flexible - but as consequence it's less dangerous too. But still... it's funny how people are becoming excited when fashionable language presets something from languge veteran they forever disliked for some reason.

Ah, Google Go

Posted Jun 16, 2011 0:15 UTC (Thu) by cmccabe (guest, #60281) [Link]

There have been a lot of dynamically typed languages in recent years that have implemented what's called "duck typing." Python and Ruby come to mind, but I'm sure there are others. What is different about Google Go is that it is statically typed. Common Lisp is dynamically typed.


Copyright © 2011, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds