Установить обратный вызов C # для структуры C ++, полученной через P / Invoke - PullRequest
0 голосов
/ 12 июня 2018

Я пытаюсь использовать внешнюю DLL C ++, которую мне дали (у меня нет источника).

В DLL есть одна функция, которая возвращает указатель на struct.Это struct определяет серию указателей на функции, которые будут использоваться в качестве обратных вызовов моим приложением.

Согласно полученной "документации", я просто "регистрирую" свои обратные вызовы, устанавливая указатель на свой метод обратного вызова.Примерно так:

server->OnConnectionRequest = &myObj.OnConnectionRequest;

Однако я пытаюсь добиться этого в C #.Я частично преуспел.Я могу:

  • загрузить DLL;
  • получить указатель struct* из функции;
  • вызвать некоторые методы, предопределенные для объекта.

Что я не могу сделать, так это установить свои собственные обратные вызовы для объекта: компилятор не жалуется, среда выполнения не жалуется, но обратные вызовы все еще не вызываются.

Я определилтип делегата и класс как таковой (обратите внимание, что ранее это было определено как структура, и она не работала точно так же):

// Original C++ signature from the header:
// void (*OnRemoteConnectionRequest)(void *caller, const char *ip, int &accept);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void OnRemoteConnectionRequestDelegate(IntPtr caller, string ip, ref int accept);

[StructLayout(LayoutKind.Sequential)]
public class RemoteServerPluginI
{
    public OnRemoteConnectionRequestDelegate OnRemoteConnectionRequest;
    // another dozen callbacks omitted
}

У меня есть статический помощник для извлечения экземпляра изdll:

public static class RemoteControlPlugin
{

    [DllImport("data/remoteplugin.dll", CallingConvention = CallingConvention.Cdecl)]
    private static extern IntPtr GetServerPluginInterface();

    private static RemoteServerPluginI _instance = null;

    public static RemoteServerPluginI Instance
    {
        get
        {
            if (_instance != null)
                return _instance;

            var ptr = GetServerPluginInterface();
            _instance = Marshal.PtrToStructure<RemoteServerPluginI>(ptr);
            if (_instance == null)
                throw new InvalidOperationException("Could not obtain the server instance");

            return _instance;
        }
    }
}

И, наконец, я использую это для регистрации своих обратных вызовов:

public class CallBacks
{
    public CallBacks(RemoteServerPluginI server)
    {
        server.OnRemoteConnectionRequest = this.OnRemoteConnectionRequest;
    }

    public void OnRemoteConnectionRequest(IntPtr caller, string ip, ref int accept)
    {
        Console.WriteLine($"Remote connection request from {ip}");

        // I try to force a reject to see an error on the client,
        // but the client always connects successfully, implying
        // we never get to run this
        accept = 0;
    }
}

static void Main()
{
    var cb = new Callbacks(RemoteControlPlugin.Instance);
    RemoteControlPlugin.Instance.StartServer();
}

И все же, когда я использую клиентское приложение, чтобы попытаться подключиться к моему серверумой обратный вызов никогда не запускается.Как вы видите, в моем обратном вызове я отказываюсь от соединения, поэтому клиент должен выйти с ошибкой, но это не так.

Что я делаю не так?

1 Ответ

0 голосов
/ 12 июня 2018

Этот:

_instance = Marshal.PtrToStructure<RemoteServerPluginI>(ptr);

создаст копию RemoteServerPluginI, поэтому вы будете работать с ней.Совершенно неверно.

Используйте Marshal.WriteIntPtr() для прямой записи в ptr, например:

Marshal.WriteIntPtr(remoteServerPluginIPtr, 0, Marshal.GetFunctionPointerForDelegate(OnRemoteConnectionRequest));

Где вместо 0 вы должны поместить смещение указателя делегата в struct.

Тогда вы не показываете С-сигнатуру обратного вызова ... Возможно, вы допустили некоторые ошибки даже там.

Как написано Фойгтом, другой очень важно то, что делегат должен оставаться в живых все время, пока его может использовать нативная библиотека.Стандартный способ сделать это - поместить его в поле / свойство объекта, а затем обязательно сохранить объект (например, сохранив ссылку на него).Вы делаете это с class RemoteServerPluginI.Еще один способ сделать это - GCHandle.Alloc(yourdelegate, GCHandleType.Normal), а затем GCHandle.Free(), когда вы уверены, что нативный код не будет вызывать его когда-либо.

Несколько простых примеров кода.

C сторона:

extern "C"
{
    typedef struct _RemoteServerPluginI
    {
        void(*OnRemoteConnectionRequest)(void *caller, wchar_t *ip, int *accept);
        void(*StartServer)(void);
    } RemoteServerPluginI;

    void StartServer();

    RemoteServerPluginI _callbacks = { NULL, StartServer };

    void StartServer()
    {
        int accept = 0;
        _callbacks.OnRemoteConnectionRequest(NULL, L"127.0.0.1", &accept);
        wprintf(L"Accept: %d", accept);
    }

    __declspec(dllexport) RemoteServerPluginI* GetServerPluginInterface()
    {
        return &_callbacks;
    }
}

Сторона C #:

[DllImport("CPlusPlusSide.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr GetServerPluginInterface();

public static void RemoteConnectionRequestTest(IntPtr caller, string ip, ref int accept)
{
    Console.WriteLine("C#: ip = {0}", ip);
    accept = 1;
}

public class Callbacks
{
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate void OnRemoteConnectionRequestDelegate(IntPtr caller, [MarshalAs(UnmanagedType.LPWStr)]string ip, ref int accept);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate void StartServerDelegate();

    public OnRemoteConnectionRequestDelegate RemoteConnectionRequest { get; set; }

    public StartServerDelegate StartServer { get; set; }
}

, а затем:

IntPtr rsp = GetServerPluginInterface();

var callbacks = new Callbacks
{
    RemoteConnectionRequest = RemoteConnectionRequestTest
};

Marshal.WriteIntPtr(rsp, 0, Marshal.GetFunctionPointerForDelegate(callbacks.RemoteConnectionRequest));
callbacks.StartServer = Marshal.GetDelegateForFunctionPointer<Callbacks.StartServerDelegate>(Marshal.ReadIntPtr(rsp, IntPtr.Size));

callbacks.StartServer();

Обратите внимание, что в вашем примере StartServer является делегатом, содержащимся в RemoteServerPluginI C структура.Поэтому мы должны получить его значение с помощью Marshal.ReadIntPtr и создать для него делегат .NET.Обратите внимание на использование GC.KeepAlive(), чтобы убедиться, что объект остается живым до определенной точки в коде.Другой распространенный метод - использование переменной static (время жизни переменных static - до конца программы)

Пример с инкапсуляцией различных Marshal:

public class Callbacks
{
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate void OnRemoteConnectionRequestDelegate(IntPtr caller, [MarshalAs(UnmanagedType.LPWStr)]string ip, ref int accept);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate void StartServerDelegate();

    private IntPtr ptr;

    public static implicit operator Callbacks(IntPtr ptr)
    {
        return new Callbacks(ptr);
    }

    public Callbacks(IntPtr ptr)
    {
        this.ptr = ptr;

        {
            IntPtr del = Marshal.ReadIntPtr(ptr, 0);

            if (del != IntPtr.Zero)
            {
                remoteConnectionRequest = Marshal.GetDelegateForFunctionPointer<OnRemoteConnectionRequestDelegate>(del);
            }
        }

        {
            IntPtr del = Marshal.ReadIntPtr(ptr, IntPtr.Size);

            if (del != IntPtr.Zero)
            {
                startServer = Marshal.GetDelegateForFunctionPointer<StartServerDelegate>(del);
            }
        }
    }

    private OnRemoteConnectionRequestDelegate remoteConnectionRequest;

    private StartServerDelegate startServer;

    public OnRemoteConnectionRequestDelegate RemoteConnectionRequest
    {
        get => remoteConnectionRequest;
        set
        {
            if (value != remoteConnectionRequest)
            {
                remoteConnectionRequest = value;
                Marshal.WriteIntPtr(ptr, 0, remoteConnectionRequest != null ? Marshal.GetFunctionPointerForDelegate(remoteConnectionRequest) : IntPtr.Zero);
            }
        }
    }

    public StartServerDelegate StartServer
    {
        get => startServer;
        set
        {
            if (value != startServer)
            {
                startServer = value;
                Marshal.WriteIntPtr(ptr, IntPtr.Size, startServer != null ? Marshal.GetFunctionPointerForDelegate(startServer) : IntPtr.Zero);
            }
        }
    }
}

а затем

Callbacks callbacks = GetServerPluginInterface();
callbacks.RemoteConnectionRequest = RemoteConnectionRequestTest;
callbacks.StartServer();

while (true)
{
}

Обратите внимание, что тогда я бы сделал все строго типизированным, полностью скрыв IntPtr:

[DllImport("CPlusPlusSide.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern CallbacksPtr GetServerPluginInterface();

[StructLayout(LayoutKind.Sequential)]
public struct CallbacksPtr
{
    public IntPtr Ptr;
}

public class Callbacks
{
    public static implicit operator Callbacks(CallbacksPtr ptr)
    {
        return new Callbacks(ptr.Ptr);
    }

    private Callbacks(IntPtr ptr)
    {
        ...
}

Добавление CallbacksPtr, которое является прокладкой для IntPtr, который может быть неявно преобразован в полный Callbacks объект.

...