Skip to content
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 15589 - extern(C++) virtual destructors are not put in vtbl[] #8277

Merged
merged 5 commits into from
Jun 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/dmd/aggregate.h
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ class ClassDeclaration : public AggregateDeclaration
TypeInfoClassDeclaration *vclassinfo; // the ClassInfo object for this ClassDeclaration
bool com; // true if this is a COM class (meaning it derives from IUnknown)
bool stack; // true if this is a scope class
int cppDtorVtblIndex; // slot reserved for the virtual destructor [extern(C++)]
bool inuse; // to prevent recursive attempts
bool isActuallyAnonymous; // true if this class has an identifier, but was originally declared anonymous
// used in support of https://issues.dlang.org/show_bug.cgi?id=17371
Expand Down
29 changes: 29 additions & 0 deletions src/dmd/clone.d
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module dmd.clone;
import core.stdc.stdio;
import dmd.aggregate;
import dmd.arraytypes;
import dmd.dclass;
import dmd.declaration;
import dmd.dscope;
import dmd.dstruct;
Expand Down Expand Up @@ -883,6 +884,34 @@ extern (C++) FuncDeclaration buildDtor(AggregateDeclaration ad, Scope* sc)
e = Expression.combine(ex, e); // combine in reverse order
}

/* extern(C++) destructors call into super to destruct the full hierarchy
*/
ClassDeclaration cldec = ad.isClassDeclaration();
if (cldec && cldec.classKind == ClassKind.cpp && cldec.baseClass && cldec.baseClass.dtor)
{
// WAIT BUT: do I need to run `cldec.baseClass.dtor` semantic? would it have been run before?
cldec.baseClass.dtor.functionSemantic();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This line.... do I need it?
When I step in there, it looks like it's always done it before.


stc = mergeFuncAttrs(stc, cldec.baseClass.dtor);
if (!(stc & STC.disable))
{
// super.__xdtor()

Expression ex = new SuperExp(loc);

// This is a hack so we can call destructors on const/immutable objects.
// Do it as a type 'paint'.
ex = new CastExp(loc, ex, cldec.baseClass.type.mutableOf());
if (stc & STC.safe)
stc = (stc & ~STC.safe) | STC.trusted;

ex = new DotVarExp(loc, ex, cldec.baseClass.dtor, false);
ex = new CallExp(loc, ex);
Copy link
Member

Choose a reason for hiding this comment

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

Would be nice if you could forward the dtor argument here, so it doesn't get lost when delete is called from C++ and the base class is also in C++ and handles it as expected.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If C++ ever calls a destructor with non-0, it means an operator delete override exists beneath it in the hierarchy, and therefore is not supported by D.
There is no case where it's valid that any argument other than 0 is used, and D is also involved. I dont think 'we can support this, so I'm not sure fixing this is worth the complexity no?

Copy link
Contributor Author

@TurkeyMan TurkeyMan Jun 6, 2018

Choose a reason for hiding this comment

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

Also, I think it's only the top-level destructor that calls delete, you don't pass the request to delete down the hierarchy, otherwise it would call delete at each level of destruction... only the top-level destruction should finally delete?

Copy link
Member

Choose a reason for hiding this comment

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

Also, I think it's only the top-level destructor that calls delete

True. I just tried how a dtor of a derived class looks like, and it's the dtor with argument that goes into the vtable, but it calls a dtor without arguments that does the chaining into the base class. Only the one in the vtable optionally frees the memory.

Copy link
Member

Choose a reason for hiding this comment

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

So forwarding the parameter would actually be wrong, passing 0 is ok.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right. If we add the enhancement to support calling C++ delete, then we should be semantically compatible I think.


e = Expression.combine(e, ex); // super dtor last
}
}

/* Build our own "destructor" which executes e
*/
if (e || (stc & STC.disable))
Expand Down
6 changes: 3 additions & 3 deletions src/dmd/cppmanglewin.d
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ private:
// <flags> ::= Y <calling convention flag>
buf.writeByte('Y');
}
const(char)* args = mangleFunctionType(cast(TypeFunction)d.type, d.needThis(), d.isCtorDeclaration() || d.isDtorDeclaration());
const(char)* args = mangleFunctionType(cast(TypeFunction)d.type, d.needThis(), d.isCtorDeclaration() || d.isDtorDeclaration(), d.isDtorDeclaration() !is null);
buf.writestring(args);
}

