Почему для вызова метода класса из DLL требуется виртуальный спецификатор? - PullRequest
1 голос
/ 22 мая 2019

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

Давайте начнем с простого Log класса с методом Write, принимающего аргумент const char*.

class ENGINE_API Log
{
private:
    const char* Category;

public:
    Log(const char* Category);

    void Write(const char* format, ...);
};

Класс помечен __declspec (с использованием макроса ENGINE_API ) как dllexport при построении его внутри DLL-файла "owner" и помечен как dllimport при использовании только заголовка при сборке другой DLL .

Также первая DLL «владельца» экспортировала функцию extern C с именем CreateLogInstance, которая просто создает экземпляр класса Log и возвращает его.

PUBLIC_FUNCTION Log* CreateLogInstance(const char* name)
{
    return new Log(name);
}

Во второй DLL я вызываю LoadLibrary и GetProcAddress с правильным приведением к указателю на функцию. Чем я просто вызываю метод Write с некоторым текстом.

typedef Log*(*CreateLogInstanceFunction)(const char*);

HINSTANCE moduleHandle = LoadLibrary("Engine.dll");
CreateLogInstanceFunction createLogInstanceFunction = (CreateLogInstanceFunction)GetProcAddress(moduleHandle, "CreateLogInstanceWithName");

// omitting the null checks etc

Output = createLogInstanceFunction("Game");
Output->Write("Hello Game");

Все отлично работает с одним требованием: метод Write должен быть помечен как virtual, если это не компиляция, на которой он сам не работает LNK2019 неразрешенная ошибка внешнего символа в строке, где Write метод вызывается.

Мне не нужно, чтобы оно было virtual в моем случае (для некоторого полиморфизма), и мой вопрос - почему для получения этой работы необходим спецификатор virtual?

Это также работает (без спецификатора virtual), когда я решаю использовать Динамическое связывание во время загрузки и связывать во время сборки файл .lib, но мне нравится придерживаться Run-Time Динамическое связывание .

Спасибо.

Использование Windows 10 (1809) и Visual Studio 2019 с последней версией Windows SDK.

1 Ответ

0 голосов
/ 22 мая 2019

для вызова функции из внешнего модуля нам нужен адрес этой функции.

если мы пометим функцию как virtual - внутри объекта существует указатель на таблицу (так называемый vftable *)1006 *) где хранятся указатели на все виртуальные функции для этого объекта.и компилятор генерирует соответствующий код для вызова функции через этот указатель.

, поэтому, когда вы пишете

class log
{
public:
    virtual void Write(const char* format, ...);
};

, компилятор создает скрытую структуру vftable внутри объекта

class log
{
public:
    virtual void Write(const char* format, ...);

    struct vftable {
        void (* Write)(const char* format, ...);
    };
};

и вызов

Output->Write("Hello Game");

действительно реализован как

(*(log::vftable**)Output)->Write("Hello Game");

, поэтому здесь у нас есть указатель объекта ( Output ), внутри него существует указатель на log :: vftable , и внутри этой таблицы существует указатель на Запись функции.обратите внимание, что в этом случае нам не нужно помечать класс как dllexport или dllimport

, поэтому работа с виртуальными функциями - все, что вам нужновызвать виртуальную функцию - указатель на объект.

в случае, если функция не виртуальная - и помечена как __ declspec (dllimport) компилятор объявить скрытую переменную, который является указателем на функцию.и функция будет вызываться через эту переменную.поэтому, когда мы пишем:

class __declspec(dllimport) log
{
public:
    virtual void Write(const char* format, ...);
};

void demo(log* Output)
{
    Output->Write("Hello Game");
}

компилятор фактически делает следующее:

extern void (* __imp_?Write@log@@QEAAXPEBDZZ)(log* This, const char* format, ...);

void demo(log* Output)
{
    __imp_?Write@log@@QEAAXPEBDZZ(Output, "Hello Game");
}

обратите внимание, что указатель на функцию __ imp_? Write @ log @@ QEAAXPEBDZZ только объявлен (с extern ), но не реализовано.если вы соберете без соответствующего lib файла (где реализовано __ imp_? Write @ log @@ QEAAXPEBDZZ symbol), вы получите ошибку компоновщика: неразрешенный внешний символ __imp_? Write @ log @@QEAAXPEBDZZ

, поэтому, если функция-член объявляется без virtual и класса, объявленного как __declspec (dllimport) , необходимо использовать соответствующие lib .загрузчик, при загрузке экспортируемого PE ? Write @ log @@ QEAAXPEBDZZ адреса функции из Engine.dll и записи этого адреса в __ imp_? Write@ log @@ QEAAXPEBDZZ

Конечно, существует еще один вариант - реализовать все это самостоятельно.взять на себя работу грузчика.как-то так

#pragma comment(linker, "/alternatename:__imp_?Write@log@@QEAAXPEBDZZ=?__imp__Write_log__QEAAXPEBDZZ@@3PEAXEA")

void* __imp__Write_log__QEAAXPEBDZZ = 0;

BOOL LoadEngine()
{
    if (HMODULE hmod = LoadLibraryW(L"Engine.dll"))
    {
        if (__imp__Write_log__QEAAXPEBDZZ = GetProcAddress(hmod, "?Write@log@@QEAAXPEBDZZ"))
        {
            return TRUE;
        }
    }

    return FALSE;
}

после этого мы уже можем вызывать Output->Write("Hello Game");

, конечно, в c ++ мы не можем напрямую объявить имя __ imp_? Write @log @@ QEAAXPEBDZZ , поэтому нужно использовать трюк с опцией компоновщика / alternatename .или мы можем объявить это имя точно в отдельном asm файле

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