Skip to content
This repository was archived by the owner on Oct 12, 2022. It is now read-only.

Make AssertError a singleton #1710

Closed
wants to merge 6 commits into from
Closed

Conversation

andralex
Copy link
Member

This makes AssertError a singleton, which allows people to use assert in higher-level applications without needing to link in the garbage collector.

This could break code that calls onAssertError repeatedly and assumes it returns separate objects. That's quite an unusual way to go about things, and it seems we need not guarantee it.

If this approach works we can (a) publish singleton so as to make it available to clients, and (b) convert RangeError to a singleton as well so we can throw range errors without worrying about memory allocations.

if( _assertHandler is null )
throw new AssertError( file, line );
_assertHandler( file, line, null);
onAssertErrorMsg(file, line, null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should really be done on the compiler level one day

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mathias-lang-sociomantic yes, and it will

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WalterBright : Having two function seems unnecessary, as the case of assert(cond) could just be treated as assert(cond, null). That's one less function between DMD and druntime, so less complication.
Also it's a common source of frustration that when debugging we need to break both on _d_assert and _d_assert_msg.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yah, that's what was happening anyway (with duplicated code)

@@ -421,6 +432,22 @@ deprecated void setAssertHandler( AssertHandler h ) @trusted nothrow @nogc
// Overridable Callbacks
///////////////////////////////////////////////////////////////////////////////

private C singleton(C)() if (is(C == class) && is(typeof(new C)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation and unittests please

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unittests are there... documentation not needed for private functions :)

@andralex
Copy link
Member Author

Added support for destruction via atexit.

@andralex
Copy link
Member Author

,,, @mathias-lang-sociomantic and dox :)

assert(typeid(C).initializer.length == b.length * size_t.sizeof);
import core.stdc.string;
memcpy(b.ptr, typeid(C).init.ptr, b.length);
result.__ctor;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't that mean that the ctor can be @system and do bad stuff ? ™️
Ditto for the dtor.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're right brb

@tsbockman
Copy link
Contributor

This makes AssertError a singleton

When I read that, I immediately wondered about the potential race condition if multiple threads were to assert simultaneously. Then I saw:

repeated calls to singleton!C within a given thread return the same instance

Is it correct to refer to an object as a "singleton" when there's actually one per thread? It's very confusing, etymologically speaking.

@andralex
Copy link
Member Author

@tsbockman good point thx, changed

@@ -416,12 +427,48 @@ deprecated void setAssertHandler( AssertHandler h ) @trusted nothrow @nogc
assertHandler = h;
}

