为C++ DLL实现C#回调函数

4
我正在为我的C++库编写DLL包装器,以便从C#调用。这个包装器还应该有从库中调用并在C#中实现的回调函数。这些函数例如具有std::vector作为输出参数。我不知道如何做到这一点。我该如何通过回调函数从C#向C++传递未知大小的缓冲区?
让我们来看一个例子。
CallbackFunction FunctionImplementedInCSharp;

void FunctionCalledFromLib(const std::vector<unsigned char>& input, std::vector<unsigned char>& output)
{
    // Here FunctionImplementedInCSharp (C# delegate) should somehow be called
}

void RegisterFunction(CallbackFunction f)
{
    FunctionImplementedInCSharp = f;
}

如何定义CallbackFunction并编写在FunctionCalledFromLib中的代码?

其中一个让我困惑的问题是:我该如何在C++代码中删除由C#创建的缓冲区?

4个回答

6
至少在Visual Studio 2013中,有一种安全的方法可以将C#中的回调传递到C++中并让C++存储它们并稍后异步地从非托管代码中调用它们。你可以创建一个托管的C++/CX类(例如,命名为"CallbackManager"),在其中使用一个映射来保存每个回调委托的引用,并以一个枚举值为键进行索引。然后,非托管代码可以通过与委托相关联的枚举值从托管的C++/CX CallbackManager类中检索一个托管委托引用。这样,你就不必存储原始函数指针,因此也不必担心委托被移动或垃圾回收:它在整个生命周期中都保持在托管堆中。
在CallbacksManager.h中的C++端:
#include <unordered_map>
#include <mutex>

using namespace Platform;

namespace CPPCallbacks
{
    // define callback IDs; this is what unmanaged C++ code will pass to the managed CallbacksManager class to retrieve a delegate instance
    public enum class CXCallbackType
    {
        cbtLogMessage,
        cbtGetValueForSetting
        // TODO: add additional enum values as you add more callbacks
    }

    // defines the delegate signatures for our callbacks; these are visible to the C# side as well
    public delegate void LogMessageDelegate(int level, String^ message);
    public delegate bool GetValueForSettingDelegate(String^ settingName, String^* settingValueOut);
    // TODO: define additional callbacks here as you need them

     // Singleton WinRT class to manage C# callbacks; since this class is marked 'public' it is consumable from C# as well
    public ref class CXCallbacksManager sealed
    {
    private:
        CXCallbacksManager() { }  // this is private to prevent incorrect instantiation

    public:
        // public methods and properties are all consumable by C# as well
        virtual ~CXCallbacksManager() { }

        static property CXCallbacksManager^ Instance
        {
            CXCallbacksManager^ get();
        }

        bool UnregisterCallback(CXCallbackType cbType);
        void UnregisterAllCallbacks();
        Delegate^ GetCallback(CXCallbackType cbType);

        // define callback registration methods
        RegisterLogMessageCallback(LogMessageDelegate^ cb) { RegisterCallback(CXCallbackType::cbtLogMessage, cb); }
        RegisterGetValueForSettingCallback(GetValueForSettingDelegate^ cb) { RegisterCallback(CXCallbackType::GetValueForSetting, cb); }
        // TODO: define additional callback registration methods as you add more callbacks

    private:
        void RegisterCallback(CXCallbackType cbType, Delegate^ rCallbackFunc);

        typedef unordered_map<CXCallbackType, Delegate^> CALLBACK_MAP;
        typedef pair<CXCallbackType, Delegate^> CBType_Delegate_Pair;

        // Note: IntelliSense errors shown for static data is a Visual Studio IntellSense bug; the code below builds fine
        // See http://social.msdn.microsoft.com/Forums/windowsapps/en-US/b5d43215-459a-41d6-a85e-99e3c30a162e/about-static-member-of-ref-class?forum=winappswithnativecode
        static mutex s_singletonMutex;
        static CXCallbacksManager^ s_rInstance;

        mutex m_callbackMapMutex;
        CALLBACK_MAP m_callbacksMap;   // key=CallbackType, value = C# delegate (function) pointer
    };
}  

在CallbacksManager.cpp中,我们实现了一个被C#和我们的非托管C++代码访问的托管C++/CX类:
#include <assert.h>
#include "CXCallbacksManager.h"

using namespace Platform;

namespace CPPCallbacks
{
    // define static class data
    CXCallbacksManager^ CXCallbacksManager::s_rInstance;
    mutex CXCallbacksManager::s_singletonMutex;

