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

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

我们来看看这个例子吧

CallbackFunction FunctionImplementedInCSharp; void FunctionCalledFromLib(const std::vector& input, std::vector& output) { // Here FunctionImplementedInCSharp (C# delegate) should somehow be called } void RegisterFunction(CallbackFunction f) { FunctionImplementedInCSharp = f; } 

如何定义CallbackFunction以及FunctionCalledFromLib中的代码是什么?

愚蠢的一件事是:如何在C ++代码中删除C#创建的缓冲区?

你应该注意一些事情。 首先,如果您从非托管代码调用.NET委托,那么除非您遵循一些非常狭窄的约束,否则您将陷入痛苦之中。

理想情况下,您可以在C#中创建一个委托,将其传递给托管代码,将其编组为一个函数指针,随意保持它,然后调用它没有任何不良影响。 .NET文档说明了这一点。

我可以告诉你,这根本不是真的。 最终,您的委托或其thunk的一部分将被垃圾收集,当您从非托管代码调用函数指针时,您将被遗忘。 我不在乎微软说的是什么,我按照他们的处方来看信,并看到function指针变成垃圾,特别是在服务器端代码中。

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

  • 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 pinnedDelegate = &delegate; SOME_CALLBACK_PTR p = Marshal::GetFunctionPointerForDelegate(pinnedDelegate); CallDeepIntoUnmanagedCode(p); // This will call p } 

退出此例程时,固定将被释放。

当你真的需要在调用之前暂停一段时间的函数指针时,我在C ++ / CLI中完成了以下操作:

  1. 制作一个哈希表,它是来自int – > delegate的映射。
  2. 制作注册/取消注册例程,将新的委托添加到哈希表中,为哈希int提出计数器。
  3. 制作了一个静态非托管回调例程,该例程使用寄存器调用中的int注册到非托管代码中。 调用此例程时,它会调用托管代码,说“找到与关联的委托并在这些参数上调用它”。

会发生的事情是,代表们没有任何进行转换的thunk,因为它们是隐含的。 他们可以根据需要自由地在GC中移动。 当他们被调用时,代表将被CLR固定并根据需要发布。 我也看到这种方法失败了,特别是在代码在开始时静态注册回调并期望它们保持到时间结束的情况下。 我已经看到ASP.NET代码中的这个失败以及通过WCF工作的Silverlight的服务器端代码。 这是相当令人不安的,但修复它的方法是重构您的API以允许延迟(r)绑定到函数调用。

为了举例说明何时会发生这种情况 – 假设您有一个包含如下函数的库:

 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,这样当您调用完成特定任务的例程时,它会注册回调,执行任务,然后将回调拉出。

至少从Visual Studio 2013开始,有一种安全的方法可以将回调从C#传递到C ++,并让C ++存储它们,并在以后从非托管代码中异步调用它们。 你可以做的是创建一个托管的C ++ / CX类(例如,命名为“CallbackManager”)来保存地图中的回调委托引用,键入每个的枚举值。 然后,您的非托管代码可以通过委托的关联枚举值从托管C ++ / CX CallbackManager类检索托管委托引用。 这样您就不必存储原始函数指针,因此您不必担心委托被移动或垃圾收集:它在整个生命周期中都保留在托管堆中。

在CallbacksManager.h中的C ++端:

 #include  #include  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 CALLBACK_MAP; typedef pair 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  #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 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 (ie, 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  #include  // 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(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(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 ++方法所做的。

事实certificate,原始问题的答案很简单,一旦你知道,整个回调问题都没有问题。 输入缓冲区参数替换为参数对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& input, std::vector& 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的美丽文章,我希望通过使用静态函数来绕过整个问题。

没有必要使用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); } 

这用于将PNG数据从C#传输到本机cairo API。

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

这是一个类似的例子。