digitalmars.D - Override assert handler
- Manu (14/14) Aug 17 So I've declared a local function:
- ryuukk_ (28/28) Aug 17 ```
- Manu (11/39) Aug 17 Orly? My project is rather more complex than this... I'll give it some m...
- ryuukk_ (5/59) Aug 17 I use custom runtime + `-betterC`, but this also works on
- kinke (22/29) Aug 17 You can set the handler in a CRT constructor, which avoids any
- Manu (7/36) Aug 17 That's a reasonable solution. Thanks for the tip!
- Walter Bright (3/9) Aug 17 Yes, that is a botched design that makes things much more complicated th...
- Walter Bright (13/13) Aug 17 Allow me to explain how a library works.
- Richard (Rikki) Andrew Cattermole (17/34) Aug 18 This needs some context.
- Johan (5/21) Aug 18 And there is of course inlining that needs to be disabled for an
- Walter Bright (2/4) Aug 19 pragma(inline, false)
- Walter Bright (36/36) Aug 19 I actually do understand how shared libraries work.
- Richard (Rikki) Andrew Cattermole (33/77) Aug 19 Its not quite as simple as that. For Windows yes that's how it'll work
- Ogi (14/16) Aug 19 As an option, you can just catch it:
So I've declared a local function: extern(C) void _d_assert_msg (string msg, string file, uint line) Intent is to replace the druntime assert handler. I declared this function and the program builds and links, but it still calls the druntime function. 1. I'm not sure exactly why linking succeeds... I would have expected a link error with multiple defined symbols... should be marked with weak linkage such that a user-override will be accepted when supplied, but that seems not to be the case right now...? Have people done this in their own programs? What is the best practice? On a related note; _d_assert_msg is missing an argument; it supplies msg, line, file, but it also needs to receive another argument which is the stringified assert expression that failed.
Aug 17
``` import core.stdc.stdio; void main() { assert(false, "no"); } extern(C) void _d_assert_msg (string msg, string file, uint line) { printf("assert failed: %.*s:%u %.*s\n", cast(int)file.length, file.ptr, line, cast(int)msg.length, msg.ptr); } ``` ``` assert failed: onlineapp.d:4 no ``` This works That's funny, because i was thinking about that just right now, and how the default message of druntime sucks ass ``` core.exception.AssertError onlineapp.d(5): no ---------------- ??:? _d_assert_msg [0x5625e324e580] ./onlineapp.d:5 _Dmain [0x5625e324e4e4] ``` assert message is obfuscated and surrounded with unpleasant characters to read, it should be clear by default, i want to send a PR but i can't be bothered to read druntime codebase, a waste of time
Aug 17
On Sat, 17 Aug 2024 at 18:42, ryuukk_ via Digitalmars-d < digitalmars-d puremagic.com> wrote:``` import core.stdc.stdio; void main() { assert(false, "no"); } extern(C) void _d_assert_msg (string msg, string file, uint line) { printf("assert failed: %.*s:%u %.*s\n", cast(int)file.length, file.ptr, line, cast(int)msg.length, msg.ptr); } ``` ``` assert failed: onlineapp.d:4 no ``` This works That's funny, because i was thinking about that just right now, and how the default message of druntime sucks ass ``` core.exception.AssertError onlineapp.d(5): no ---------------- ??:? _d_assert_msg [0x5625e324e580] ./onlineapp.d:5 _Dmain [0x5625e324e4e4] ``` assert message is obfuscated and surrounded with unpleasant characters to read, it should be clear by default, i want to send a PR but i can't be bothered to read druntime codebase, a waste of timeOrly? My project is rather more complex than this... I'll give it some more time to find where it goes sideways. What was your build cmdline? Did you link druntime? I mean, there's also the `assertHandler()` stuff, but I don't really see the point; I'd rather just replace the assert handler symbol directly. Using `assertHandler` is awkward because you need a static constructor to register it at runtime, and then anything you do in your assert handler almost inevitably leads to bootup issues with cyclic module dependencies because everything leads back to assert.
Aug 17
On Saturday, 17 August 2024 at 09:31:53 UTC, Manu wrote:On Sat, 17 Aug 2024 at 18:42, ryuukk_ via Digitalmars-d < digitalmars-d puremagic.com> wrote:I use custom runtime + `-betterC`, but this also works on run.dlang.io with no specific flags Do you use `-betterC`? Do you load a dll? it perhaps overwrite the symbol?``` import core.stdc.stdio; void main() { assert(false, "no"); } extern(C) void _d_assert_msg (string msg, string file, uint line) { printf("assert failed: %.*s:%u %.*s\n", cast(int)file.length, file.ptr, line, cast(int)msg.length, msg.ptr); } ``` ``` assert failed: onlineapp.d:4 no ``` This works That's funny, because i was thinking about that just right now, and how the default message of druntime sucks ass ``` core.exception.AssertError onlineapp.d(5): no ---------------- ??:? _d_assert_msg [0x5625e324e580] ./onlineapp.d:5 _Dmain [0x5625e324e4e4] ``` assert message is obfuscated and surrounded with unpleasant characters to read, it should be clear by default, i want to send a PR but i can't be bothered to read druntime codebase, a waste of timeOrly? My project is rather more complex than this... I'll give it some more time to find where it goes sideways. What was your build cmdline? Did you link druntime? I mean, there's also the `assertHandler()` stuff, but I don't really see the point; I'd rather just replace the assert handler symbol directly. Using `assertHandler` is awkward because you need a static constructor to register it at runtime, and then anything you do in your assert handler almost inevitably leads to bootup issues with cyclic module dependencies because everything leads back to assert.
Aug 17
On Saturday, 17 August 2024 at 09:31:53 UTC, Manu wrote:I mean, there's also the `assertHandler()` stuff, but I don't really see the point; I'd rather just replace the assert handler symbol directly. Using `assertHandler` is awkward because you need a static constructor to register it at runtime, and then anything you do in your assert handler almost inevitably leads to bootup issues with cyclic module dependencies because everything leads back to assert.You can set the handler in a CRT constructor, which avoids any cycles and makes sure it is installed before initializing druntime (`rt_init()`), so should really cover all asserts: ``` pragma(crt_constructor) void setupAssertHandler() { import core.exception : assertHandler; assertHandler = &myAssertHandler; } void myAssertHandler(string file, size_t line, string msg) nothrow { // print... import core.stdc.stdlib; abort(); // or something like that } ``` ` core.attribute.weak` isn't implemented by DMD, plus needs emulation on Windows (done by LDC) with according limitations. The extra indirection via that custom assert handler in druntime works e.g. for a pre-linked druntime DLL too.
Aug 17
On Sat, 17 Aug 2024 at 22:46, kinke via Digitalmars-d < digitalmars-d puremagic.com> wrote:On Saturday, 17 August 2024 at 09:31:53 UTC, Manu wrote:That's a reasonable solution. Thanks for the tip! I'd still prefer to just replace a weak library call though, so that calls to assert are direct and don't add a bunch of extra layers to the callstack. I'd also really like it if the condition were stringified and handed to the assert handler... that seems like a weird oversight?I mean, there's also the `assertHandler()` stuff, but I don't really see the point; I'd rather just replace the assert handler symbol directly. Using `assertHandler` is awkward because you need a static constructor to register it at runtime, and then anything you do in your assert handler almost inevitably leads to bootup issues with cyclic module dependencies because everything leads back to assert.You can set the handler in a CRT constructor, which avoids any cycles and makes sure it is installed before initializing druntime (`rt_init()`), so should really cover all asserts: ``` pragma(crt_constructor) void setupAssertHandler() { import core.exception : assertHandler; assertHandler = &myAssertHandler; } void myAssertHandler(string file, size_t line, string msg) nothrow { // print... import core.stdc.stdlib; abort(); // or something like that } ``` ` core.attribute.weak` isn't implemented by DMD, plus needs emulation on Windows (done by LDC) with according limitations. The extra indirection via that custom assert handler in druntime works e.g. for a pre-linked druntime DLL too.
Aug 17
On 8/17/2024 2:31 AM, Manu wrote:I mean, there's also the `assertHandler()` stuff, but I don't really see the point; I'd rather just replace the assert handler symbol directly. Using `assertHandler` is awkward because you need a static constructor to register it at runtime, and then anything you do in your assert handler almost inevitably leads to bootup issues with cyclic module dependencies because everything leads back to assert.Yes, that is a botched design that makes things much more complicated than necessary.
Aug 17
Allow me to explain how a library works. The linker resolves all the symbols in the object files given to it. When there are references to symbols, but no symbols, then (and only then) does the linker consult the library. The linker will pull in modules from the library that export those missing symbols. Therefore, the easiest way to override a function that's in the library is to write your own and put it in the list of object files for the linker to incorporate. Unfortunately, some early work done on druntime was not aware of this, and so the assert handler is set up as a pointer to a function, with another function to call to set that pointer to your own function. By the time I noticed that, it was too late to fix it. I have spent nearly the entire time I've been working on compilers trying and failing to explain how linkers work.
Aug 17
On 18/08/2024 1:53 PM, Walter Bright wrote:Allow me to explain how a library works. The linker resolves all the symbols in the object files given to it. When there are references to symbols, but no symbols, then (and only then) does the linker consult the library. The linker will pull in modules from the library that export those missing symbols. Therefore, the easiest way to override a function that's in the library is to write your own and put it in the list of object files for the linker to incorporate.This needs some context. It only applies to object files and executables, the moment shared libraries enter the picture (such as druntime) this no longer is the full story. Once shared libraries enter, you have to consult the image loader as well. If you have a symbol that overrides another it is first come first served and loading is dependency aware. If the process has been up for a month already, a replacement symbol will not replace the existing one. It isn't safe. The loader would effectively have the ability to unmap a currently executing function. Needless to say that is a bad thing to do on loading of a binary.Unfortunately, some early work done on druntime was not aware of this, and so the assert handler is set up as a pointer to a function, with another function to call to set that pointer to your own function.Unfortunately there is a broader context, and it is my belief that this was the correct solution. It works in more situations and still allows you via ldc's `` weak`` (see Martin's reply) to replace the initializer during initial link.By the time I noticed that, it was too late to fix it. I have spent nearly the entire time I've been working on compilers trying and failing to explain how linkers work.And now I get to explain how loaders work ;)
Aug 18
On Sunday, 18 August 2024 at 17:22:44 UTC, Richard (Rikki) Andrew Cattermole wrote:On 18/08/2024 1:53 PM, Walter Bright wrote:And there is of course inlining that needs to be disabled for an "overridable" function. weak does that too. -JohanAllow me to explain how a library works. The linker resolves all the symbols in the object files given to it. When there are references to symbols, but no symbols, then (and only then) does the linker consult the library. The linker will pull in modules from the library that export those missing symbols. Therefore, the easiest way to override a function that's in the library is to write your own and put it in the list of object files for the linker to incorporate.This needs some context. It only applies to object files and executables, the moment shared libraries enter the picture (such as druntime) this no longer is the full story.
Aug 18
On 8/18/2024 1:29 PM, Johan wrote:And there is of course inlining that needs to be disabled for an "overridable" function. weak does that too.pragma(inline, false)
Aug 19
I actually do understand how shared libraries work. What happens by default when an assert failure happens is the function: ``` core.exception._d_assertp(immutable(char*) file, uint line); ``` is called. That forwards the call to: ``` core.exception.onAssertError(string file, size_t line); ``` which then forwards the call to: ``` (*_assertHandler)(file,line,null); ``` and core.exception._assertHandler is the pointer to the function. The default behavior of _assertHandler is: ``` throw staticError!AssertError(file, line); ``` Therefore, if you write your own _d_assertp function in the executable, it will override the library version in your executable. For code in the shared library, the shared library _d_assertp will be called. ------ To understand assert error handling, it's necessary to understand: ``` AssertHandler assertHandler assertHandler (yes, two of them!) _assertHandler onAssertError onAssertErrorMsg AssertError _d_assertp _d_assert_msg _d_assert ``` which is overly complex.
Aug 19
On 20/08/2024 9:03 AM, Walter Bright wrote:I actually do understand how shared libraries work. What happens by default when an assert failure happens is the function: ``` core.exception._d_assertp(immutable(char*) file, uint line); ``` is called. That forwards the call to: ``` core.exception.onAssertError(string file, size_t line); ``` which then forwards the call to: ``` (*_assertHandler)(file,line,null); ``` and core.exception._assertHandler is the pointer to the function. The default behavior of _assertHandler is: ``` throw staticError!AssertError(file, line); ``` Therefore, if you write your own _d_assertp function in the executable, it will override the library version in your executable. For code in the shared library, the shared library _d_assertp will be called.Its not quite as simple as that. For Windows yes that's how it'll work without compiler & linker assistance. "Symbols so introduced may duplicate symbols already defined by the program or previous dlopen() operations. To resolve the ambiguities such a situation might present, the resolution of a symbol reference to symbol definition is based on a symbol resolution order. Two such resolution orders are defined: load or dependency ordering. Load order establishes an ordering among symbol definitions, such that the definition first loaded (including definitions from the image file and any dependent objects loaded with it) has priority over objects added later (via dlopen()). Load ordering is used in relocation processing. Dependency ordering uses a breadth-first order starting with a given object, then all of its dependencies, then any dependents of those, iterating until all dependencies are satisfied. With the exception of the global symbol object obtained via a dlopen() operation on a file of 0, dependency ordering is used by the dlsym() function. Load ordering is used in dlsym() operations upon the global symbol object." https://pubs.opengroup.org/onlinepubs/009695399/functions/dlopen.html If your ``_d_assertp`` was first seen it would apply to the entire process if the symbol was exported. If the shared library symbol was first seen, its the one that gets used no matter what image is being discussed as long as everything is exported. Of course, the Posix behavior of setting the assert handler for the entire process is what you want. Not the Windows one where it is localized to the binary. After all it doesn't matter who errors out, you want to act upon it the same way consistently. Which brings us back to what I already said, the function pointer design works best and doesn't limit you into a subset of desirable situations. It also covers the use case where you want to swap it out in a process that has been running for a month. Not to mention it's fully portable and won't change behavior based upon platform.------ To understand assert error handling, it's necessary to understand: ``` AssertHandler assertHandler assertHandler (yes, two of them!) _assertHandler onAssertError onAssertErrorMsg AssertError _d_assertp _d_assert_msg _d_assert ``` which is overly complex.Agreed, there may be some simplification possible.
Aug 19
On Saturday, 17 August 2024 at 08:10:26 UTC, Manu wrote:Have people done this in their own programs? What is the best practice?As an option, you can just catch it: ```D void main() { import core.exception : AssertError; try { assert(true == false); } catch (AssertError e) { writeln("Oh no! ", e.msg); } } ```
Aug 19