www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Destructor attribute inheritance, yea or nay?

reply Stanislav Blinov <stanislav.blinov gmail.com> writes:
I'd like to hear what you guys think about this issue:

https://issues.dlang.org/show_bug.cgi?id=15246

Marco argues that because "it currently doesn't work that way" 
(i.e. destructors are not inherited), the bug is invalid.

However, what this means in practice is:

- destroy()/rt_finalize() can never be anything but  system
- destructors of derived classes, and even destructors of 
aggregates (structs) can violate attributes, and the compiler 
does nothing to prevent that

Considering that the core runtime component - the GC - is the one 
that usually handles finalization, it follows that *GC collection 
can never be  safe*. And since collection only happens during 
allocation, it follows that allocation cannot be  safe either. 
Nor can they be  trusted, because destructors are effectively not 
restricted in any way. IOW, the "doesn't work that way" claim 
effectively hammers shut the coffin of memory safety as far as 
dynamic allocation is concerned, and that means the whole runtime 
and anything that depends on it.

I am of the opinion that the destructors should not be capable of 
violating the aggregated destruction attributes. This would allow 
the destroy() function to safely infer the correct attribute set 
for finalization, and propagate it to the calling code.

I.e. we could implement destroy() for classes as follows:

void destroy(T)(T obj) if (is(T == class))
{
    (cast(_finalizeType!T)&rt_finalize)(cast(void*)obj);
}

void destroy(T)(T obj) if (is(T == interface))
{
    destroy(cast(Object)obj);
}
extern(C) void rt_finalize(void* p, bool det = true);
extern(C)
template _finalizeType(T)
{
    static if (is(T == Object))
    {
        alias _finalizeType = typeof(&rt_finalize);
    }
    else
    {
         alias _finalizeType = typeof((void* p, bool det = true) 
 {
            // generate a body that calls all the destructors in 
 the chain,
            // compiler should infer the intersection of 
 attributes
            // _Seq is an equivalent of std.meta.AliasSeq
            // _Bases is an equivalent of 
 std.traits.BaseClassesTuple
            foreach (B; _Seq!(T, _Bases!T)) {
                // __dtor, i.e. B.~this
                static if (__traits(hasMember, B, "__dtor"))
                    () { B obj; obj.__dtor; } ();
                // __xdtor, i.e. dtors for all RAII members
                static if (__traits(hasMember, B, "__xdtor"))
                    () { B obj; obj.__xdtor; } ();
            }
        });
    }
}
This would keep the inferred attributes for code that actually calls destroy(). However, currently we cannot do that, because the language does not enforce attribute propagation in destructors, and at runtime, destroy() could be called via base class reference, while derived class violates the attributes: class Base { ~this() safe nogc {} } class Derived : Base { ~this() {} } Base b = new Derived; destroy(b); // infer safe nogc, while in reality this call is neither safe nor nogc, it is system Any thoughts?
May 22
next sibling parent Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Monday, 22 May 2017 at 17:05:06 UTC, Stanislav Blinov wrote:

 Considering that the core runtime component - the GC - is the 
 one that usually handles finalization, it follows that *GC 
 collection can never be  safe*. And since collection only 
 happens during allocation, it follows that allocation cannot be 
  safe either. Nor can they be  trusted, because destructors are 
 effectively not restricted in any way.
This program, executed on my machine:
import std.stdio;

class Innocious
{
    ~this()  safe {}
}

class Malicious : Innocious
{
    int[] data;

    this()  safe
    {
        data = new int[1000000];
    }

    ~this()
    {
        writeln("    Sure, here you go:");
        writeln("      import std.random;");
        writeln("      auto n = uniform(1, uint.max);");
        writeln("      *(cast(int*)n) = 0xbadf00d;");
    }
}

