Проблема с функцией обратного вызова C в C# - как передать значение указателю?

У меня есть обратный вызов C, определенный следующим образом:

Int16 (CALLBACK *ProcessMessage)(Uint16 ServerId,
   const char PTR *RequestMsg, Uint32 RequestSize,
   char PTR **ResponseMsg, Uint32 PTR *ResponseSize,
   Int16 PTR *AppErrCode);

Пример использования этого обратного вызова в C:

Int16 CALLBACK ProcessMessage(Uint16 ServerId, const char PTR *RequestMsg, Uint32 RequestSize, char PTR **ResponseMsg, Uint32 PTR *ResponseSize, Int16 PTR *AppErrCode)
{
    printf("ProcessMessage() -> ServerId=%u\n", ServerId);

    //**** SET THE VALUE FOR RESPONSEMSG (POINTER), THAT'S WHAT I NEED TO DO IN C# ****       
    sprintf(resp,"(%05lu) REPLY TEST", ServerId);
    *ResponseMsg = resp;

    printf("ProcessMessage() -> atribuido %p(p) a *ResponseMsg\n", *ResponseMsg);
    *ResponseSize = strlen(*ResponseMsg);
    *AppErrCode = -1; 
    return SS_OK;
}

Затем я реализовал этот обратный вызов на С#:

 [DllImport("Custom.dll", SetLastError = true)]
    static extern Int16 SS_Initialize(
        UInt16[] ServerIds,
        UInt16 ServerQty,
        [MarshalAs(UnmanagedType.LPStr)] string Binding,
        [MarshalAs(UnmanagedType.LPStr)] string LogPath,
        UInt16 LogDays,
        Int16 LogLevel,
        UInt16 MaxThreads,
        UInt16 MaxConThread,
        ProcessMessageCallback callback);  

Определение обратного вызова:

public delegate Int16 ProcessMessageCallback(
        UInt16 ServerId,
        [MarshalAs(UnmanagedType.LPStr)] string RequestMsg,
        UInt32 RequestSize,
        [MarshalAs(UnmanagedType.LPStr)] ref string ResponseMsg,
        ref UInt32 ResponseSize,
        ref Int16 AppErrCode);

Метод, который устанавливает обратный вызов:

public void Call_SS_Initialize(
        UInt16[] serverIds,
        string binding,
        string logPath,
        UInt16 logDays,
        Int16 logLevel,
        UInt16 maxThreads,
        UInt16 maxConThread
        )
    {
        Int16 ret;
        try
        {
            pmc = new ProcessMessageCallback(ProcessMessage);

            ret = SS_Initialize(
                serverIds,
                Convert.ToUInt16(serverIds.ToList().Count),
                binding,
                logPath,
                logDays,
                logLevel,
                maxThreads,
                maxConThread,
                pmc);
        }
    }

И, наконец, метод обратного вызова, где ПРОБЛЕМА:

public Int16 ProcessMessage(
      UInt16 ServerId,
      string RequestMsg,
      UInt32 RequestSize,
      ref string ResponseMsg,
      ref UInt32 ResponseSize,
      ref Int16 AppErrCode)
    {
       //Implement return to ResponseMsg POINTER
    }

Проблема в том, что ResponseMsg на самом деле является POINTER в C. Поэтому в методе C# ProcesMessage я должен установить для ResponseMsg пробел в память (указатель), откуда DLL получит строку.

Я не могу просто установить ResponseMsg = "REPLY", потому что, когда метод завершает работу, память, в которой была строка, уже уничтожена.

Как мне это сделать?? Пожалуйста, любые советы приветствуются!

Спасибо!


person Rodrigo Pires    schedule 06.08.2009    source источник
comment
Это выглядит нормально для меня на первый взгляд. Когда вы устанавливаете ResponseMsg в своем обработчике C#, что вы получаете на конце C? Ошибка отладки? Мусор? Ничего такого?   -  person Ben M    schedule 07.08.2009
comment
Я думаю, что использование ref там дает ему двойную косвенность - то есть он фактически не передает там char*, а скорее char**.   -  person Pavel Minaev    schedule 07.08.2009
comment
Эх, извините, пропустил тот факт, что это действительно то, что требуется!   -  person Pavel Minaev    schedule 07.08.2009
comment
Бен, кажется, что строка затерта, потому что программа на C ничего не получает в переменной ResponseMsg. Павел, я почти ничего не знаю о программировании на C... можешь объяснить получше???   -  person Rodrigo Pires    schedule 07.08.2009
comment
Присмотритесь повнимательнее: каково определение PTR? ResponseMsg определяется как char PTR ** .. это char ***? Ведь это не сработает. :-)   -  person Ben M    schedule 07.08.2009
comment
@ Родриго, в коде C, каково время жизни resp?   -  person Pavel Minaev    schedule 07.08.2009
comment
Бен, у него нет конкретного определения. Как я вижу, это так: #define PTR.   -  person Rodrigo Pires    schedule 07.08.2009
comment
Павел, resp - это глобальное поле в классе...   -  person Rodrigo Pires    schedule 07.08.2009


