Why? Exceptions are, by definition, rare (otherwise they'd be the rule). So any thought of performance goes out the window as soon as you actually hit the exceptional case.kzinti wrote:because you wouldn't want to be locking each time you throw and catch exceptions.
C++-like exception handling in kernel
Re: C++-like exception handling in kernel
Carpe diem!
Re: C++-like exception handling in kernel
Thanks for pointing this out. From my point of view, the trend of std::expected<> is a sign that many C++ developers are tired of exceptions and would prefer in several contexts (again, not everywhere!) to just use check return values. std::expected<> is a convenient wrapper for return values. I'd personally use it in a C++ project.kzinti wrote:This type of construct is becoming a more and more popular as an alternative to exceptions. It is also reasonably efficient in term of space / runtime overhead (if any), especially when constexpr and rvo are taken into account. So popular that std::expected<> is now an accepted proposal for C++23.
Since I wanted to use std::expected<> in my bootloader (and possibly kernel) and since we don't have C++23 yet, I had to implement it myself. So if anyone is interested, it is available here: https://github.com/kiznit/expected.
Of course, if the project runs in userspace, I'll still use exceptions for some cases. My personal rule of the thumb is that exceptions should be used for really *exceptional* cases. In most of the cases, solutions like std::expected<> could do the job.
Tilck, a Tiny Linux-Compatible Kernel: https://github.com/vvaltchev/tilck
Re: C++-like exception handling in kernel
When and whether or not exceptions should be used is a whole discussion in itself. But when it comes to C++, the main reason one would want to use exceptions is RAII. When using RAII, you initialize objects in constructors. Constructors cannot return values. The only way for a constructor to fail is to throw an exception.nullplan wrote:Why? Exceptions are, by definition, rare (otherwise they'd be the rule). So any thought of performance goes out the window as soon as you actually hit the exceptional case.kzinti wrote:because you wouldn't want to be locking each time you throw and catch exceptions.
So throwing exceptions can happen any time a class constructor has to fail. Or said another way, any time initialization (which includes any resource acquisition) can fail. Whether or not this is common depends on what you are doing and your design. Out of memory allocations should be rare (or arguably never happen in a kernel). What about trying to process some invalid data passed to the constructor? Any API your kernel exposes will receive some parameters from user space and you will at some point have to handle invalid input. It might not be that rare. I can easily imagine a DOS attack from user space exploiting such locks.
But even if exceptions are to be rare (for some definition of rare), locking the whole world (in kernel space!) just because one of the treads on your 8 cores machine with 100 processes decided to throw an exception doesn't seem acceptable to me.
Re: C++-like exception handling in kernel
True. What you do here is, you validate the parameters at the highest appropriate level and return early. There is an engineering principle called "fail faster".kzinti wrote:What about trying to process some invalid data passed to the constructor? Any API your kernel exposes will receive some parameters from user space and you will at some point have to handle invalid input.
You are completely correct that it is a discussion in itself when to use exceptions, but the fact remains that all exception systems I am aware of do not focus on performance after throwing an exception. So designing a program around the idea that exceptions are free is probably a bad idea. In any language, actually.
That may well be true. But that is a property of the allocator rather than the exception system. You say you have dlmalloc. May I interest you in jemalloc? It is essentially dlmalloc, but you put all the global variables into thread-local arenas (I suppose for the kernel you would use CPU-local arenas). Downside is that now each chunk of allocated memory needs to link back to its arena in order to be freed correctly, so the space overhead grows by one machine word per allocated chunk. But otherwise, you do not need to grab a global lock unless you need to allocate an arena for a CPU that does not yet have one (and you can put that part into CPU-initialization, so it never comes up at run-time after that), and other than that you only grab a lock for the arena you are working with, which is typically going to be unshared with any other CPU except for the few blocks you actually do share. And then only the CPUs involved will actually contend for the lock, and not the whole system.kzinti wrote:But even if exceptions are to be rare (for some definition of rare), locking the whole world (in kernel space!) just because one of the treads on your 8 cores machine with 100 processes decided to throw an exception doesn't seem acceptable to me.
Carpe diem!
Re: C++-like exception handling in kernel
I agree with you. All these problems are solvable. But there is a lot of work required to get C++ exceptions handling working properly in the kernel, which is the point I was trying to make. I used dlmalloc as an easy way to complete a proof of concept prototype knowing full well it would need to be replaced / enhanced. I gave up before reaching that point.
-
- Member
- Posts: 426
- Joined: Tue Apr 03, 2018 2:44 am
Re: C++-like exception handling in kernel
Is that a failing of the C++ standard though, and the facilities it provides in handling exceptions?vvaltchev wrote:Thanks for pointing this out. From my point of view, the trend of std::expected<> is a sign that many C++ developers are tired of exceptions and would prefer in several contexts (again, not everywhere!) to just use check return values. std::expected<> is a convenient wrapper for return values. I'd personally use it in a C++ project.kzinti wrote:This type of construct is becoming a more and more popular as an alternative to exceptions. It is also reasonably efficient in term of space / runtime overhead (if any), especially when constexpr and rvo are taken into account. So popular that std::expected<> is now an accepted proposal for C++23.
Since I wanted to use std::expected<> in my bootloader (and possibly kernel) and since we don't have C++23 yet, I had to implement it myself. So if anyone is interested, it is available here: https://github.com/kiznit/expected.
Exceptions can be hard to debug sometimes, because you don't know exactly where they're being thrown from. You have to track down the source of an exception if you haven't encoded that into the exception itself, such as a filename:lineno indicator.
I think the problem stems from the lack of stack trace information from a C++ exception. In Java, whenever I catch an exception, I can generate a nice backtrace to stick in log file, and use that to directly find where and why an exception is being thrown. In my old day job, I used to diagnose failures in systems I couldn't even run locally just based on Java stack traces, it makes support of such products so much easier. My current day job is all C++, and exceptions can be a pain to track down after the fact.
In my kernel, I can generate a stack trace, but I have to do it myself, and my code assumes I'm using a standard frame layout to trace back up the stack frames, so it won't work if I omit the frame pointer, for example.
I just wish C++ (and C) would provide that facility as part of the standard library. The compiler knows the ABI details.
Re: C++-like exception handling in kernel
In my lib I built a set of macros that are just for this purpose. There's one for throw, one for rethrow and a few to create add to the stack in the methods/functions that don't throw or catch. All of them subject to conditional compilation (depending on the debug level, they can just resolve to regular plain old throw or add an increasingly verbose level of debug information, from 0 to 4). It took some doing, and enforces a bit more discipline (to add the macros event to the methods that don't directly use exceptions in order to be able to trace the calls) but it does help a lot with debugging. I know there are a lot of libraries specific for this purpose out there, but most are compiler and platform dependent. This way it is completely independent as it doesn't use any compiler specific functionality neither does it need to be tied to a specific ABI.thewrongchristian wrote:Exceptions can be hard to debug sometimes, because you don't know exactly where they're being thrown from. You have to track down the source of an exception if you haven't encoded that into the exception itself, such as a filename:lineno indicator.
Writing a bootloader in under 15 minutes: https://www.youtube.com/watch?v=0E0FKjvTA0M
Re: C++-like exception handling in kernel
I totally agree that having a standard mechanism for generating backtraces would be useful, but I don't think that's the biggest problem of exceptions. The biggest one, IMHO, is that it's not easy to write perfectly exception-safe code. It's easy to miss subtle corner cases. If everything can throw, than it's super tricky to write destructors because calling code that might throw there is super dangerous: C++ doesn't support nested exceptions, therefore, if an exception is thrown during the stack unwinding (caused by another exception), the program is aborted. And noexcept doesn't solve this problem properly.thewrongchristian wrote:Is that a failing of the C++ standard though, and the facilities it provides in handling exceptions?vvaltchev wrote:Thanks for pointing this out. From my point of view, the trend of std::expected<> is a sign that many C++ developers are tired of exceptions and would prefer in several contexts (again, not everywhere!) to just use check return values. std::expected<> is a convenient wrapper for return values. I'd personally use it in a C++ project.kzinti wrote:This type of construct is becoming a more and more popular as an alternative to exceptions. It is also reasonably efficient in term of space / runtime overhead (if any), especially when constexpr and rvo are taken into account. So popular that std::expected<> is now an accepted proposal for C++23.
Since I wanted to use std::expected<> in my bootloader (and possibly kernel) and since we don't have C++23 yet, I had to implement it myself. So if anyone is interested, it is available here: https://github.com/kiznit/expected.
Exceptions can be hard to debug sometimes, because you don't know exactly where they're being thrown from. You have to track down the source of an exception if you haven't encoded that into the exception itself, such as a filename:lineno indicator.
I think the problem stems from the lack of stack trace information from a C++ exception. In Java, whenever I catch an exception, I can generate a nice backtrace to stick in log file, and use that to directly find where and why an exception is being thrown. In my old day job, I used to diagnose failures in systems I couldn't even run locally just based on Java stack traces, it makes support of such products so much easier. My current day job is all C++, and exceptions can be a pain to track down after the fact.
In my kernel, I can generate a stack trace, but I have to do it myself, and my code assumes I'm using a standard frame layout to trace back up the stack frames, so it won't work if I omit the frame pointer, for example.
I just wish C++ (and C) would provide that facility as part of the standard library. The compiler knows the ABI details.
What we'd need is to put the boolean value of noexcept in the ABI and having compile-time checks that some path checks, cannot throw anything. It's similar (to some degree) to what we have in Dlang, C# and other languages: by default you write only safe code which cannot do pointer arithmetic, but if you mark the function as "unsafe" you can. Now, from "unsafe" functions, you can call everything, while from "safe" function you can only all other "safe" functions. How to reach unsafe functions? Well, some "safe" functions act as a bridge and are marked as "trusted". Those can be called by any "safe" function, but they are allowed to call "unsafe" code.
If we had something for the C++ exceptions it will be great: noexpect(true) functions can only call noexcept(true) functions and special "trusted" functions/methods. noexpect(false) functions can call anything. And this requires an ABI change because if you declare a noexcept(true) a function defined in another translation unit, you should get a linkage error.
With that, we could mark all the destructors and many other functions as noexcept(false) and proceed forward. I had actually plans for implementing a plugins for the clang static analyzer to check that, but never had the time. Over the years, my focus shifted away from C++. Still, I believe it would be a great idea to have such a tool. Compile time checks are infinitely better than runtime checks.
Tilck, a Tiny Linux-Compatible Kernel: https://github.com/vvaltchev/tilck
Re: C++-like exception handling in kernel
Well, it is well known (even for me, a C guy) that C++ destructors should really not throw because of that problem. There is also the philosophical problem "what does it mean if destruction fails?", that most people probably don't want to tackle (it is similar to "what happens if close() fails? Is the FD closed or not?")vvaltchev wrote: If everything can throw, than it's super tricky to write destructors because calling code that might throw there is super dangerous: C++ doesn't support nested exceptions, therefore, if an exception is thrown during the stack unwinding (caused by another exception), the program is aborted.
But I believe you miss the point of "noexcept": It is a promise from the programmer to the compiler that the code will not throw. If it does anyway, that is undefined behavior (though the C++ people like to bandy the term "ill-formed program" about). The core of C has always been to trust the programmer, and the C++ people have kept that alive. "noexcept" is not a magical exception repellent. It is similar to "restrict" in C99: You have to ensure the promise is kept, and if not, things break. You can even call functions that throw in case of invalid arguments if you ensure your arguments are always valid.
Having a tool that detects such unsafe calls might be good idea, but would probably be very noisy for the above reason. But that tool does not belong in the compiler for the same reason.
I was about to ask how other languages solve the same problem, but then remembered that most other OO languages use garbage collection and don't have the problem of throwing destructors, because they have no destructors.
Carpe diem!
Re: C++-like exception handling in kernel
Yep, I agree about the philosophical problem. But no matter what, dtors should not throw. Ultimately, if dtors have critical code that should release critical resources and that operation might fail, the whole code has to be re-designed to not use dtors, but explicit clean-up methods or, dtors can enqueue the real clean-up code for async processing in another thread. It should be possible to transfer the ownership of the inner object (e.g. moving an unique_ptr etc.)nullplan wrote:Well, it is well known (even for me, a C guy) that C++ destructors should really not throw because of that problem. There is also the philosophical problem "what does it mean if destruction fails?", that most people probably don't want to tackle (it is similar to "what happens if close() fails? Is the FD closed or not?")
I'm not missing the point. I don't like that design decision, as I don't like almost anything around UB, as you probably already know from my infamous thread I don't like the idea of making a promise that could or could not be honored, given that, theoretically, we know this at compile time.nullplan wrote:But I believe you miss the point of "noexcept": It is a promise from the programmer to the compiler that the code will not throw. If it does anyway, that is undefined behavior (though the C++ people like to bandy the term "ill-formed program" about). The core of C has always been to trust the programmer, and the C++ people have kept that alive. "noexcept" is not a magical exception repellent. It is similar to "restrict" in C99: You have to ensure the promise is kept, and if not, things break. You can even call functions that throw in case of invalid arguments if you ensure your arguments are always valid.
I disagree. If you always honor the promise to not throw in noexcept functions, you'll get 0 warnings/errors. That's the whole point: enforce the rules in 100% of the cases. You can still have "trusted" functions that can call throwing functions and be called by noexcept, because they promise to catch all the exceptions. And here we open another can of worms. How can those trusted functions be sure they'll catch all the exceptions, unless they use catch(...) ? Well, that's harder. It would seem possible to add to the ABI the list of possible exceptions that a function (directly or not) might throw.. but in reality that would mean having some functions which can throw 100s of different exceptions and that's non-sense. It might be OK to check that with static analysis, but not in the ABI. Therefore, we could add just 2 bits of info in ABI for 3 cases: { except, noexcept, trusted }. It has already been done for const (in order to have deep const-correctness), I don't know why nobody wanted to do the same for noexcept.nullplan wrote:Having a tool that detects such unsafe calls might be good idea, but would probably be very noisy for the above reason. But that tool does not belong in the compiler for the same reason.
Yep, exactly. It's really an unsolved problem.nullplan wrote:I was about to ask how other languages solve the same problem, but then remembered that most other OO languages use garbage collection and don't have the problem of throwing destructors, because they have no destructors.
Tilck, a Tiny Linux-Compatible Kernel: https://github.com/vvaltchev/tilck
Re: C++-like exception handling in kernel
Here's what I don't understand. SEC. 14.5 of the C++ standard (C++20) says:vvaltchev wrote:Yep, I agree about the philosophical problem. But no matter what, dtors should not throw. Ultimately, if dtors have critical code that should release critical resources and that operation might fail, the whole code has to be re-designed to not use dtors, but explicit clean-up methods or, dtors can enqueue the real clean-up code for async processing in another thread. It should be possible to transfer the ownership of the inner object (e.g. moving an unique_ptr etc.)nullplan wrote:Well, it is well known (even for me, a C guy) that C++ destructors should really not throw because of that problem. There is also the philosophical problem "what does it mean if destruction fails?", that most people probably don't want to tackle (it is similar to "what happens if close() fails? Is the FD closed or not?")
I'm not missing the point. I don't like that design decision, as I don't like almost anything around UB, as you probably already know from my infamous thread I don't like the idea of making a promise that could or could not be honored, given that, theoretically, we know this at compile time.nullplan wrote:But I believe you miss the point of "noexcept": It is a promise from the programmer to the compiler that the code will not throw. If it does anyway, that is undefined behavior (though the C++ people like to bandy the term "ill-formed program" about). The core of C has always been to trust the programmer, and the C++ people have kept that alive. "noexcept" is not a magical exception repellent. It is similar to "restrict" in C99: You have to ensure the promise is kept, and if not, things break. You can even call functions that throw in case of invalid arguments if you ensure your arguments are always valid.
I disagree. If you always honor the promise to not throw in noexcept functions, you'll get 0 warnings/errors. That's the whole point: enforce the rules in 100% of the cases. You can still have "trusted" functions that can call throwing functions and be called by noexcept, because they promise to catch all the exceptions. And here we open another can of worms. How can those trusted functions be sure they'll catch all the exceptions, unless they use catch(...) ? Well, that's harder. It would seem possible to add to the ABI the list of possible exceptions that a function (directly or not) might throw.. but in reality that would mean having some functions which can throw 100s of different exceptions and that's non-sense. It might be OK to check that with static analysis, but not in the ABI. Therefore, we could add just 2 bits of info in ABI for 3 cases: { except, noexcept, trusted }. It has already been done for const (in order to have deep const-correctness), I don't know why nobody wanted to do the same for noexcept.nullplan wrote:Having a tool that detects such unsafe calls might be good idea, but would probably be very noisy for the above reason. But that tool does not belong in the compiler for the same reason.
Yep, exactly. It's really an unsolved problem.nullplan wrote:I was about to ask how other languages solve the same problem, but then remembered that most other OO languages use garbage collection and don't have the problem of throwing destructors, because they have no destructors.
My question is: why? If I mark a function as noexcept, the compiler should enforce that contract, just as it would enforce the contract between me and me calling a function marked with the nodiscard attribute. A function declared as noexcept should not be allowed to call a function that the compiler knows could throw an exception, just as the throw keyword should be illegal in those functions marked noexcept. Or, if not illegal, a diagnostic should still be issued. This just feels like common sense to me.Whenever an exception is thrown and the search for a handler (14.4) encounters the outermost block of a function with a non-throwing exception specification, the function std::terminate is called (14.6.2).
[Note 1 : An implementation is not permitted to reject an expression merely because, when executed, it throws or might throw an exception from a function with a non-throwing exception specification. — end note]
Re: C++-like exception handling in kernel
Yep, I agree 100%. The standard committee probably didn't want to ask the compilers to make an ABI change to support the new feature. It would be easy to enforce that in a single TU, but it would require an ABI change to do that across TUs. Still a bad decision, IMHO though.Ethin wrote:My question is: why? If I mark a function as noexcept, the compiler should enforce that contract, just as it would enforce the contract between me and me calling a function marked with the nodiscard attribute. A function declared as noexcept should not be allowed to call a function that the compiler knows could throw an exception, just as the throw keyword should be illegal in those functions marked noexcept. Or, if not illegal, a diagnostic should still be issued. This just feels like common sense to me.
Tilck, a Tiny Linux-Compatible Kernel: https://github.com/vvaltchev/tilck
Re: C++-like exception handling in kernel
I disagree. No ABI change would be required. Just make the program ill-formed/issue a warning under those conditions. Since the compiler (usually) has the entire program available to it, it can go through all the function definitions and determine which ones throw an exception and which ones don't. Then, find all the functions that are marked noexcept. If those noexcept-specified functions call others that the compiler knows throw exceptions, issue a diagnostic or abort compilation.vvaltchev wrote:Yep, I agree 100%. The standard committee probably didn't want to ask the compilers to make an ABI change to support the new feature. It would be easy to enforce that in a single TU, but it would require an ABI change to do that across TUs. Still a bad decision, IMHO though.Ethin wrote:My question is: why? If I mark a function as noexcept, the compiler should enforce that contract, just as it would enforce the contract between me and me calling a function marked with the nodiscard attribute. A function declared as noexcept should not be allowed to call a function that the compiler knows could throw an exception, just as the throw keyword should be illegal in those functions marked noexcept. Or, if not illegal, a diagnostic should still be issued. This just feels like common sense to me.
Re: C++-like exception handling in kernel
Without an ABI change, it would be possible to cheat easily by defining a function as noexcept(true) in the C++ file, and declaring it as noexcept(false) in the header, or locally inside another C++ file. The point of adding those things in the ABI is to get a linkage error if you try to cheat, or just genuinely make a mistake by changing the implementation site without fixing the header or vice-versa.Ethin wrote:I disagree. No ABI change would be required. Just make the program ill-formed/issue a warning under those conditions. Since the compiler (usually) has the entire program available to it, it can go through all the function definitions and determine which ones throw an exception and which ones don't. Then, find all the functions that are marked noexcept. If those noexcept-specified functions call others that the compiler knows throw exceptions, issue a diagnostic or abort compilation.
Tilck, a Tiny Linux-Compatible Kernel: https://github.com/vvaltchev/tilck
Re: C++-like exception handling in kernel
Again, I must disagree -- I think your overthinking the solution to this problem. Preventing violations of noexcept is as simple as ensuring that signatures -- including the noexcept attribute -- match. You shouldn't need to modify the ABI, only the language.vvaltchev wrote:Without an ABI change, it would be possible to cheat easily by defining a function as noexcept(true) in the C++ file, and declaring it as noexcept(false) in the header, or locally inside another C++ file. The point of adding those things in the ABI is to get a linkage error if you try to cheat, or just genuinely make a mistake by changing the implementation site without fixing the header or vice-versa.Ethin wrote:I disagree. No ABI change would be required. Just make the program ill-formed/issue a warning under those conditions. Since the compiler (usually) has the entire program available to it, it can go through all the function definitions and determine which ones throw an exception and which ones don't. Then, find all the functions that are marked noexcept. If those noexcept-specified functions call others that the compiler knows throw exceptions, issue a diagnostic or abort compilation.