【问题标题】:C# / C++ Asynchronous reverse pinvoke?C#/C++ 异步反向调用?
【发布时间】:2015-02-25 09:42:14
【问题描述】:

我需要从原生 C/C++ .dll 异步调用 C# 代码

在搜索如何做时,我发现我可以创建一个 C# 委托并从中获取一个函数指针,我将在我的本机代码中使用它。

问题是我的本机代码需要异步运行,即在从本机代码创建的单独线程中,这意味着从C#调用的本机代码将在委托之前返回C#被调用。

我在另一个 SO 问题中读到有人遇到了这个问题,尽管 MSDN 说了什么,因为他的委托在被调用之前就被垃圾收集了,因为他的任务是异步的。

我的问题是:使用在本地代码中创建的线程中运行的本地代码中的函数指针调用 C# 委托真的可行吗?谢谢。

【问题讨论】:

    标签: c++ c asynchronous pinvoke native


    【解决方案1】:

    不,这是一个通用错误,并非特定于异步代码。在您的情况下,它只是一个 bit 更可能字节,因为您从来没有 [DllImport] 背后的机器来让您摆脱麻烦。我会解释为什么会出错,也许这会有所帮助。

    始终需要回调方法的委托声明,这就是 CLR 现在知道如何调用该方法的方式。您经常使用 delegate 关键字显式声明它,如果非托管代码是 32 位并假定函数是用 C 或 C++ 编写的,则可能需要应用 [UnmanagedFunctionPointer] 属性。声明很重要,这就是 CLR 如何知道您从本机代码传递的参数需要如何转换为它们的托管等效项的方式。如果您的本机代码将字符串、数组或结构传递给回调,那么这种转换可能会很复杂。

    该场景在 CLR 中高度优化,这很重要,因为托管代码不可避免地在非托管操作系统上运行。这些转换有很多,您看不到它们,因为它们大部分发生在 .NET Framework 代码中。这种优化涉及 thunk,即自动生成的机器代码片段,负责调用外部方法或函数。每当您使用委托进行互操作调用时,都会即时创建 Thunk。在您的情况下,当 C# 代码将委托传递给您的 C++ 代码时。你的 C++ 代码得到一个指向 thunk 的指针,一个函数指针,你存储它并在稍后进行回调。

    “你存储它”是问题开始的地方。 CLR 不知道您存储了指向 thunk 的指针,垃圾收集器看不到它。 Thunks 需要内存,机器代码通常只需要几个字节。它们不会永远存在,当不再需要 thunk 时,CLR 会自动释放内存。

    “不再需要”是问题所在,您的 C++ 代码无法神奇地告诉 CLR 它不再进行回调。所以它使用的简单而明显的规则是,当委托对象被垃圾回收时,thunk 被销毁。

    程序员永远都会遇到这条规则的麻烦。他们没有意识到委托对象的生命周期很重要。在 C# 中尤其棘手,它有很多语法糖,使得创建委托对象非常容易。您甚至不必使用 new 关键字或命名委托类型,只需使用目标方法名称就足够了。这种委托对象的生命周期是 pinvoke 调用。在调用完成并且您的 C++ 代码存储了指针后,委托对象不再在任何地方引用,因此可以进行垃圾回收。

    确切地何时发生,并且thunk被破坏,是不可预测的。 GC 仅在需要时运行。在您拨打电话后可能是一纳秒,当然这不太可能,可能是几秒钟。最棘手的,可能永远不会。发生在典型的单元测试中,否则不会显式调用 GC.Collect()。单元测试很少对 GC 堆施加足够的压力来引发收集。当您从另一个线程进行回调时,更有可能bit,隐含的是其他代码正在其他线程上运行,这使得触发GC的可能性更大。你会更快地发现问题。尽管如此,thunk 迟早会在真正的程序中被销毁。之后在 C++ 代码中进行回调时 Kaboom。

    因此,严格的规则,您必须存储对委托的引用以避免过早收集问题。非常简单,只需将其存储在 C# 程序中声明为 static 的变量中即可。通常足够好,当 C# 代码告诉您的 C++ 代码停止进行回调时,您可能希望将其显式设置回 null,这在您的情况下不太可能。偶尔,您会想使用GCHandle.Alloc() 而不是静态变量。

    【讨论】:

    • 感谢您抽出宝贵时间做出如此详细的回答。我不熟悉 C#,所以更不用说互操作性了。如果我很好理解你写的内容,存储对委托的引用将阻止它被收集,因此“thunk”(无论是什么)也将保持活动状态,我将能够从即使在启动线程的本机函数返回到 C# 之后,线程也是如此,对吗?
    • 我从不热衷于有人将我的一个帖子浓缩成一个句子。但是,是的,就是这样。
    • 这对我来说是新的,所以我只是想综合一下这个想法,以便我更容易理解和记住。
    • 公平通话。请记住,您的 C++ 代码无法解决此问题。因此,如果您收到“您的代码使我的程序崩溃”的投诉,请向 C# 程序员指出此答案。他会利用冗长的答案让他远离你。
    • @Hans Passant -- 当你说“thunk”时,你是指反向 PInvoke 存根吗?我看到反向 PInvoke 存根只为我的委托类型创建一次,但不知何故 CLR 知道要调用哪个实例。所以我深入研究了它,MSIL 中似乎有类似“GetStubContext”的东西,它似乎是特定于线程的,并且能够知道我指的是哪个实例,因为堆栈似乎在某些内存中捕获了该实例。如果我的假设是正确的,那意味着异步调用将不起作用。所以要么我错了(很高兴),要么异步调用不起作用?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-01-19
    相关资源
    最近更新 更多