LWN.net Logo

Object-oriented design patterns in the kernel, part 1

Object-oriented design patterns in the kernel, part 1

Posted Jun 2, 2011 0:23 UTC (Thu) by Cyberax (✭ supporter ✭, #52523)
In reply to: Object-oriented design patterns in the kernel, part 1 by pr1268
Parent article: Object-oriented design patterns in the kernel, part 1

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.


(Log in to post comments)

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 (subscriber, #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 (subscriber, #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 (subscriber, #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 (subscriber, #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 (guest, #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 (guest, #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 (guest, #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 (guest, #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 michaeljt (subscriber, #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 michaeljt (subscriber, #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.

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