    // Returns our singleton instance; this method is thread-safe
    CXCallbacksManager^ CXCallbacksManager::Instance::get()
    {
        s_singletonMutex.lock();

        if (s_rInstance == nullptr)
            s_rInstance = ref new CXCallbacksManager();  // this lives until the application terminates

        s_singletonMutex.unlock();
        return s_rInstance;
    }

    // Register a C# callback; this method is thread-safe
    void CXCallbacksManager::RegisterCallback(const CXCallbackType cbType, Delegate^ rCallbackFunc)
    {
        _ASSERTE(rCallbackFunc);

        m_callbackMapMutex.lock();
        m_callbacksMap.insert(CBType_Delegate_Pair(cbType, rCallbackFunc)); 
        m_callbackMapMutex.unlock();
    }

    // Unregister a C# callback; this method is thread-safe
    // Returns: true on success, false if no callback was registered for callbackType
    bool CXCallbacksManager::UnregisterCallback(const CXCallbackType cbType)
    {
        m_callbackMapMutex.lock();
        const bool bRemoved = (m_callbacksMap.erase(cbType) > 0);
        m_callbackMapMutex.unlock();

        return bRemoved;
    }

    // Unregister all callbacks; this method is thread-safe
    void CXCallbacksManager::UnregisterAllCallbacks()
    {
        // must lock the map before iterating across it
        // Also, we can't change the contents of the map as we iterate across it, so we have to build a vector of all callback types in the map first.
        vector<CXCallbackType> allCallbacksList;
        m_callbackMapMutex.lock();

        for (CALLBACK_MAP::const_iterator it = m_callbacksMap.begin(); it != m_callbacksMap.end(); it++)
            allCallbacksList.push_back(it->first);

        for (unsigned int i = 0; i < allCallbacksList.size(); i++)
        {
            CALLBACK_MAP::const_iterator it = m_callbacksMap.find(allCallbacksList[i]);
            if (it != m_callbacksMap.end())     // sanity check; should always succeed
                UnregisterCallback(it->first);
        }
        m_callbackMapMutex.unlock();
    }

    // Retrieve a registered C# callback; returns NULL if no callback registered for type
    Delegate^ CXCallbacksManager::GetCallback(const CXCallbackType cbType)
    {
        Delegate^ rCallbackFunc = nullptr;
        m_callbackMapMutex.lock();

        CALLBACK_MAP::const_iterator it = m_callbacksMap.find(cbType);
        if (it != m_callbacksMap.end())
            rCallbackFunc = it->second;
        else
            _ASSERTE(false);    // should never happen! This means the caller either forgot to register a callback for this cbType or already unregistered the callback for this cbType.

        m_callbackMapMutex.unlock();
        return rCallbackFunc;
    }
}

我们的CXCallbacksManager类将委托实例存储在托管堆中,因此现在在C++端上安全地存储回调以供未受管代码稍后异步调用变得容易了。以下是C#端注册两个回调函数的示例:
using CPPCallbacks;

namespace SomeAppName
{
    internal static class Callbacks
    {
        // invoked during app startup to register callbacks for unmanaged C++ code to invoke asynchronously
        internal static void RegisterCallbacks()
        {
            CPPCallbacks.CXCallbacksManager.Instance.RegisterLogMessageCallback(new LogMessageDelegate(LogMessageDelegateImpl));
            CPPCallbacks.CXCallbacksManager.Instance.RegisterGetValueForSettingCallback(new GetValueForSettingDelegate(GetValueForSettingDelegateImpl));
            // TODO: register additional callbacks as you add them
        }

        //-----------------------------------------------------------------
        // Callback delegate implementation methods are below; these are invoked by C++
        // Although these example implementations are in a static class, you could also pass delegate instances created 
        // from inside a non-static class, which would maintain their state just like any other instance method (i.e., they have a 'this' object).
        //-----------------------------------------------------------------

        private static void LogMessageDelegateImpl(int level, string message)
        {
            // This next line is shown for example purposes, but at this point you can do whatever you want because 
            // you are running in a normal C# delegate context.
            Logger.WriteLine(level, message);
        }

        private static bool GetValueForSettingDelegateImpl(String settingName, out String settingValueOut)
        {
            // This next line is shown for example purposes, but at this point you can do whatever you want because 
            // you are running in a normal C# delegate context.
            return Utils.RetrieveEncryptedSetting(settingName, out settingValueOut);   
        }
    };
}

最后,以下是如何从非托管的C++代码中调用您已注册的C#回调函数的方法:
#include <assert.h>
#include <atlstr.h>   // for CStringW
#include "CXCallbacksManager.h"

using namespace CPPCallbacks;

