After update 2.081 I'm experimenting with binding external CPP classes with extern(C++). I use simple class to test:
---testlib.cpp
#include <iostream>
class Bar {
public:
Bar() {}
~Bar() {}
};
class __declspec(dllexport) Foo {
int *a;
Bar *b;
public:
Foo(int value) {
std::cout << "Creating in CPP\r\n";
a = new int(value);
b = new Bar();
std::cout << "Done\r\n";
}
~Foo() {
std::cout << "Deleting in CPP\r\n";
delete a;
delete b;
std::cout << "Done\r\n";
}
};
---
This code is compiled with "cl testlib.cpp", "lib /out:testlib.lib testlib.obj" and resulting lib linked to D app.
D side:
---app.d
import std.stdio;
extern(C++) {
class Foo {
this(int value);
final ~this();
}
}
void main() {
for (int i; i < 100; i++) {
writeln("Creating in D");
Foo bar = new Foo(0);
writeln("Deleting in D");
bar.destroy();
}
writeln("Finished");
readln();
}
---
This example works perfect. But when I change Foo::~Foo to virtual (and remove "final" on D side) app silently crashes on exactly 3rd iteration after "Creating in D" step. So, for some reason, I've changed destructor but it crashes on/before constructor. And, for some reason, I have no error messages or something - application just hangs a bit and stops.
This seems to work OK if Foo contains only int*, no other objects.
Same results on both DMD 2.081.1 and LDC 1.11.0-beta2.
Comment #1 by kinke — 2018-07-26T19:44:05Z
Confirmed. It's the 3rd iteration only because GC collection is triggered then; adding `import core.memory; GC.collect();` after destroy makes it segfault in the 1st iteration, as does using `delete(bar);` instead of destroy.
I guess there's something wrong with the dtor field of the ClassInfo.
Comment #2 by kinke — 2018-07-26T20:28:28Z
Nope, druntime apparently doesn't support destroying C++ classes yet. E.g., _d_delclass() and rt_finalize2() assume D objects, where the ClassInfo is stored in the first vtable slot. rt_finalize2() also assumes an implicit monitor field etc.
Comment #3 by turkeyman — 2018-07-27T05:27:25Z
I never tested the new extern(C++) work with the GC.
destroy() knows how to destroy a C++ class because it knows the argument type, but the GC collect doesn't know the type, and can't know the memory is a C++ class and how to destroy it properly.
I'm not sure what options are possible in this case.
I reckon, if you're using C++ classes, use C++ allocation strategies.
Comment #4 by turkeyman — 2018-07-27T05:51:44Z
It might be possible to make gcnew detect it's allocating a C++ class, and allocate it wrapped in a thin D class for the classinfo, and forward the D destructor to the C++ destructor...?
Comment #5 by vladimmi — 2018-07-27T08:20:38Z
Sorry if it's not useful, just want to point out couple of things from my perspective as language newcomer that may hint something for you:
1) Example in ticket works when Foo::~Foo is not virtual. So does in that case D know how to destroy CPP classes and is it "virtual" causing some troubles?
2) Example in ticket also works in other case - Foo::~Foo is virtual but Foo doesnt contain Bar. Why implementation details (Bar isnt even exported and mapped in D) do some changes to behavior?
Comment #6 by dfj1esp02 — 2018-07-27T11:34:08Z
Shouldn't you declare class fields on the D side? How one can determine how much memory to allocate for the instance?
Comment #7 by vladimmi — 2018-07-27T13:07:17Z
Sounds logical. I've tried to additionally export Bar, map it in D and also to add Foo::a and Foo::b to mapping to make it full. Problem has gone and it seems to work correct without crashes or memory leaks.
But how does it happen to work under some conditions before (like with non-virtual dtor)? Does "virtual" change used methods to create/delete objects somehow or was that sort of random luck?..
Comment #8 by turkeyman — 2018-07-27T17:42:22Z
(In reply to Vladimir Marchevsky from comment #7)
> Sounds logical. I've tried to additionally export Bar, map it in D and also
> to add Foo::a and Foo::b to mapping to make it full. Problem has gone and it
> seems to work correct without crashes or memory leaks.
Yeah, sorry, I didn't notice that the classes were not matching!
If you're going to allocate instances of a class, they need to be the same size.
> But how does it happen to work under some conditions before (like with
> non-virtual dtor)? Does "virtual" change used methods to create/delete
> objects somehow or was that sort of random luck?..
You were lucky. The CPP ctor/dtor were assigning and destroying a,b which were outside of the memory allocation (since you didn't define them in the class that D allocated.
It's likely that there's a minimum allocation granularity (like 16 bytes or something) and those members just happened to fit inside... but when you added the vtable, the class got bigger, maybe b was outside of the minimum allocation block... or lots of possibilities. You were writing to unallocated memory; anything could happen!
Comment #9 by kinke — 2018-07-27T18:56:40Z
Ouch, I totally overlooked the missing fields on the D side too.
The GC now releases the memory correctly - without trying to destruct the extern(C++) objects first (independent from virtual-ness of dtor), which might be an issue in its own right.
Comment #10 by turkeyman — 2018-07-27T19:07:34Z
Right, and that goes back to my original point; the GC doesn't know the memory is a C++ class, and doesn't know how to destruct.
So either, use the GC under the assumption the destructor will never be called... or maybe there's some opportunity to make gcnew wrap C++ class allocations in a thin D class that forwards the destructor?
Comment #11 by kinke — 2018-07-27T19:18:40Z
(In reply to Manu from comment #10)
> or maybe there's some opportunity to make gcnew wrap C++ class
> allocations in a thin D class that forwards the destructor?
I guess something like having specially-marked GC blocks for C++ objects and gcnew prepending the C++ dtor (or D ClassInfo) address right before the actual class instance would work => 1 pointer overhead per GC-allocated C++ object to make it destructible during garbage collection.
Comment #12 by kinke — 2018-07-27T19:37:31Z
Something very similar is apparently already done for GC-allocated structs - if they have a destructor, the TypeInfo pointer is stored right after the actual instance, and a specially marked GC block is requested; see rt.lifetime._d_newitemU().
Comment #13 by turkeyman — 2018-07-27T21:24:04Z
(In reply to kinke from comment #12)
> Something very similar is apparently already done for GC-allocated structs -
> if they have a destructor, the TypeInfo pointer is stored right after the
> actual instance, and a specially marked GC block is requested; see
> rt.lifetime._d_newitemU().
Right. I think gcnew should implement a trick like that for C++ objects.
I don't have time to look at it now though.
Super-busy, which is why I was rushing to get all my C++ stuff in a month back.
Comment #14 by robert.schadek — 2024-12-13T18:59:55Z