Expand Down Expand Up @@ -1144,7 +1144,7 @@ private:
cur.accept(this);
}

const(char)* mangleFunctionType(TypeFunction type, bool needthis = false, bool noreturn = false)
const(char)* mangleFunctionType(TypeFunction type, bool needthis = false, bool noreturn = false, bool noargs = false)
{
scope VisualCPPMangler tmp = new VisualCPPMangler(this);
// Calling convention
Expand Down Expand Up @@ -1204,7 +1204,7 @@ private:
rettype.accept(tmp);
tmp.flags &= ~MANGLE_RETURN_TYPE;
}
if (!type.parameters || !type.parameters.dim)
if (!type.parameters || !type.parameters.dim || noargs)
{
if (type.varargs == 1)
tmp.buf.writeByte('Z');
Expand Down
3 changes: 3 additions & 0 deletions src/dmd/dclass.d
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ extern (C++) class ClassDeclaration : AggregateDeclaration
/// true if this is a scope class
bool stack;

/// if this is a C++ class, this is the slot reserved for the virtual destructor
int cppDtorVtblIndex = -1;

/// to prevent recursive attempts
private bool inuse;

Expand Down
47 changes: 46 additions & 1 deletion src/dmd/dsymbolsem.d
Original file line number Diff line number Diff line change
Expand Up @@ -3735,7 +3735,17 @@ private extern(C++) final class DsymbolSemanticVisitor : Visitor
if (dd.ident == Id.dtor && dd.semanticRun < PASS.semantic)
ad.dtors.push(dd);
if (!dd.type)
dd.type = new TypeFunction(null, Type.tvoid, false, LINK.d, dd.storage_class);
{
Parameters* params = null;
if (ad.classKind == ClassKind.cpp && !Target.cppDeletingDestructor)
{
// Windows doesn't use a deleting destructor, it takes an argument instead!
Parameter param = new Parameter(STC.undefined_, Type.tuns32, new Identifier("del"), new IntegerExp(Loc.initial, 0, Type.tuns32));
Copy link
Member

Choose a reason for hiding this comment

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

I don't think it can work so easily (AFAIU you are adding a parameter with a default value so it can still be called without arguments). This helps for explicit calls, but not for the dtor written to the TypeInfo. The latter needs a shim that adds the default value as an argument. Here's an example that calls the dtor this way:

import core.stdc.stdio;

extern(C++) 
struct S
{
	int x;
	~this() { printf("~S %x\n", x); }
}

void main()
{
	S[] arr = new S[3];
	arr[1].x = 1;
	arr[2].x = 2;
	arr.length = 1;
	assumeSafeAppend(arr); // destroys arr[1] and arr[2]
	printf("done\n");
}

Output is:

~S 2
~S 1
done
~S 0

Works for Win64, but not for Win32 in dmd 2.080. (Falsely assumes that C++ dtor does not have an argument, though).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh no, you're killing me bro! >_<

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here I was thinking this was a super clever and elegant solution :(

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Perhaps I could address this in a follow-up? Situation in this PR is immeasurably better than what we had before, and I have people here that want to be using this code already.
Obviously, I'm happy to continue to work on this issue until it's correct.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe druntime can handle that case, too. Any user of the dtor member of TypeInfo_Struct/Class needs to know about it then, though.

Copy link
Member

Choose a reason for hiding this comment

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

Perhaps I could address this in a follow-up?

Sounds reasonable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why is it that druntime isn't calling the destructor with the default arg supplied like everywhere else?
Anyway yeah, this patch is already kinda huge. I'd prefer to tackle that detail in a follow up where the noise is free from the context.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you put this snippet in an issue?
I'm not sure if I'll work on this case, or the constructor issue first...

Copy link
Member

Choose a reason for hiding this comment

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

Why is it that druntime isn't calling the destructor with the default arg supplied like everywhere else?

Because it uses a function pointer assuming it does not need an extra argument. See
https://github.com/dlang/druntime/blob/master/src/rt/lifetime.d#L1347
https://github.com/dlang/druntime/blob/master/src/object.d#L2010

Can you put this snippet in an issue?

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

I just realized that calling the C++ dtor is pretty much broken for Win32 anyway because it doesn't use the __thiscall ABI.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We'll need that hack you suggest to generate a shim when assigning to a delegate I guess.

params = new Parameters;
(*params).push(param);
}
dd.type = new TypeFunction(params, Type.tvoid, false, LINK.d, dd.storage_class);
}

sc = sc.push();
sc.stc &= ~STC.static_; // not a static destructor
Expand Down Expand Up @@ -4767,6 +4777,27 @@ private extern(C++) final class DsymbolSemanticVisitor : Visitor
{
auto s = (*cldec.members)[i];
s.dsymbolSemantic(sc2);

if (cldec.classKind == ClassKind.cpp)
{
const DtorDeclaration dtor = s.isDtorDeclaration();
if (dtor && cldec.cppDtorVtblIndex == -1)
{
if (cldec.baseClass && cldec.baseClass.cppDtorVtblIndex != -1)
{
// override the base virtual
cldec.cppDtorVtblIndex = cldec.baseClass.cppDtorVtblIndex;
}
else if (!dtor.isFinal())
{
// reserve the dtor slot for the destructor (which we'll create later)
cldec.cppDtorVtblIndex = cast(int)cldec.vtbl.dim;
cldec.vtbl.push(s);
if (Target.cppDeletingDestructor)
cldec.vtbl.push(s); // deleting destructor uses a second slot
}
}
}
}

if (!cldec.determineFields())
Expand Down Expand Up @@ -4854,6 +4885,20 @@ private extern(C++) final class DsymbolSemanticVisitor : Visitor

cldec.dtor = buildDtor(cldec, sc2);

if (cldec.classKind == ClassKind.cpp && cldec.cppDtorVtblIndex != -1)
{
// now we've built the aggregate destructor, we'll make it virtual and assign it to the reserved vtable slot
cldec.dtor.vtblIndex = cldec.cppDtorVtblIndex;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is cppDtorVtblIndex needed, why can't cldec.dtor.vtblIndex be used directly?

Copy link
Contributor Author

@TurkeyMan TurkeyMan May 22, 2018

Choose a reason for hiding this comment

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

You can see on this line that cldec.dtor.vtblIndex is being assigned :)
That function was only created on the line above, but its virtual slot was reserved way back when processing members...

Copy link
Contributor

Choose a reason for hiding this comment

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

Right, I didn't look up far enough 😃.

cldec.vtbl[cldec.cppDtorVtblIndex] = cldec.dtor;

if (Target.cppDeletingDestructor)
{
// TODO: create a C++ compatible deleting destructor (call out to `operator delete`)
// for the mooment, we'll call the non-deleting destructor and leak
cldec.vtbl[cldec.cppDtorVtblIndex + 1] = cldec.dtor;
}
}

if (auto f = hasIdentityOpAssign(cldec, sc2))
{
if (!(f.storage_class & STC.disable))
Expand Down
5 changes: 3 additions & 2 deletions src/dmd/func.d
Original file line number Diff line number Diff line change
Expand Up @@ -3372,8 +3372,9 @@ extern (C++) final class DtorDeclaration : FuncDeclaration

override bool isVirtual() const
{
// false so that dtor's don't get put into the vtbl[]
return false;
// D dtor's don't get put into the vtbl[]
// this is a hack so that extern(C++) destructors report as virtual, which are manually added to the vtable
return vtblIndex != -1;
}

override bool addPreInvariant()
Expand Down
5 changes: 5 additions & 0 deletions src/dmd/target.d
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ struct Target
bool cppExceptions; /// set if catching C++ exceptions is supported
char int64Mangle; /// mangling character for C++ int64_t
char uint64Mangle; /// mangling character for C++ uint64_t
bool cppDeletingDestructor; /// target C++ ABI uses deleting destructor
}

/**
Expand Down Expand Up @@ -115,6 +116,9 @@ struct Target
ptrsize = 4;
classinfosize = 0x4C; // 76

// all systems but windows use deleting destructors
cppDeletingDestructor = true;

/* gcc uses int.max for 32 bit compilations, and long.max for 64 bit ones.
* Set to int.max for both, because the rest of the compiler cannot handle
* 2^64-1 without some pervasive rework. The trouble is that much of the
Expand Down Expand Up @@ -148,6 +152,7 @@ struct Target
realpad = 0;
realalignsize = 2;
reverseCppOverloads = true;
cppDeletingDestructor = false;
c_longsize = 4;
if (ptrsize == 4)
{
Expand Down
1 change: 1 addition & 0 deletions src/dmd/target.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ struct Target
static bool cppExceptions; // set if catching C++ exceptions is supported
static char int64Mangle; // mangling character for C++ int64_t
static char uint64Mangle; // mangling character for C++ uint64_t
static bool cppDeletingDestructor; // target C++ ABI uses deleting destructor

template <typename T>
struct FPTypeProperties
Expand Down
44 changes: 44 additions & 0 deletions test/runnable/cppa.d
Original file line number Diff line number Diff line change
Expand Up @@ -1264,6 +1264,49 @@ void test16536()
version(OSX) assert(pass16536(123) == 123);
}

/****************************************/
// 15589 - extern(C++) virtual destructors are not put in vtbl[]

extern(C++)
{
class A15589
{
extern(D) static int[] dtorSeq;
struct S
{
this(int x) { this.x = x; }
~this() { dtorSeq ~= x; }
int x;
}
int foo() { return 100; } // shift dtor to slot 1
~this() { dtorSeq ~= 10; }
S s1 = S(1);
S s2 = S(2);
}
class B15589 : A15589
{
int bar() { return 200;} // add an additional function AFTER the dtor at slot 2
~this() { dtorSeq ~= 20; }
S s3 = S(3);
}

void test15589b(A15589 p);
}

void test15589()
{
A15589 c = new B15589;
assert(A15589.dtorSeq == null);
assert(c.foo() == 100);
assert((cast(B15589)c).bar() == 200);
c.__xdtor(); // virtual dtor call
assert(A15589.dtorSeq[] == [ 20, 3, 10, 2, 1 ]); // destroyed full hierarchy!

A15589.dtorSeq = null;
test15589b(c);
assert(A15589.dtorSeq[] == [ 20, 3, 10, 2, 1 ]); // destroyed full hierarchy!
}

/****************************************/

void main()
Expand Down Expand Up @@ -1308,6 +1351,7 @@ void main()
test15372();
test15802();
test16536();
test15589();

printf("Success\n");
}
37 changes: 34 additions & 3 deletions test/runnable/extra-files/cppb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ void test14200b(float a, int b, double c) {};

namespace std {
namespace N14956 {
struct S14956 { };
struct S14956 { };
}
}

Expand Down Expand Up @@ -762,7 +762,7 @@ int foo15372(int value)

void test15372b()
{
int t = foo15372<int>(1);
int t = foo15372<int>(1);
}

/****************************************/
Expand Down Expand Up @@ -793,7 +793,7 @@ class Foo15802

void test15802b()
{
int t = Foo15802<int>::boo(1);
int t = Foo15802<int>::boo(1);
}


Expand All @@ -806,3 +806,34 @@ uint64_t pass16536(uint64_t a)
return a;
}
#endif

/****************************************/
// 15589 - extern(C++) virtual destructors are not put in vtbl[]

class A15589
{
public:
struct S
{
public:
int x;
};
virtual int foo();
virtual ~A15589();
S s1;
S s2;
};
class B15589 : public A15589
{
public:
virtual int bar();
virtual ~B15589();
S s3;
};

void test15589b(A15589 *p)
{
assert(p->foo() == 100);
assert(((B15589*)p)->bar() == 200);
p->~A15589();
}