// this is an unmanaged C++ function in the same project as our CXCallbacksManager class
void LogMessage(LogLevel level, const wchar_t *pMsg)
{
    _ASSERTE(msg);

    auto rCallback = static_cast<LogMessageDelegate^>(CXCallbacksManager::Instance->GetCallback(CXCallbackType::cbtLogMessage));
    _ASSERTE(rCallback);
    rCallback(level, ref new String(pMsg));   // invokes C# method
}

// this is an unmanaged C++ function in the same project as our CXCallbacksManager class
// Sets settingValue to the value retrieved from C# for pSettingName
// Returns: true if the value existed and was set, false otherwise
bool GetValueForSetting(const wchar_t *pSettingName, CStringW &settingValue)
{
    bool bRetCode = false;

    auto rCallback = static_cast<GetValueForSettingDelegate^>(CXCallbacksManager::Instance->GetCallback(CXCallbackType::cbtGetValueForSetting));
    _ASSERTE(rCallback);
    if (rCallback)    // sanity check; should never be null
    {
        String^ settingValueOut;
        bRetCode = rCallback(ref new String(pSettingName), &settingValueOut);

        // store the retrieved setting value to our unmanaged C++ CStringW output parameter
        settingValue = settingValueOut->Data(); 
    }
    return bRetCode;
}

这一切都是因为虽然你不能将托管委托引用作为成员变量存储在未托管类中,但你仍然可以从未托管代码中检索和调用托管委托,这就是上述两个本地C++方法所做的。


5

有一些事情你需要知道。首先,如果你从非托管代码中调用.NET委托,除非你遵循一些相当狭窄的限制,否则你将会遭受痛苦。

理想情况下,你可以在C#中创建一个委托,将其传递到托管代码中,将其封送到函数指针中,随意持有它,然后调用它而不会产生任何不良影响。.NET文档是这样说的。

我可以告诉你,这简直就是不真实的。最终,你的委托或其thunk的一部分将被垃圾回收,当你从非托管代码中调用函数指针时,你将被送入无尽虚无之中。我不在乎微软说什么,我已经严格按照他们的处方执行过了,但看到函数指针变成垃圾,尤其是在服务器端代码背后。

鉴于此,使用函数指针最有效的方法如下:

  • C#代码调用非托管代码,传递委托。
  • 非托管代码将委托封送到函数指针中。
  • 非托管代码进行一些工作,可能调用函数指针。
  • 非托管代码放弃所有对函数指针的引用。
  • 非托管代码返回托管代码。

鉴于此,假设我们在C#中有以下内容:

public void PerformTrick(MyManagedDelegate delegate)
{
    APIGlue.CallIntoUnamangedCode(delegate);
}

然后在托管 C++ 中(不是 C++/CLI):

static CallIntoUnmanagedCode(MyManagedDelegate *delegate)
{
    MyManagedDelegate __pin *pinnedDelegate = delegate;
    SOME_CALLBACK_PTR p = Marshal::GetFunctionPointerForDelegate(pinnedDelegate);
    CallDeepIntoUnmanagedCode(p); // this will call p
}

我最近没有在C++/CLI中做过这个 - 语法不同 - 我想它最终会看起来像这样:

// This is declared in a class
static CallIntoUnamangedCode(MyManagedDelegate ^delegate)
{
    pin_ptr<MyManagedDelegate ^> pinnedDelegate = &delegate;
    SOME_CALLBACK_PTR p = Marshal::GetFunctionPointerForDelegate(pinnedDelegate);
    CallDeepIntoUnmanagedCode(p); // This will call p
}

当您退出这些例程时,固定会被释放。

当您真正需要在调用之前挂起函数指针一段时间时,我在C++/CLI中执行了以下操作:

  1. 创建一个哈希表,其中包含从int到委托的映射。
  2. 创建注册/注销例程,将新委托添加到哈希表中,并增加哈希int的计数器。
  3. 创建一个单个静态非托管回调例程,使用来自注册调用的int在未管理的代码中进行注册。当调用此例程时,它会回调到托管代码,说“查找与关联的委托,并在这些参数上调用它”。
发生的情况是,委托不再具有执行转换的thunk,因为它们是隐含的。它们可以自由地徘徊在空中,并根据需要由GC移动。当它们被调用时,委托将被CLR固定并根据需要释放。我也见过这种方法失败的情况,特别是在静态注册回调函数并期望它们一直存在到时间结束的代码中。我在ASP.NET代码后面以及通过WCF工作的Silverlight服务器端代码中看到了这种情况。这相当令人不安,但解决方法是重构您的API以允许更晚绑定到函数调用。
举个例子,当发生这种情况时,假设您有一个包括以下函数的库:
typedef void * (*f_AllocPtr) (size_t nBytes);
typedef void *t_AllocCookie;

