System.EngineExecutionException 当 PInvoking native code with callbacks 时

System.EngineExecutionException when PInvoking native code with callbacks

我正在尝试找出 EngineExecutionException 的根本原因。我已将其缩小到我认为最小的可重现示例。

我有两个项目,一个非托管 C++ DLL 和一个托管 C# 控制台应用程序。非托管代码有两个函数,一个存储回调,另一个调用它:

#define WINEXPORT extern "C" __declspec(dllexport)

typedef bool (* callback_t)(unsigned cmd, void* data);
static callback_t callback;

WINEXPORT void set_callback(callback_t cb)
{
    callback = cb;
}

WINEXPORT void run(void)
{
    callback(123, nullptr);
}

在 C# 方面:

using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

namespace ExecutionExceptionReproConsole
{
    class Program
    {
        private const string dllPath = "ExecutionExceptionReproNative.dll";

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        [return: MarshalAs(UnmanagedType.I1)]
        private delegate bool callback_t(uint cmd, IntPtr data);

        [DllImport(dllPath, CallingConvention = CallingConvention.Cdecl)]
        private static extern void set_callback(callback_t callback);

        [DllImport(dllPath, CallingConvention = CallingConvention.Cdecl)]
        private static extern void run();

        static async Task Main(string[] args)
        {
            set_callback(Callback);
            while (!Console.KeyAvailable)
            {
                run();
                await Task.Delay(1);
            }
        }

        static bool Callback(uint cmd, IntPtr data)
        {
            return true;
        }
    }
}

当我 运行 控制台应用程序时,在 run() 调用 System.EngineExecutionException 崩溃之前,它 运行 可以正常运行三分半钟。

调用堆栈:

    [Managed to Native Transition]      Annotated Frame
>   ExecutionExceptionReproConsole.dll!ExecutionExceptionReproConsole.Program.Main(string[] args = {string[0x00000000]}) Line 26    C#  Symbols loaded.
    [Resuming Async Method]     Annotated Frame
    System.Private.CoreLib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state)   Unknown No symbols loaded.
    System.Private.CoreLib.dll!System.Runtime.CompilerServices.AsyncTaskMethodBuilder<System.Threading.Tasks.VoidTaskResult>.AsyncStateMachineBox<ExecutionExceptionReproConsole.Program.<Main>d__4>.MoveNext(System.Threading.Thread threadPoolThread) Unknown No symbols loaded.
    System.Private.CoreLib.dll!System.Runtime.CompilerServices.TaskAwaiter.OutputWaitEtwEvents.AnonymousMethod__12_0(System.Action innerContinuation, System.Threading.Tasks.Task innerTask = Id = 0x000036d4, Status = RanToCompletion, Method = "{null}") Unknown No symbols loaded.
    System.Private.CoreLib.dll!System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(System.Action action, bool allowInlining)   Unknown No symbols loaded.
    System.Private.CoreLib.dll!System.Threading.Tasks.Task.RunContinuations(object continuationObject)  Unknown No symbols loaded.
    System.Private.CoreLib.dll!System.Threading.Tasks.Task.TrySetResult()   Unknown No symbols loaded.
    System.Private.CoreLib.dll!System.Threading.Tasks.Task.DelayPromise.CompleteTimedOut()  Unknown No symbols loaded.
    System.Private.CoreLib.dll!System.Threading.TimerQueueTimer.CallCallback(bool isThreadPool) Unknown No symbols loaded.
    System.Private.CoreLib.dll!System.Threading.TimerQueueTimer.Fire(bool isThreadPool) Unknown No symbols loaded.
    System.Private.CoreLib.dll!System.Threading.TimerQueue.FireNextTimers() Unknown No symbols loaded.

可能导致崩溃的原因是什么?

一些其他信息:

正如其他人所指出的,这是由于 .NET 垃圾收集了实际的委托。这是 .NET 的一个常见问题 p/Invoke。

具体来说,这段代码:

set_callback(Callback);

实际上是 syntactic sugar 这个代码:

set_callback(new callback_t(Callback));

如您所见,callback_t 实例实际上并未保存在任何地方。所以,在set_callbackreturns之后,就不再root了,有资格进行GC了。

最简单的解决方案是将它保存在一个有根变量中,直到它不再被 C++ 代码引用:

static async Task Main(string[] args)
{
    _callback = Callback;
    set_callback(_callback);
    while (!Console.KeyAvailable)
    {
        run();
        GC.Collect();
        await Task.Delay(1);
    }
}

private static callback_t _callback;

请注意,使此同步或将 Task.Delay 更改为 0 将删除最终导致 GC 的 Task 分配,释放委托。