Implement callback C# functions for C++ DLL

后端 未结 4 1459
攒了一身酷
攒了一身酷 2021-02-06 16:29

I\'m writing a DLL wrapper for my C++ library, to be called from C#. This wrapper should also have callback functions called from the library and implemented in C#. These functi

相关标签:
4条回答
  • 2021-02-06 16:44

    There is no point to using C++/cli.

    And here is a real world example from my project.

    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);
    }
    

    That is used to transfer PNG data from C# to the native cairo API.

    You can see how the C function pointer cairo_read_func_t is implemented in C# and then used as a callback for cairo_image_surface_create_from_png_stream.

    Here is a similar example.

    0 讨论(0)
  • 2021-02-06 16:46

    There are some things you should be aware of. The first is that if you are calling a .NET delegate from unmanaged code, then unless you follow some pretty narrow constraints, you will be in for pain.

    Ideally, you can create a delegate in C# pass it into managed code, marshal it into a function pointer, hold onto it for as long as you like, then call it with no ill effects. The .NET documentation says so.

    I can tell you that this is simply not true. Eventually, part of your delegate or its thunk will get garbage collected and when you call the function pointer from unmanaged code you will get sent into oblivion. I don't care what Microsoft says, I've followed their prescription to the letter and watched function pointers get turned into garbage, especially in server-side code behinds.

    Given that, the most effective way to use function pointers is thus:

    • C# code calls unmanaged code, passing in delegate.
    • Unmanaged code marshals the delegate to a function pointer.
    • Unmanaged code does some work, possible calling the function pointer.
    • Unmanaged code drops all references to the function pointer.
    • Unmanaged code returns to managed code.

    Given that, suppose we have the following in C#:

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

    and then in managed C++ (not C++/CLI):

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

    I haven't done this recently in C++/CLI - the syntax is different - I think it ends up looking like this:

    // 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
    }
    

    When you exit this routines, the pinning gets released.

    When you really, really need to have function pointers hanging around for a while before calling, I have done the following in C++/CLI:

    1. Made a hashtable that is a map from int -> delegate.
    2. Made register/unregister routines that add new delegates into the hashtable, bumping up a counter for the hash int.
    3. Made a single static unmanaged callback routine that is registered into unmanaged code with an int from the register call. When this routine is called, it calls back into managed code saying "find the delegate associated with <int> and call it on these arguments".

    What happens is that the delegates don't have thunks that do transitions anymore since they're implied. They're free to hang around in limbo being moved by the GC as needed. When they get called, the delegate will get pinned by the CLR and released as needed. I have also seen this method fail, particularly in the case of code that statically registers callbacks at the beginning of time and expects them to stay around to the end of time. I've seen this fail in ASP.NET code behind as well as server side code for Silverlight working through WCF. It's rather unnerving, but the way to fix it is to refactor your API to allow late(r) binding to function calls.

    To give you an example of when this will happen - suppose you have a library that includes a function like this:

    typedef void * (*f_AllocPtr) (size_t nBytes);
    typedef void *t_AllocCookie;
    
    extern void RegisterAllocFunction(f_AllocPtr allocPtr, t_AllocCookie cookie);
    

    and the expectation is that when you call an API that allocates memory, it will be vectored off into the supplied f_AllocPtr. Believe it or not, you can write this in C#. It's sweet:

    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 stuffs the triplet away for safe keeping. That way when the corresponding free gets called, you can do this:

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

    And of course this is stupid because allocated memory is now pinned in the GC heap and will fragment it to hell - but the point is that it's doable.

    But again, I have personally seen this fail unless the actual pointers are short lived. This typically means wrapping your API, so that when you call into a routine that accomplishes a specific task, it registers callbacks, does the task, and then pulls the callbacks out.

    0 讨论(0)
  • 2021-02-06 16:51

    As of Visual Studio 2013 at least, there is a safe way to pass callbacks from C# to C++ and have C++ store them and invoke them asynchronously later from unmanaged code. What you can do is create a managed C++/CX class (e.g., named "CallbackManager") to hold the callback delegate references in a map, keyed off an enum value for each. Then your unmanaged code can retrieve a managed delegate reference from the managed C++/CX CallbackManager class via the delegate's associated enum value. That way you don't have to store raw function pointers and so you don't have to worry about the delegate being moved or garbage-collected: it stays in the managed heap throughout its lifecycle.

    On the C++ side in CallbacksManager.h:

    #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
        };
    }  
    

    In CallbacksManager.cpp we implement the managed C++/CX class accessed by both C# and our unmanaged C++ code:

    #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;
        }
    }
    

    The delegate instances remain stored in the managed heap by our CXCallbacksManager class, so now it's easy and safe to store callbacks on the C++ side for unmanaged code to invoke later asynchronously. Here is the C# side registering two callbacks:

    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);   
            }
        };
    }
    

    Lastly, here is how to invoke your registered C# callbacks from unmanaged C++ code:

    #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;
    }
    

    This all works because although you cannot store a managed delegate reference as a member variable inside an unmanaged class, you can still retrieve and invoke a managed delegate from unmanaged code, which is what the above two native C++ methods do.

    0 讨论(0)
  • 2021-02-06 17:08

    As it turns out, the answer to the original question is rather simple, once you know it, and the whole callback issue was no issue. The input buffer parameter is replaced with parameter pair unsigned char *input, int input_length, and the output buffer parameter is replaced with parameter pair unsigned char **output, int *output_length. The C# delegate should be something like this

    public delegate int CallbackDelegate(byte[] input, int input_length,
                                         out byte[] output, out int output_length);
    

    And wrapper in C++ should be something like this

    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?
    }
    

    The last line is the last part of the mini-puzzle. Do I have to call CoTaskMemFree, or will the marshaller do it for me automagically?

    As for the beautiful essay by plinth, I hope to bypass the whole problem by using a static function.

    0 讨论(0)
提交回复
热议问题