Skip to content

Commit 24b02e4

Browse files
authored
Fix incorrect dtor call for non-owned instances (#1615)
* Introduce failing test for disposing bug * Dispose(bool, bool) really shouldn't be public. Clients should call Dipose() instead. Derived classes may need to call Dispose(bool, bool) so lets make it internal protected.
1 parent 36c9e5f commit 24b02e4

File tree

5 files changed

+60
-11
lines changed

5 files changed

+60
-11
lines changed

src/Generator/Generators/CSharp/CSharpSources.cs

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1879,7 +1879,7 @@ private void GenerateVTableManagedCall(Method method)
18791879
{
18801880
if (method.IsDestructor)
18811881
{
1882-
WriteLine("{0}.Dispose(true);", Helpers.TargetIdentifier);
1882+
WriteLine("{0}.Dispose(disposing: true, callNativeDtor: true);", Helpers.TargetIdentifier);
18831883
return;
18841884
}
18851885

@@ -2200,7 +2200,7 @@ private void GenerateClassFinalizer(INamedDecl @class)
22002200

22012201
WriteLine("~{0}()", @class.Name);
22022202
WriteOpenBraceAndIndent();
2203-
WriteLine("Dispose(false);");
2203+
WriteLine($"Dispose(false, callNativeDtor : { Helpers.OwnsNativeInstanceIdentifier} );");
22042204
UnindentAndWriteCloseBrace();
22052205

22062206
PopBlock(NewLineKind.BeforeNextBlock);
@@ -2217,21 +2217,27 @@ private void GenerateDisposeMethods(Class @class)
22172217
WriteLine("public void Dispose()");
22182218
WriteOpenBraceAndIndent();
22192219

2220-
WriteLine("Dispose(disposing: true);");
2220+
WriteLine($"Dispose(disposing: true, callNativeDtor : { Helpers.OwnsNativeInstanceIdentifier} );");
22212221
if (Options.GenerateFinalizers)
22222222
WriteLine("GC.SuppressFinalize(this);");
22232223

22242224
UnindentAndWriteCloseBrace();
22252225
PopBlock(NewLineKind.BeforeNextBlock);
22262226
}
22272227

2228-
// Generate Dispose(bool) method
2228+
// Declare partial method that the partial class can implement to participate
2229+
// in dispose.
22292230
PushBlock(BlockKind.Method);
2230-
Write("public ");
2231+
WriteLine("partial void DisposePartial(bool disposing);");
2232+
PopBlock(NewLineKind.BeforeNextBlock);
2233+
2234+
// Generate Dispose(bool, bool) method
2235+
PushBlock(BlockKind.Method);
2236+
Write("internal protected ");
22312237
if (!@class.IsValueType)
22322238
Write(hasBaseClass ? "override " : "virtual ");
22332239

2234-
WriteLine("void Dispose(bool disposing)");
2240+
WriteLine("void Dispose(bool disposing, bool callNativeDtor )");
22352241
WriteOpenBraceAndIndent();
22362242

22372243
if (@class.IsRefType)
@@ -2251,6 +2257,13 @@ private void GenerateDisposeMethods(Class @class)
22512257
}
22522258
}
22532259

