-
-
Notifications
You must be signed in to change notification settings - Fork 610
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix Issue 14650 - Destructors are not called on global variables #9151
Conversation
|
Thanks for your pull request and interest in making D better, @RazvanN7! We are looking forward to reviewing it, and you should be hearing from a maintainer soon.
Please see CONTRIBUTING.md for more information. If you have addressed all reviews or aren't sure how to proceed, don't hesitate to ping us with a simple comment. Bugzilla references
Testing this PR locallyIf you don't have a local development environment setup, you can use Digger to test this PR: dub fetch digger
dub run digger -- build "master + dmd#9151" |
| * in the below defined array to be later used in a static | ||
| * destructor. | ||
| */ | ||
| Statements dtors; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Headers.
|
I think the platform supports de/constructors for TLS variables. I'm guessing it's necessary for |
|
Please take a look at the failures. Thanks! |
The VC runtime has |
src/dmd/dsymbolsem.d
Outdated
| // expression so it can be used in the static destructor of the | ||
| // module. | ||
| auto m = dsym.getModule(); | ||
| if (dsym.isStatic || sc.parent == m) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess you will have to distinguish between TLS and shared/__gshared data here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think shared/__gshared is not as important as thread local destruction, as the process should clean up most of the stuff anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be more important in a shared library that gets unloaded from the process.
Anyway, the shared globals should not be destroyed from within every thread.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes, I always forget about that case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean that shared variables should be destroyed in a shared static destructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean that shared variables should be destroyed in a shared static destructor?
Yes, that should be the case. A non-shared static destructor will run on every thread termination. If that is destroying the shared variables, then you will have problems.
test/runnable/test14650.d
Outdated
|
|
||
| static ~this() | ||
| { | ||
| assert(a == 2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the globals should be destroyed after the explicit static ~this() have run, so the test should verify that (a == 0).
A shared static ~this() can verify the destruction instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This reveals a dilemma. a shared static ~this() is going to currently expect it's own thread local storage to still be valid. Changing this will break that code. I've seen shared static destructors do something different with the TLS data because it knows it's in the main thread.
What we may have to do is only emit the static destructors if no manual static destructors exist.
Edit: the ones inside functions might be OK, but no telling if they are accessible through some inner struct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to tell if a static destructor accesses a particular variable? That probably would be a more complex fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This reveals a dilemma. a
shared static ~this()is going to currently expect it's own thread local storage to still be valid. Changing this will break that code.
Maybe the main thread should run TLS destructors after shared static ~this(), but before shared destructors. The latter could contain the assertion above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't shared static ~this() a shared destructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be a breaking change as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to tell if a static destructor accesses a particular variable? That probably would be a more complex fix.
It is possible to know if a particular variable is accessed, but it is hard to tell if the variable is destroyed or not:
struct A
{
~this() {}
}
A a;
void fun(ref A a)
{
a.__dtor();
}
static ~this()
{
import std.stdio : writeln;
writeln(a); // accessed but not destroyed;
}
static ~this()
{
fun(a); // need to examine fun to tell if it is destroyed or not
}Not to mention the overhead of re-analyzing all the function bodies that a static destructor calls.
Honestly, at this point I think that a better approach would be to document the existing behavior and guide the user to destroy its global variables using a static destructor. I'm not really sure how you can destroy static variables from different scopes (maybe use the mangled name?).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but before shared destructors.
Isn'tshared static ~this()a shared destructor?
I meant destructors of shared variables.
You mean that shared variables should be destroyed in a shared static destructor?
yes, if it is the last to be executed for that module.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly, at this point I think that a better approach would be to document the existing behavior and guide the user to destroy its global variables using a static destructor.
There are some issues there:
- adding a static destructor to a module makes it participate in the cycle detection. It may not be possible to add such things. However, we can in the compiler make standalone static destructors which are free to execute in any order (that change needs to go into this PR).
- any destructors should be run AFTER everything is done, doing it in the compiler the way this PR is done is much safer than someone adding a static destructor, possibly in the middle of the module, that will destroy the item early.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe the main thread should run TLS destructors after
shared static ~this(), but before shared destructors. The latter could contain the assertion above.
This means adding another entry point to moduleinfo, as all destructors are combined into one function. Which I'm OK with, BTW, but it's going to require more effort than this PR has done.
All destructors should be fine to run on items that are already destroyed. Classes should be on the heap, so only structs are what we need to worry about. |
@rainers Currently, as far as I know, TLS variables are initialized by the system, that means druntime is not necessary. With this change, druntime is required for TLS destructors to be run correctly. |
On Windows, the system only makes a plain copy, anything else has to be done within the user code. I think we should avoid the C++ trouble of out-of-order con/destructors, and module dependencies are D's solution to that (note that this PR might add circular dependencies to existing code). Moving druntime into the compiler is not a great idea, when the alternative of adding a library to the command line is simple. |
That's not good.
Not sure I follow. |
I do not follow. |
Module info contains infomation about imports that contain static ctors/dtors so they can execute in an appropriate order (those with no dependencies first for ctors, reversed for dtors). Adding a static dtor in a module that has none yet will add this kind of dependency and can cause cycles. |
I meant replicating druntime functionality in the compiler (calling dtors for TLS variables and ensuring a proper order in this case). |
Hm... but a module destructor can be marked as standalone, and so it won't be included in the cycle detection (i.e. it's done last, after all other static destructors are done). I think the way this PR is, that isn't happening, but it should be. I think type destructors are not normally dependent on other module variables. It's possible they are, but unlikely. The main thing we need to handle (most important), is to destroy thread local items when the thread is no longer active. Since threads can be created and destroyed many times during a process, not doing this leads to leaks. Although there is the possibility of shared globals being leaky with dynamic library loading and unloading, it's less of a problem, and we can always go the route of saying shared data is never destroyed, but thread local is automatically destroyed, so handle the shared globals in shared destructors. |
|
Another idea: construct a fake standalone module that just has the static destructor, make it dependent on the current module, and it will naturally run its static destructors after all dependencies have run theirs. Note, we will have to disable this functionality for betterC. |
|
The reason this bug exists in D at all is because it was originally designed without RAII, and all initialization/teardown would be manually done in the |
|
That means no more stdout? |
Anyhow, |
|
No, I mean stdout is a |
|
How about using |
@WalterBright I think that this is a bit restrictive. Currently, phobos and druntime have global/static |
I like this. |
I know that scope at the module level is a noop, but changing something from a noop to a yesop is still a change. If it was an error before, then this would be a change that wouldn't affect any code. However, I think it will not break any code at all! If you destroy items after they are already destroyed, that is supposed to be allowed. So it would be probably OK to do this. However, @WalterBright should chime in on the effects of this before we merge. |
|
Hamstringing the language to disallow globals would be a major breakage that is only justifiable as a copout. The right solution is to use and integrate with the current module dtors, as follows. Right after each top-level global declaration, insert this: static shared T x;
// generated code {
static shared ~this() { x.__dtor(); }
// }Then let the usual language rules take care of circular dependencies etc. |
|
Similarly for static unshared top-level declarations, the code generation would go: static T x;
// generated code {
static ~this() { x.__dtor(); }
// } |
Not right after, it needs to go at the end of the module. Otherwise, actual static destructors could be using already-destroyed data. This is what the PR originally did I believe. There is still a problem with adding cycles to the runtime using this method. My previous suggestion of putting it into a dependent module would not work either, because the destructors would be run first. So the best answer I can think of is to add those calls at the end of the module, and if the static destructor is ONLY added because of these inserted calls, mark it standalone, so it doesn't participate in cycles. |
The bug fix creates a static destructor that calls the destructors of static and global variables. Currently, this fix has exposed a bug (see [1], cc @MartinNowak). Also there might be breakage due to the fact that what this patch does might be done manually in user defined static destructors which will lead to double free errors. A solution might be to abandon the generation of static this when one already exists.
[1] https://issues.dlang.org/show_bug.cgi?id=16980#c8