Warm tip: This article is reproduced from serverfault.com, please click

c#-通过回调调用本机代码时发生System.EngineExecutionException

(c# - System.EngineExecutionException when PInvoking native code with callbacks)

发布于 2020-11-27 20:43:33

我正在尝试找出引起的根本原因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;
        }
    }
}

当我运行控制台应用程序时,它可以正常运行三分半钟,然后System.EngineExecutionExceptionrun()通话中崩溃

调用堆栈:

    [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.

是什么原因导致飞机坠毁?

其他一些信息:

  • Visual Studio版本是16.8.2。
  • 我正在为x64构建。x86仍然会发生此问题,但是抛出该错误的时间大约是原来的两倍。
  • 我正在使用.NET 5.0,但是我也可以重现.NET Core 3.1和2.1的问题。
    • 特别是.NET Core 2.1,它崩溃的时间要得多,大约需要20秒,而不是三分半钟。
  • 我注意到在应用程序的运行期间内存使用量一直在稳步上升,但不足以耗尽它。它在崩溃时以大约16 kB / s的速度爬升,最终总计达到13 MB(根据诊断工具的报告)。
  • 如果将Task.Delay时间降低到0,或者如果我以同步循环而不是异步方式运行,则无法重现该问题在这些情况下,我没有注意到内存使用量的增加。
  • 如果我run()在C ++代码中注释掉了回调调用,则无法重现该问题
  • 可以重现的问题,如果我使用C#9.0函数指针与LoadLibraryGetProcAddress而不是DllImportstatic extern ...
Questioner
Jeff
Viewed
22
Stephen Cleary 2020-11-28 10:08:54

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

具体来说,这段代码:

set_callback(Callback);

实际上是此代码的语法糖

set_callback(new callback_t(Callback));

如你所见,callback_t实例实际上并没有保存在任何地方。因此,set_callback退货,它不再扎根并且有资格使用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.Delay0将会删除Task最终导致GC分配,从而释放了委托。