2260+
// TODO: if disposing == true we should delegate to the dispose methods of references
2261+
// we hold to other generated type instances since those instances could also hold
2262+
// references to unmanaged memory.
2263+
//
2264+
// Delegate to partial method if implemented
2265+
WriteLine("DisposePartial(disposing);");
2266+
22542267
var dtor = @class.Destructors.FirstOrDefault();
22552268
if (dtor != null && dtor.Access != AccessSpecifier.Private &&
22562269
@class.HasNonTrivialDestructor && !@class.IsAbstract)
@@ -2259,7 +2272,29 @@ private void GenerateDisposeMethods(Class @class)
22592272
if (!Options.CheckSymbols ||
22602273
Context.Symbols.FindLibraryBySymbol(dtor.Mangled, out library))
22612274
{
2262-
WriteLine("if (disposing)");
2275+
// Normally, calling the native dtor should be controlled by whether or not we
2276+
// we own the underlying instance. (i.e. Helpers.OwnsNativeInstanceIdentifier).
2277+
// However, there are 2 situations when the caller needs to have direct control
2278+
//
2279+
// 1. When we have a virtual dtor on the native side we detour the vtable entry
2280+
// even when we don't own the underlying native instance. I think we do this
2281+
// so that the managed side can null out the __Instance pointer and remove the
2282+
// instance from the NativeToManagedMap. Of course, this is somewhat half-hearted
2283+
// since we can't/don't do this when there's no virtual dtor available to detour.
2284+
// Anyway, we must be able to call the native dtor in this case even if we don't
2285+
/// own the underlying native instance.
2286+
//
2287+
// 2. When we we pass a disposable object to a function "by value" then the callee
2288+
// calls the dtor on the argument so our marshalling code must have a way from preventing
2289+
// a duplicate call. Here's a native function that exhibits this behavior:
2290+
// void f(std::string f)
2291+
// {
2292+
// ....
2293+
// compiler generates call to f.dtor() at the end of function
2294+
// }
2295+
//
2296+
// IDisposable.Dispose() and Object.Finalize() set callNativeDtor = Helpers.OwnsNativeInstanceIdentifier
2297+
WriteLine($"if (callNativeDtor)");
22632298
if (@class.IsDependent || dtor.IsVirtual)
22642299
WriteOpenBraceAndIndent();
22652300
else
@@ -2282,9 +2317,6 @@ c is ClassTemplateSpecialization ?
22822317
// unmanaged memory isn't always initialized and/or a reference may be owned by the
22832318
// native side.
22842319
//
2285-
// TODO: We should delegate to the dispose methods of references we hold to other
2286-
// generated type instances since those instances could also hold references to
2287-
// unmanaged memory.
22882320
foreach (var field in @class.Layout.Fields.Where(f => f.QualifiedType.Type.IsConstCharString()))
22892321
{
22902322
var ptr = $"(({Helpers.InternalStruct}*){Helpers.InstanceIdentifier})->{field.Name}";

src/Generator/Types/Std/Stdlib.CSharp.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ public override void CSharpMarshalToNative(CSharpMarshalContext ctx)
343343
assign.Name}({varBasicString}, {ctx.Parameter.Name});");
344344
ctx.Return.Write($"{varBasicString}.{Helpers.InstanceIdentifier}");
345345
ctx.Cleanup.WriteLine($@"{varBasicString}.Dispose({
346-
(!Type.IsAddress() || ctx.Parameter?.IsIndirect == true ? "false" : string.Empty)});");
346+
(!Type.IsAddress() || ctx.Parameter?.IsIndirect == true ? "disposing: true, callNativeDtor:false" : string.Empty)});");
347347
}
348348
}
349349

tests/CSharp/CSharp.Tests.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,7 @@ public void TestNativeToManagedMapWithOwnObjects()
497497
[Test]
498498
public void TestCallingVirtualDtor()
499499
{
500+
CallDtorVirtually.Destroyed = false;
500501
using (var callDtorVirtually = new CallDtorVirtually())
501502
{
502503
var hasVirtualDtor1 = CallDtorVirtually.GetHasVirtualDtor1(callDtorVirtually);
@@ -505,6 +506,15 @@ public void TestCallingVirtualDtor()
505506
Assert.That(CallDtorVirtually.Destroyed, Is.True);
506507
}
507508

509+
[Test]
510+
public void TestNonOwning()
511+
{
512+
CallDtorVirtually.Destroyed = false;
513+
var nonOwned = CallDtorVirtually.NonOwnedInstance;
514+
nonOwned.Dispose();
515+
Assert.That(CallDtorVirtually.Destroyed, Is.False);
516+
}
517+
508518
[Test]
509519
public void TestParamTypeToInterfacePass()
510520
{

tests/CSharp/CSharp.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,6 +1019,12 @@ HasVirtualDtor1* CallDtorVirtually::getHasVirtualDtor1(HasVirtualDtor1* returned
10191019
return returned;
10201020
}
10211021

1022+
CallDtorVirtually nonOwnedInstance;
1023+
CallDtorVirtually* CallDtorVirtually::getNonOwnedInstance()
1024+
{
1025+
return &nonOwnedInstance;
1026+
}
1027+
10221028
TestOverrideFromSecondaryBase::TestOverrideFromSecondaryBase()
10231029
{
10241030
}

tests/CSharp/CSharp.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,7 @@ class DLL_API CallDtorVirtually : public HasVirtualDtor1
704704
~CallDtorVirtually();
705705
static bool Destroyed;
706706
static HasVirtualDtor1* getHasVirtualDtor1(HasVirtualDtor1* returned);
707+
static CallDtorVirtually* getNonOwnedInstance();
707708
};
708709

709710
class HasProtectedNestedAnonymousType

0 commit comments

Comments
 (0)