void important()  safe
{
    writeln("I am working here, i'm not doing anything 
 dangerous...");
    scope(exit) writeln("I'm good, no, I'm awesome. You can 
 trust me!");
    writeln("  Good GC, would you kindly give me some room to 
 maneuver?");
    int[] storage = new int[1000000];
    /* do some calculations... */
}

void oblivious()  safe
{
    Innocious i = new Malicious();
    /* do something with i and then leave it for GC. */
}

void main()
{
    oblivious();
    important();
}
prints this:
 I am working here, i'm not doing anything dangerous...
   Good GC, would you kindly give me some room to maneuver?
     Sure, here you go:
       import std.random;
       auto n = uniform(1, uint.max);
       *(cast(int*)n) = 0xbadf00d;
 I'm good, no, I'm awesome. You can trust me!
May 26
prev sibling next sibling parent reply Igor Shirkalin <mathsoft inbox.ru> writes:
On Monday, 22 May 2017 at 17:05:06 UTC, Stanislav Blinov wrote:
 I'd like to hear what you guys think about this issue:

 https://issues.dlang.org/show_bug.cgi?id=15246

 [...]
If your destructor is not safe and nogc, why not to make it be the same or call inherited destructor implicity?
May 26
parent reply Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Friday, 26 May 2017 at 17:08:40 UTC, Igor Shirkalin wrote:
 On Monday, 22 May 2017 at 17:05:06 UTC, Stanislav Blinov wrote:
 I'd like to hear what you guys think about this issue:

 https://issues.dlang.org/show_bug.cgi?id=15246

 [...]
If your destructor is not safe and nogc, why not to make it be the same or call inherited destructor implicity?
Destructors of derived classes are called implicitly on finalization. The net effect is that such finalization adopts the weakest set of attributes among all the destructors it calls. There are two sides of this problem: one is that we cannot have deterministic destruction (i.e. manually allocate/free classes) while keeping attribute inference: under current rules, finalization has to be system. This one can be tackled if the language provided strict rules of attribute inheritance in destructors. Another side, clearly demonstrated by my second post, is that non-deterministic destruction cannot be safe, period. Because when GC collects and calls destructors, it calls all of them, regardless of their safe status, even when the collection is triggered inside a safe function.
May 26
parent reply Igor Shirkalin <mathsoft inbox.ru> writes:
On Friday, 26 May 2017 at 17:17:39 UTC, Stanislav Blinov wrote:
 Destructors of derived classes are called implicitly on 
 finalization. The net effect is that such finalization adopts 
 the weakest set of attributes among all the destructors it 
 calls.
I'm sorry, I ment explicitly. I hope it is not possible.
 There are two sides of this problem: one is that we cannot have 
 deterministic destruction (i.e. manually allocate/free classes) 
 while keeping attribute inference: under current rules, 
 finalization has to be  system. This one can be tackled if the 
 language provided strict rules of attribute inheritance in 
 destructors.

 Another side, clearly demonstrated by my second post, is that 
 non-deterministic destruction cannot be  safe, period. Because 
 when GC collects and calls destructors, it calls all of them, 
 regardless of their  safe status, even when the collection is 
 triggered inside a  safe function.
Doesn't that mean if compiler can't call inherited destructor despite of GC it must be error?
May 26
parent reply Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Friday, 26 May 2017 at 17:32:38 UTC, Igor Shirkalin wrote:
 On Friday, 26 May 2017 at 17:17:39 UTC, Stanislav Blinov wrote:
 Destructors of derived classes are called implicitly on 
 finalization. The net effect is that such finalization adopts 
 the weakest set of attributes among all the destructors it 
 calls.
I'm sorry, I ment explicitly. I hope it is not possible.
It is very possible, and it should be possible, otherwise we couldn't even think about deterministic destruction.
 Another side, clearly demonstrated by my second post, is that 
 non-deterministic destruction cannot be  safe, period. Because 
 when GC collects and calls destructors, it calls all of them, 
 regardless of their  safe status, even when the collection is 
 triggered inside a  safe function.
Doesn't that mean if compiler can't call inherited destructor despite of GC it must be error?
1) Destructors are not "inherited" in D. Each derived class has it's own independent destructor. That's why they don't inherit any attributes either. 2) Compiler doesn't call destructors for classes. It is done either manually (by calling destroy()) or by the GC. Look at the example in the second post: I'm in safe function (important()), I need some memory. I ask for it, the GC decides to do a collection before giving me memory. And during that collection it calls a system destructor. So the language and runtime are effectively in disagreement: language says "no system calls in safe context", runtime says "whatever, I need to call those destructors".
May 26
parent reply Igor Shirkalin <mathsoft inbox.ru> writes:
On Friday, 26 May 2017 at 17:48:24 UTC, Stanislav Blinov wrote:
 I'm sorry, I ment explicitly. I hope it is not possible.
It is very possible, and it should be possible, otherwise we couldn't even think about deterministic destruction.
Hm, you've said it is decision of GC (see bellow), so how can it be deterministic?
 Another side, clearly demonstrated by my second post, is that 
 non-deterministic destruction cannot be  safe, period. 
 Because when GC collects and calls destructors, it calls all 
 of them, regardless of their  safe status, even when the 
 collection is triggered inside a  safe function.
Doesn't that mean if compiler can't call inherited destructor despite of GC it must be error?
1) Destructors are not "inherited" in D. Each derived class has it's own independent destructor. That's why they don't inherit any attributes either. 2) Compiler doesn't call destructors for classes. It is done either manually (by calling destroy()) or by the GC. Look at the example in the second post: I'm in safe function (important()), I need some memory. I ask for it, the GC decides to do a collection before giving me memory. And during that collection it calls a system destructor. So the language and runtime are effectively in disagreement: language says "no system calls in safe context", runtime says "whatever, I need to call those destructors".
Your example is very interesting and it derives some questions. First, why 'oblivious' function does not free Malicious object (no matter GC or not GC). What if 'important' function needs some "external an not safe" resource used by 'oblivious'? Is it all about safe that stops allowing it? If so, safe is really important feature in Dlang. Second, same as first, it looks like I got it.
May 26
parent Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Friday, 26 May 2017 at 18:58:46 UTC, Igor Shirkalin wrote:

 First, why 'oblivious' function does not free Malicious object 
 (no matter GC or not GC).
It actually does matter. It doesn't manually release the resources precisely because it relies on the GC. I've made it overly explicit, but in real world it could have just as easily been an implicit allocation done by some library function (say, Phobos), perhaps even without giving me an actual reference to allocated memory. The name of this function reflects this: it doesn't know or care what's going on inside it.
 What if 'important' function needs some "external an not safe" 
 resource used by 'oblivious'?
That's the point. 'important' has nothing to do with 'oblivious' at all, yet it *may* suffer from side effects that originate in 'oblivious' at an unspecified point in time during program execution. What's worse, at a glance it would look like safe function breaking it's own promise
 Is it all about  safe that stops allowing it? If so,  safe is 
 really important feature in Dlang. Second, same as first, it 
 looks like I got it.
Per language rules, you're not allowed to call system functions in safe code: void important() safe { auto obj = new Malicious(); obj.destroy(); // this will be a compiler error, destroy() is system } However, the runtime currently ignores this altogether, and happily calls that same system function while executing that same safe function, or rather, may or may not call depending on conditions beyond our control. If that doesn't sound bad, I don't know what does.
May 26
prev sibling parent reply Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Monday, 22 May 2017 at 17:05:06 UTC, Stanislav Blinov wrote:
 I'd like to hear what you guys think about this issue:

 https://issues.dlang.org/show_bug.cgi?id=15246

 Any thoughts?
By the absence of replies from those who (I think) should care I conclude that either: 1. I'm saying something stupidly silly, and people are too polite to point it out; 2. Everybody knows this already; 3. Nobody actually cares. I'm hoping it's 1 or 2...
May 27
parent reply Adam D. Ruppe <destructionator gmail.com> writes:
On Saturday, 27 May 2017 at 10:11:38 UTC, Stanislav Blinov wrote:
 3. Nobody actually cares.
That's me. I think the attribute mess is completely broken and mostly just ignore it. That said, I do agree with you: it SHOULD work like you describe if we want the attributes to be meaningful.
May 27
parent Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Saturday, 27 May 2017 at 13:32:57 UTC, Adam D. Ruppe wrote:
 On Saturday, 27 May 2017 at 10:11:38 UTC, Stanislav Blinov 
 wrote:
 3. Nobody actually cares.
That's me. I think the attribute mess is completely broken and mostly just ignore it.
Hm. That's a strategy, perhaps I should try it :)
 That said, I do agree with you: it SHOULD work like you 
 describe if we want the  attributes to be meaningful.
Thanks, option 1 just became less of a one.
May 27