extern void RegisterAllocFunction(f_AllocPtr allocPtr, t_AllocCookie cookie);

预期情况是,当您调用分配内存的API时,它将被定向到提供的f_AllocPtr。信不信由你,您可以使用C#编写此代码。很棒:

public IntPtr ManagedAllocMemory(long nBytes)
{
    byte[] data = new byte[nBytes];
    GCHandle dataHandle = GCHandle.Alloc(data, GCHandleType.Pinned);
    unsafe {
        fixed (byte *b = &data[0]) {
            dataPtr = new IntPtr(b);
            RegisterPointerHandleAndArray(dataPtr, dataHandle, data);
            return dataPtr;
        }
    }
}

RegisterPointerHandleAndArray将三元组存储起来以供安全保管。这样,当相应的free被调用时,您可以执行以下操作:

public void ManagedFreeMemory(IntPtr dataPointer)
{
    GCHandle dataHandle;
    byte[] data;
    if (TryUnregister(dataPointer, out dataHandle, out data)) {
        dataHandle.Free();
        // do anything with data?  I dunno...
    }
}

当然,这是愚蠢的,因为分配的内存现在被固定在GC堆中,并且会使其碎片化 - 但重点是它是可行的。
但同样地,我个人曾经见过这种方法失败,除非实际指针的生命周期很短。通常意味着包装您的API,以便在调用完成特定任务的例程时,注册回调函数,执行任务,然后拉出回调函数。

天啊,这超出了我的现有知识范围!我需要花两天时间弄清楚所有这些内容的含义。我只希望里面有我的问题的答案。也许我应该指出,我缺乏高级互操作技能,甚至不知道托管C++和C++/CLI之间的区别(我确实知道很多直接的C++和足够的C#)。你的努力值得赞扬,但是现在我必须付出自己的努力来理解你的帖子。同时,有人能发布一些至少有时可行的简单解决方案吗? - Dialecticus
现在我明白了保持回调函数活动的困难。我的当前解决方案是使用静态函数上的静态字段委托调用RegisterFunction。这在应用程序的长期运行中安全吗? - Dialecticus

1
事实证明,一旦你知道答案,原问题的答案相当简单,而整个回调问题也不是问题。输入缓冲区参数被替换为参数对 unsigned char *input,int input_length,而输出缓冲区参数被替换为参数对 unsigned char **output,int *output_length。C#代理应该像这样。
public delegate int CallbackDelegate(byte[] input, int input_length,
                                     out byte[] output, out int output_length);

而C++中的包装器应该是这样的

void FunctionCalledFromLib(const std::vector<unsigned char>& input, std::vector<unsigned char>& output)
{
    unsigned char *output_aux;
    int output_length;

    FunctionImplementedInCSharp(
        &input[0], input.size(), &ouput_aux, &output_length);

    output.assign(output_aux, output_aux + output_length);

    CoTaskMemFree(output_aux); // IS THIS NECESSARY?
}

最后一行是小谜题的最后一部分。我需要调用CoTaskMemFree吗,还是马歇尔会自动为我完成?

至于plinth写的美丽文章,我希望通过使用静态函数来绕过整个问题。


0

使用C++/cli没有任何意义。

这是我项目中的一个真实世界的例子

public ImageSurface(byte[] pngData)
    : base(ConstructImageSurfaceFromPngData(pngData), true)
{
    offset = 0;
}

private static int offset;

private static IntPtr ConstructImageSurfaceFromPngData(byte[] pngData)
{
    NativeMethods.cairo_read_func_t func = delegate(IntPtr closure, IntPtr out_data, int length)
    {
        Marshal.Copy(pngData, offset, out_data, length);
        offset += length;
        return Status.Success;
    };
    return NativeMethods.cairo_image_surface_create_from_png_stream(func, IntPtr.Zero);
}

这是用于将C#中的PNG数据传输到本地cairo API的。

您可以看到C函数指针cairo_read_func_t在C#中的实现,然后作为cairo_image_surface_create_from_png_stream的回调使用。

这里有一个类似的例子。


1
Cairo库有一个"C"接口,这个接口是标准化的,并且受到所有编译器供应商的尊重。在我的情况下,接口是"C++",带有像std::vector这样的参数。这个接口没有标准化,甚至在同一供应商的不同编译器版本之间也可能发生变化,而且C#不支持使用DllImport属性。在我的情况下,C++/CLI是必须的。 - Dialecticus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接