Ответы (5)


Вот что я сделал, чтобы воссоздать это. Может мой эксперимент поможет.

Код С#:

public delegate void ProcessMessageCallback(
        [MarshalAs(UnmanagedType.LPStr)] ref string ResponseMsg);

static class test
{
    [DllImport("test.dll")]
    static extern void TestCallback(ProcessMessageCallback callback);

    static public void Main(string[] args)
    {
        TestCallback(MyCallback);
    }

    static void MyCallback(ref string ResponseMsg)
    {
        ResponseMsg = "hi there";
    }
}

Код C (в DLL):

#include <windows.h>
#include "objbase.h"

__declspec(dllexport) void TestCallback(
    void (* CALLBACK managedCallback(char **)))
{
    char *test = NULL;
    managedCallback(&test);
    printf(test);
    CoTaskMemFree(test); // NB!
}

Это успешно распечатывает «привет».

Примечание. CoTaskMemFree следует использовать для освобождения памяти, выделенной слоем P/Invoke. Если вы хотите, чтобы возвращаемая строка хранилась дольше, чем метод, вызывающий обратный вызов C#, подумайте о том, чтобы скопировать ее в другое место, прежде чем освобождать возвращенную память.

person Ben M    schedule 06.08.2009

Причина, по которой это сбивает с толку P/Invoke, заключается в необычной семантике управления памятью, подразумеваемой этим. Вы фактически возвращаете строку, выделенную неизвестным образом, вызывающей стороне, которая затем не освобождает ее (по крайней мере, насколько я вижу). Это довольно проблематичный дизайн (не потокобезопасный и, возможно, не реентерабельный) и определенно нетипичный.

Здесь вы ничего не можете сделать со строкой, потому что код C фактически не получает указатель непосредственно на ваши строковые данные. Поскольку строки C# представляют собой Unicode, а вы запросили сортировку строки как ANSI, будет создана и возвращена «копия» (с результатом преобразования Unicode в ANSI). Я понятия не имею ни о продолжительности жизни этого, ни о том, как это контролировать, и я не вижу никаких гарантий в документации.

Итак, похоже, вам лучше всего управлять этим самостоятельно, используя Marshal.AllocHGlobal для выделения буфера (возможно, только один раз для всех вызовов, так же, как ваш код C), Encoding.GetBytes для преобразования строк в байтовые массивы и Marshal.Copy для копирования результирующего байт в выделенный буфер.

person Pavel Minaev    schedule 06.08.2009
comment
Выделение памяти уровнем P/Invoke довольно хорошо документировано. В этом случае он использует распределитель памяти задач OLE по умолчанию, поэтому для ее освобождения на стороне C следует использовать CoTaskMemFree. - person Ben M; 07.08.2009
comment
Павел, спасибо за ваши комментарии. Но я никогда не работал с этими ребятами, о которых вы говорите (AllocHGlobal, Marshal.Copy и т. д.). Не могли бы вы помочь мне собрать все вместе?? Спасибо!!! - person Rodrigo Pires; 07.08.2009
comment
Бен, из любопытства, можешь дать ссылку, где это описано? В любом случае, я сомневаюсь, что код C собирается CoTaskMemFree его, и кажется, что это то, что есть (т.е. не может быть исправлено). - person Pavel Minaev; 07.08.2009
comment
Вот и все, код C от сторонней компании. @Pavel, не могли бы вы привести пример использования Marshal.AllocHGlobal и Copy, пожалуйста??? Спасибо!! - person Rodrigo Pires; 07.08.2009
comment
Вот достойная статья на эту тему. msdn.microsoft.com/en-us/magazine/cc164193.aspx - person Ben M; 07.08.2009

Попробуйте изменить тип ResponseMsg на StringBuilder и убедитесь, что емкости достаточно для хранения ответа.

person Hasani Blackwell    schedule 24.10.2009

Код С#:

public delegate void ProcessMessageCallback(StringBuilder ResponseMsg);

static class test
{
    [DllImport("test.dll")]
    static extern void TestCallback(ProcessMessageCallback callback);

    static public void Main(string[] args)
    {
        TestCallback(MyCallback);
    }

    static void MyCallback(StringBuilder ResponseMsg)
    {
        ResponseMsg.Append("hi there");
    }
}

Код C (в DLL):

typedef int(__stdcall * Callback)(char *message);

__declspec(dllexport) void TestCallback(Callback callback) 
{
    char* test = (char*)malloc(10000);
    test[0] = '\0';
    callback(test);
    printf(test);
    free(test);
}
person Mohamamd A.Mohadjer    schedule 05.12.2015

Как насчет :

IntPtr p = Marshal.GetFunctionPointerForDelegate(pmc);
person R Ubben    schedule 06.08.2009
comment
R Ubben, в каком месте кода я должен его использовать и как?? - person Rodrigo Pires; 07.08.2009