/*
Returns a perThreadInstance object of class type `C`, i.e. repeated calls to
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the standard Ddoc function documentation style.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point the function is private so I just added a casual comment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added nicer dox

}();
result.__ctor;
import core.internal.traits;
static if (hasElaborateDestructor!C)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to a quick test:

import std.traits, core.exception;
static assert(hasElaborateDestructor!AssertError);

// Error: static assert  (hasElaborateDestructor!(AssertError)) is false

So it would be nice to have this line covered. Otherwise, LGTM.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I think we can defer coverage of that to the time we get to use this private function elsewhere.

@andralex
Copy link
Member Author

@mathias-lang-sociomantic added coverage

@dushibaiyu
Copy link

I think other Error should be singleton too.

}();
result.__ctor;
import core.internal.traits;
static if (hasElaborateDestructor!C)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasElaborateDestructor!C == false if C is not a struct. That's why I proposed dlang/phobos#4119.

static size_t[1 + (__traits(classInstanceSize, C) - 1) / size_t.sizeof] b;
static bool initialized = false;
auto result = () @trusted { return cast(C) b.ptr; }();
if (!initialized)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not do an early exit:

if (initialized)
    return result;

Is it just because you don't want to type return result; twice?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm glad you're giving the same advice I often do :). I'd say when the if is short things are fine either way.


Throws: If the default constructor of `C` throws.
*/
private C perThreadInstance(C)() if (is(C == class) && is(typeof(new C)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There already is a staticError which covers pretty much the same use case. Perhaps you can reuse it / replace it / unify the implementations?

{
static size_t[1 + (__traits(classInstanceSize, C) - 1) / size_t.sizeof] b;
static bool initialized = false;
auto result = () @trusted { return cast(C) b.ptr; }();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're druntime, I think it's worth going the extra mile (i.e. do the necessary safe-ty hacks - see line 760) in order to return immutable C or tag the function with pure. @nogc, pure and nothrow are orthogonal in the language and therefore I think it's a good idea to allow @nogc pure functions to throw staticErrors. (To me personally, throw staicError!RangeError sound better, than throw perThreadInstance!RangeError, but naming is not that important at this stage.)

static if (is(typeof(result.__ctor())))
result.__ctor;
import core.internal.traits;
static if (hasElaborateDestructor!C)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasElaborateDestructor!C always returns false if C is not a struct (even the one in druntime). That's why I proposed dlang/phobos#4119.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm... I think just remove this static if. There is always a destructor for a class (i.e. rt_finalize2 will always call destructors, even for Object).

import core.stdc.stdlib;
static void destroy()
{
(cast(C) b.ptr).__dtor;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't want to leverage object.destroy, you should at least use rt_finalize2, because that's what object.destroy does for classes.
The problem with calling obj.__dtor directly is that:

  • It does a non-virtual call (see 1 and 2) to C.~this(), without calling any of C's base classes' dtors.
  • It does not destroy C's __monitor field.
  • It does not clear the memory, causing the GC to attempt destroy (this time "properly") the object again - see this (reproduces perfectly on newer compilers too).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, this was actually a problem with destroy before we fixed it.

*/
private C perThreadInstance(C)() if (is(C == class) && is(typeof(new C)))
{
static size_t[1 + (__traits(classInstanceSize, C) - 1) / size_t.sizeof] b;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would have been simpler to do ubyte[__traits(classInstanceSize, C] b. Why do you want to round up to a multiple of size_t

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alignment

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be sufficiently paranoid, better add a check for the instance alignment not being larger than size_t then (SIMD vectors, …).

import core.stdc.string;
() @trusted
{
memcpy(b.ptr, typeid(C).initializer.ptr, b.length);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory the following could be more efficient auto ci = typeid(C); b[0 .. ci.initializer.length] = ci.initializer[] (assuming b is of type ubyte[]), because the compiler knows at compile time typeid(C).initializer.length and can make a better decision about the codegen (e.g. potentially inline the memcpy for smaller types).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code will be executed once per run really

memcpy(b.ptr, typeid(C).initializer.ptr, b.length);
}();
static if (is(typeof(result.__ctor())))
result.__ctor;
Copy link
Member

@PetarKirov PetarKirov Dec 13, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may need to make sure that the BlkAttr for this piece of memory has the BlkAttr.FINALIZE bit unset, otherwise the GC may attempt to call the dtors again at the end of the program.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... this is not GC-allocated memory so it does not have block attributes. No?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andralex correct.

@PetarKirov
Copy link
Member

Ping @andralex. I need perThreadInstance to continue my quest on making more of druntime @nogc and nothrow (see #1726 and #1728).

Copy link
Member

@MartinNowak MartinNowak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use/adopt staticError, it was build for that purpose.

Copy link
Member

@schveiguy schveiguy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Just need some tweaks to destruction, then it should be good to go.

memcpy(b.ptr, typeid(C).initializer.ptr, b.length);
}();
static if (is(typeof(result.__ctor())))
result.__ctor;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andralex correct.

import core.stdc.stdlib;
static void destroy()
{
(cast(C) b.ptr).__dtor;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, this was actually a problem with destroy before we fixed it.

static if (is(typeof(result.__ctor())))
result.__ctor;
import core.internal.traits;
static if (hasElaborateDestructor!C)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm... I think just remove this static if. There is always a destructor for a class (i.e. rt_finalize2 will always call destructors, even for Object).

@schveiguy
Copy link
Member

Note, this will close https://issues.dlang.org/show_bug.cgi?id=4587

@schveiguy
Copy link
Member

schveiguy commented May 8, 2017

Please use/adopt staticError, it was build for that purpose.

Hm... I think actually staticError may be better off using perThreadInstance. It solves some problems (i.e. the note about chaining), and uses exact memory size vs. guessing for all possible Error types.

@andralex, looking at how staticError works, I think using a void static array may be cleaner than size_t array. two reasons: 1) no math needed for size, so it looks cleaner. 2) precise scanning may care one day.

@rainers
Copy link
Member

rainers commented May 8, 2017

precise scanning may care one day.

It already does, see #1798. For the buffer to still be scanned with proper alignment, it should be an array of void*.

@rainers
Copy link
Member

rainers commented May 8, 2017

I think that there should also be a test showing what happens if an assert triggers during unwinding an AssertError. It doesn't have to support perfect chaining, but it should somehow pass the initial error along.

@andralex
Copy link
Member Author

superseded

@andralex andralex closed this Oct 31, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Needs Rebase needs a `git rebase` performed Needs Work stalled
Projects
None yet
Development

Successfully merging this pull request may close these issues.