Search code examples
c++ccallbackc++-cliwrapper

How to wrap C library callbacks in C++/CLI


Given the following C library with a callback event that ask to set a buffer, how to write a proper C++/CLI wrapper in a type safe manner?

// The callback signature
typedef void (__cdecl *BUFFERALLOCATOR)(void *opaque, void **buffer);

// A struct that contains the context of the library
struct lib_context_base_s
{
    // The stored callback function pointer 
    BUFFERALLOCATOR buffer_allocator;
    // Opaque pointer that contain the local context. Needed in C because
    // C doesn't have closures (functions that knows the context where
    // they are defined)
    void* opaque;
};

typedef struct lib_context_base_s lib_context_base;

// Init the base context
lib_context_base* new_lib_context_base()
{
    return malloc(sizeof(lib_context_base));
}

// Free the base context
void free_lib_context_base(lib_context_base *lib_context_base)
{
    free(lib_context_base);
}

// Set the buffer allocation callback
void set_allocate_buffer_callback(lib_context_base *lib_context_base,
                                  BUFFERALLOCATOR allocate_buffer, void* opaque)
{
    lib_context_base->buffer_allocator = allocate_buffer;
    lib_context_base->opaque = opaque;
}

The library should be usable by managed code using the delegate void BufferAllocator(ref IntPtr buffer) .


Solution

  • I will insist on type-safe principles: I know there's already Marshal.GetFunctionPointerForDelegate but that requires function pointer type cast in C++/CLI and hides how marshalling unmanaged->managed works (debugging is much harder and I don't like not understanding what's happening behing the scene). Just noticed the approach is similar to this but doesn't need a managed native class (less overhead). Please, tell me if you know how to further simplify it (mantaining type safety and marshaling control) and reduce overhead.

    The following is the C++/CLI Wrapper.h header:

    #include <gcroot.h>
    
    using namespace System;
    using namespace System::Runtime::InteropServices;
    
    namespace LibraryWrapper
    {
        // Declare the cdecl function that will be used 
        void cdecl_allocate_buffer(void *opaque, void **buffer);
    
        public ref class Library
        {
        public:
            // The BufferAllocator delegate declaration, available to any clr language
        // [In, Out] attributes needed (?) to pass the pointer as reference
            delegate void BufferAllocator([In, Out] IntPtr% buffer);
    
        internal:
            // The stored delegate ref to be used later
            BufferAllocator ^_allocate_buffer;
    
        private:
            // Native handle of the ref Library class, castable to void *
            gcroot<Library^> *_native_handle;
            // C library context
            lib_context_base *_lib_context_base;
    
        public:
            Library();
            ~Library();
            // The clr callback setter equivalent to the C counterpart, don't need
            // the context because in CLR we have closures
            void SetBufferAllocateCallback(BufferAllocator ^allocateBuffer);
        };
    }
    

    Follows C++/CLi Wrapper.cpp defines:

    #include "wrapper.h"
    
    namespace LibraryWrapper
    {
        Library::Library()
        {
            // Construct the native handle
            _native_handle = new gcroot<Library^>();
            // Initialize the library base context
            _lib_context_base = new_lib_context_base();
            // Null the _allocate_buffer delegate instance
            _allocate_buffer = nullptr;
        }
    
        Library::~Library()
        {
            free_lib_context_base(_lib_context_base);
            delete _native_handle;
        }
    
        void Library::SetBufferAllocateCallback(BufferAllocator ^allocateBuffer)
        {
            _allocate_buffer = allocateBuffer;
            // Call the C lib callback setter. Use _native_handle pointer as the opaque data 
            set_allocate_buffer_callback(_lib_context_base, cdecl_allocate_buffer,
                _native_handle);
        }
    
        void cdecl_allocate_buffer(void *opaque, void **buffer)
        {
            // Cast the opaque pointer to the hnative_handle ref (for readability)
            gcroot<Library^> & native_handle = *((gcroot<Library^>*)opaque);
            // Prepare a IntPtr wrapper to the buffer pointer
            IntPtr buffer_cli(*buffer);
            // Call the _allocate_buffer delegate in the library wrapper ref
            native_handle->_allocate_buffer(buffer_cli);
            // Set the buffer pointer to the value obtained calling the delegate
            *buffer = buffer_cli.ToPointer();
        }
    }
    

    Can be used in this way (C#):

    // Allocate a ~10mb buffer in unmanaged memory. Will be deallocated
    // automatically when buffer go out of scope
    IntPtr _buffer = Marshal.AllocHGlobal(10000000);
    
    // Init the library wrapper
    Library library = new Library();
    
    // Set the callback wrapper with an anonymous method
    library.SetBufferAllocateCallback(delegate(ref IntPtr buffer)
    {
        // Because we have closure, I can use the _buffer variable in the outer scope
        buffer = _buffer;
    });