Summarizing very useful comments and research done:
1) Is GC.KeepAlive(this)
always needed in a managed instance method if last instruction is a P/Invoke call using unmanaged resources hold by the instance?
Yes, if you don't want the user of the API to have last responsibility of holding a non-collectible reference for the instance of the managed object in pathological cases, look the example below. But it's not the only way: HandleRef
or SafeHandle
techiniques can also be used to prolong the lifetime of a managed object when doing P/Invoke Interop.
The example will subsequently call native methods through managed instances holding native resources:
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
new Thread(delegate()
{
// Run a separate thread enforcing GC collections every second
while(true)
{
GC.Collect();
Thread.Sleep(1000);
}
}).Start();
while (true)
{
var test = new TestNet();
test.Foo();
TestNet.Dump();
}
}
}
class TestNet
{
static ManualResetEvent _closed;
static long _closeTime;
static long _fooEndTime;
IntPtr _nativeHandle;
public TestNet()
{
_closed = new ManualResetEvent(false);
_closeTime = -1;
_fooEndTime = -1;
_nativeHandle = CreateTestNative();
}
public static void Dump()
{
// Ensure the now the object will now be garbage collected
GC.Collect();
GC.WaitForPendingFinalizers();
// Wait for current object to be garbage collected
_closed.WaitOne();
Trace.Assert(_closeTime != -1);
Trace.Assert(_fooEndTime != -1);
if (_closeTime <= _fooEndTime)
Console.WriteLine("WARN: Finalize() commenced before Foo() return");
else
Console.WriteLine("Finalize() commenced after Foo() return");
}
~TestNet()
{
_closeTime = Stopwatch.GetTimestamp();
FreeTestNative(_nativeHandle);
_closed.Set();
}
public void Foo()
{
// The native implementation just sleeps for 250ms
TestNativeFoo(_nativeHandle);
// Uncomment to have all Finalize() to commence after Foo()
//GC.KeepAlive(this);
_fooEndTime = Stopwatch.GetTimestamp();
}
[DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]
static extern IntPtr CreateTestNative();
[DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]
static extern void FreeTestNative(IntPtr obj);
[DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]
static extern void TestNativeFoo(IntPtr obj);
}
}
For the native call to be always safe we expect finalizer to be called only after Foo()
return. Instead we can easily enforce violations by manually invoking garbage collection in a background thread. Output follows:
Finalize() commenced after Foo() return
WARN: Finalize() commenced before Foo() return
Finalize() commenced after Foo() return
Finalize() commenced after Foo() return
Finalize() commenced after Foo() return
WARN: Finalize() commenced before Foo() return
Finalize() commenced after Foo() return
2) Where I can find documentation?
Documentation of GC.KeepAlive()
provides an example very similar to the managed callback in the original question. HandleRef
has also very interesting considerations about lifecycle of managed objects and Interop:
If you use platform invoke to call a managed object, and the object is
not referenced elsewhere after the platform invoke call, it is
possible for the garbage collector to finalize the managed object.
This action releases the resource and invalidates the handle, causing
the platform invoke call to fail. Wrapping a handle with HandleRef
guarantees that the managed object is not garbage collected until the
platform invoke call completes.
Also link[1] found by @GSerg explains when an object is eligible for collection, pointing that this
reference is not in the root set, allowing it to be collected also when instance method has not returned.
3) Why is this happening only in Release build?
It's an optimization and can happen also in Debug build, with optimization enabled, as pointed by @SimonMourier. It's not enabled by default also in Debug because it could prevent debugging of variables in the current method scope, as explained in these other answers.
[1]
https://devblogs.microsoft.com/oldnewthing/20100810-00/?p=13193?