Делегат С++/CLI в качестве указателя функции (System.AccessViolationException)

Я экспериментировал с делегатами С++/CLI (поскольку я пытаюсь создать справочную библиотеку .NET), и у меня возникла следующая проблема.

Я определяю делегат в С++/CLI, а затем создаю экземпляр делегата в С#, а затем вызываю экземпляр делегата через неуправляемый С++ с помощью указателя функции. Все это работает как ожидается.

Код, чтобы проиллюстрировать это (сначала мой С#)

using System;

namespace TestProgram
{
    class Program
    {
        static void Main(string[] args)
        {
            Library.Test.MessageDelegate messageDelegate = new Library.Test.MessageDelegate(Message);
            Library.Test test = new Library.Test(messageDelegate);
            test.WriteMessage();
            Console.Read();
        }

        static void Message()
        {
            Console.WriteLine(1024);
        }
    }
}

Далее мой управляемый файл С++ (Managed.cpp)

#include "Unmanaged.hpp"
#include <string>

namespace Library
{
    using namespace System;
    using namespace System::Runtime::InteropServices;

    public ref class Test
    {
    public:
        delegate void MessageDelegate();
    internal:
        MessageDelegate^ Message;
        void* delegatePointer;

    public:
        Test(MessageDelegate^ messageDelegate)
        {
            delegatePointer = (void*)Marshal::GetFunctionPointerForDelegate(messageDelegate).ToPointer();
        }

        void WriteMessage()
        {
            Unmanaged::WriteMessage(delegatePointer);
        }
    };
}

И мой неуправляемый файл С++ (Unmanaged.cpp)

#include "Unmanaged.hpp"

namespace Unmanaged
{
    typedef void (*WriteMessageType)();
    WriteMessageType WriteMessageFunc;

    void WriteMessage(void* Function)
    {
        WriteMessageType WriteMessageFunc = (WriteMessageType)(Function);
        WriteMessageFunc();
    }
}

Этот код работает как ожидалось, а выход - "1024", так как метод() вызывается указателем на метод делегата.

Моя проблема возникает при попытке применить тот же метод с делегатом с аргументами, то есть: delegate void MessageDelegate (int number);

Мой код выглядит следующим образом (С#):

using System;

namespace AddProgram
{
    class Program
    {
        static void Main(string[] args)
        {
            Library.Test.MessageDelegate messageDelegate = new Library.Test.MessageDelegate(Message);
            Library.Test test = new Library.Test(messageDelegate);
            test.WriteMessage(1024);
            Console.Read();
        }

        static void Message(int number)
        {
            Console.WriteLine(number);
        }
    }
}

Мой управляемый файл С++:

#include "Unmanaged.hpp"
#include <string>

namespace Library
{
    using namespace System;
    using namespace System::Runtime::InteropServices;

    public ref class Test
    {
    public:
        delegate void MessageDelegate(int number);
    internal:
        MessageDelegate^ Message;
        void* delegatePointer;

    public:
        Test(MessageDelegate^ messageDelegate)
        {
            delegatePointer = (void*)Marshal::GetFunctionPointerForDelegate(messageDelegate).ToPointer();
        }

        void WriteMessage(int number)
        {
            Unmanaged::WriteMessage(delegatePointer, number);
        }
    };
}

И мой неуправляемый файл С++:

#include "Unmanaged.hpp"

namespace Unmanaged
{
    typedef void (*WriteMessageType)(int number);
    WriteMessageType WriteMessageFunc;

    void WriteMessage(void* Function, int number)
    {
        WriteMessageType WriteMessageFunc = (WriteMessageType)(Function);
        WriteMessageFunc(number);
    }
}

При выполнении программы я получаю следующую ошибку:

Необработанное исключение типа "System.AccessViolationException" произошло в Unmanaged Library Test.dll

Дополнительная информация: Попытка чтения или записи защищенной памяти. Это часто свидетельствует о том, что другая память повреждена.

Кстати, окно консоли показывает 1024, но затем следует случайный int (~ 1000000), а затем я получаю ошибку.

Я могу начать думать о некоторых причинах, по которым я получаю эту ошибку, но я не уверен, и мне трудно понять. Если бы кто-нибудь мог сказать мне, почему я получаю эту ошибку, и что я могу сделать, чтобы исправить это, я был бы очень признателен.

Ответы

Ответ 1

 void WriteMessage(void* Function, int number)

Передача указателей функций как void * - довольно плохая идея. Это не позволяет компилятору проверить, что вы делаете что-то неправильно. Что-то не так, хотя компилятор не может обнаружить его в этом конкретном случае. Делегат маршалируется как указатель функции, который использует соглашение о вызове __stdcall, ваш фактический указатель функции использует соглашение о вызове __cdecl, значение по умолчанию для собственного кода. Это приводит к тому, что стек становится несбалансированным при вызове.

Вы можете исправить это, применив атрибут [UnmanagedFunctionPointer] к объявлению делегата, укажите CallingConvention:: Cdecl.

Ответ 2

Указатель функций, созданный из делегата, невидим для сборщика мусора и не учитывается при анализе достижимости.

Из документация:

Вы должны вручную удерживать делегата от сбора сборщика мусора из управляемого кода. Сборщик мусора не отслеживает ссылку [sic] на неуправляемый код.

Если делегат собран, указатель функции остается свисающим, и ваша программа будет вести себя плохо. Нарушение доступа является одним из наиболее вероятных результатов, но не единственной возможностью. Если память, используемая для хранения родного/управляемого батута, повторно используется для некоторых других данных, ЦП может попытаться интерпретировать ее как инструкции, что может означать что угодно.

Решение состоит в том, чтобы обеспечить доступность делегата, например, через класс С++/CLI gcroot, который представляет собой тонкую оболочку вокруг .NET GCHandle.