Реализовать функции обратного вызова C # для C ++ DLL - PullRequest
4 голосов
/ 29 июля 2010

Я пишу оболочку DLL для моей библиотеки C ++, которая будет вызываться из 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 голосов
/ 21 марта 2014

Начиная с Visual Studio 2013, по крайней мере, существует безопасный способ передавать обратные вызовы из C # в C ++, а C ++ сохраняет их и позже вызывает их асинхронно из неуправляемого кода. Что вы можете сделать, это создать управляемый класс C ++ / CX (например, с именем «CallbackManager») для хранения ссылок делегатов обратного вызова на карте, для каждого из которых установлено значение перечисления. Затем ваш неуправляемый код может получить ссылку на управляемый делегат из управляемого класса C ++ / CX CallbackManager через ассоциированное значение делегата enum. Таким образом, вам не нужно хранить необработанные указатели функций, и вам не придется беспокоиться о перемещении делегата или его сборщике мусора: он остается в управляемой куче на протяжении всего своего жизненного цикла.

На стороне C ++ в 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
    };
}  

В CallbacksManager.cpp мы реализуем управляемый класс C ++ / CX, к которому обращается как C #, так и наш неуправляемый код C ++:

#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 голосов
/ 29 июля 2010

Есть некоторые вещи, о которых вы должны знать.Во-первых, если вы вызываете делегат .NET из неуправляемого кода, то если вы не будете следовать некоторым довольно узким ограничениям, вам будет больно.

В идеале вы можете создать делегат в C # и передать его вУправляемый код, поместите его в указатель на функцию, держите его так долго, как хотите, затем вызывайте его без каких-либо побочных эффектов.В документации .NET говорится так.

Я могу вам сказать, что это просто неправда.В конце концов, часть вашего делегата или его thunk будет собирать мусор, и когда вы вызываете указатель на функцию из неуправляемого кода, вы отправляетесь в забвение.Мне все равно, что говорит Microsoft, я следовал их предписанию и буквально следил за тем, как указатели на функции превращались в мусор, особенно в части кода на стороне сервера.

Учитывая это, самый эффективный способ использованияТаким образом, указатели функций таковы:

  • Код 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
}

Когда вы выходите из этой подпрограммы, пиннинг освобождается.

Когда вы действительно, действительнонужно иметь функциюДо того, как вызывать n указателей, которые я оставил на некоторое время, я сделал в C ++ / CLI следующее:

  1. Сделал хеш-таблицу, которая является картой из int -> Delegate.
  2. Сделал регистр/ unregister подпрограммы, которые добавляют новых делегатов в хеш-таблицу, увеличивая счетчик для хеша int.
  3. Создали одну статическую подпрограмму неуправляемого обратного вызова, которая регистрируется в неуправляемом коде с помощью int из вызова register.Когда вызывается эта подпрограмма, она возвращает обратно в управляемый код, говоря: «найдите делегата, связанного с , и вызовите его по этим аргументам».

Что происходит, так как у делегатов нет thunksкоторые делают переходы больше, так как они подразумеваются.Они могут свободно зависать в подвешенном состоянии, перемещаясь GC по мере необходимости.Когда им позвонят, делегат будет закреплен CLR и освобожден по мере необходимости.Я также видел сбой этого метода, особенно в случае кода, который статически регистрирует обратные вызовы в начале времени и ожидает, что они останутся до конца времени.Я видел этот сбой в коде ASP.NET, а также в коде на стороне сервера для Silverlight , работающего через WCF .Это немного нервирует, но способ исправить это состоит в том, чтобы реорганизовать ваш 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 заполняет триплет для безопасного хранения.Таким образом, когда вызывается соответствующее свободное место, вы можете сделать это:

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, чтобы при вызове подпрограммы, выполняющей определенную задачу, она регистрировала обратные вызовы, выполняла задачу, а затем извлекала обратные вызовы.

1 голос
/ 30 июля 2010

Как оказалось, ответ на первоначальный вопрос довольно прост, если вы его знаете, и проблема с обратным вызовом не была проблемой. Параметр входного буфера заменяется парой параметров 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 , или маршаллер сделает это для меня автоматически?

Что касается прекрасного эссе по постаментам, я надеюсь обойти всю проблему, используя статическую функцию.

0 голосов
/ 01 ноября 2016

Нет смысла использовать 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 # в собственный API Cairo.

Вы можете увидеть, как указатель на функцию C cairo_read_func_t реализован в C # и затем используется в качестве обратного вызова для cairo_image_surface_create_from_png_stream .

Здесь аналогичный пример.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...