Как я могу "запустить" DLL как исполняемый файл во время выполнения?
Я хочу написать очень, очень маленькую программу, которая анализирует аргументы запуска и выбирает одну из нескольких DLL для загрузки.
Я уже написал приложение, которое я хотел бы "запустить" как DLL, написав его как приложение, а затем изменив свойства проекта Visual Studio, чтобы вместо этого построить его как DLL. Я знаю, что мне нужно совместно использовать LoadLibrary и GetProcAddress, чтобы получить желаемую функциональность, но у меня возникают проблемы с нахождением четкой и исчерпывающей документации по этому вопросу, поскольку многие варианты использования на самом деле не такого рода. Кроме того, я должен идти по этому пути в зависимости от ограничений проекта и платформы.
Я нашел эту страницу, на которой есть некоторая информация, но она не достаточно ясна для меня, чтобы приспособиться к моим целям.
Редактировать: Вот где я сейчас нахожусь.
У меня есть проект DLL, чья сигнатура основной функции выглядит примерно так:
__declspec(dllexport) int cdecl main(int argc, char *argv[])
У меня также есть проект приложения, попытка загрузки DLL и запуска вышеуказанной функции выглядит следующим образом:
typedef int (CALLBACK* LPFNDLLFUNC1)(int, char *);
...
HMODULE dllHandle = NULL;
BOOL freeResult, runTimeLinkSuccess = FALSE;
LPFNDLLFUNC1 lpfnDllFunc1; // Function pointer
if (args->IsEmpty())
{
dllHandle = LoadLibrary(L"TrueApplication.dll");
if (NULL != dllHandle)
{
lpfnDllFunc1 = (LPFNDLLFUNC1)GetProcAddress(dllHandle, "main");
if (lpfnDllFunc1)
{
int retVal = lpfnDllFunc1(0, "1");
}
В настоящее время вызов LoadLibrary работает, но не GetProcAddress.
3 ответа
Прежде всего, изменение типа проекта с исполняемого на DLL недостаточно для создания DLL. Вам также нужно экспортировать некоторые символы, чтобы создать свой API. По крайней мере, вам нужно украсить функции, которые вы экспортируете __declspec(dllexport)
, Тем не менее, я рекомендую вам экспортировать C API, то есть extern "C"
функции с C
-совместимые аргументы. Итак, функции, которые вы экспортируете, должны начинаться с extern "C" __declspec(dllexport)
,
Как только вы это сделаете, вы можете динамически загрузить вашу DLL следующим образом:
const char* dllname = "myfile.dll";
h = LoadLibrary(dllname);
if( h == nullptr )
{
/*handle error*/
}
using myfunc_type = bool (*)(int x, double y); //example
auto myfunc = reinterpret_cast<myfunc_type>(GetProcAddress(h, "myfunc"));
//......
myfunc(x,y); //call the imported function
Это решение требует больше работы, чем статическая загрузка с /delayload
показано Джерри Коффином, но у него есть преимущество: если DLL требуется, но не найдена, вы можете дать пользователям свое собственное сообщение об ошибке вместо того, чтобы полагаться на сообщение, поступающее из Windows (что часто неприемлемо для людей, не являющихся техническими специалистами). Вы также можете включить проверку версии API в собственное сообщение об ошибке в API.
Изменить: пример кода будет работать, если вы измените его следующим образом
extern "C" __declspec(dllexport) int main(int argc, char *argv[]){...}
typedef int (* LPFNDLLFUNC1)(int, char **);
Вам не нужно GetProcAddress (...)
сделать это, хотя этот подход (вариант № 2) проще, когда вы понимаете, как компилятор генерирует имена символов.
Опция 1
DllMain порождает основную тему
Никогда не делайте ничего сложного внутри DllMain, вы можете заблокировать свое программное обеспечение.
У DLL есть своя собственная точка входа (и точка выхода, и точка подключения потока... это очень занятая функция). Просто звоню LoadLibrary (...)
на вашей DLL вызывает как минимум один вызов DllMain (...)
для прикрепления процесса.
BOOL
APIENTRY
DllMain ( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved )
Вы можете на самом деле лечить ul_reason_for_call == DLL_PROCESS_ATTACH
как мандат на выполнение DllMain, как если бы это была основная функция вашей программы.
Теперь вы ни в коем случае не должны запускать цикл программы здесь... в любое время DllMain
запускает он содержит очень важную блокировку операционной системы (DLL Loader), и вам нужно снять ее, вернувшись для нормальной работы программы.
Это означает, что если вы хотите использовать DllMain
как точка входа вашей программы, она должна порождать поток и ваш оригинал main
метод не должен возвращаться, пока этот поток не завершится...
Вариант № 2
DLL экспортирует
main
Функция.Примите во внимание соглашение о вызовах, компилятор переименует символы для вас и сделает поиск функций в DLL с
GetProcAddress
менее чем интуитивно.
В вашей DLL, экспорт main
:
__declspec (dllexport)
int
__cdecl main (int argc, char *argv [])
{
printf ("foobar");
return 0;
}
В вашей программе импортируйте main
из DLL:
// Need a typedef for the function you are going to get from the DLL
typedef int (__cdecl *main_pfn)(int argc, char *argv[]);
int main (int argc, char *argv[])
{
HMODULE hModMyDLL = LoadLibraryA ("MyDll.dll");
if (hModMyDLL != 0) {
//
// The preceding underscore deals with automatic decorations
// the compiler added to the __cdecl function name.
//
// It is possible to do away with this completely if you use a .def
// file to assign export names and ordinals manually, but then you
// lose the ability to tell a function's calling convention by its
// name alone.
//
main_pfn MyMain = (main_pfn)
GetProcAddress (hModMyDLL, "_main");
// Call the main function in your DLL and return when it does
if (MyMain != nullptr)
return MyMain (argc, argv);
}
return -1;
}
Оба подхода имеют свои достоинства.
Нерест из DllMain
избегает вообще ничего знать о том, как реализована DLL, которую вы хотите загрузить, но это также требует от вас main
функцию никогда не вернуть - DLL вызовет ExitProcess (...)
,
Экспорт функций и последующий импорт их по названию позволяет избежать пушистой стрельбы вокруг загрузчика Windows DLL. Однако, если вы не используете .def
файл для явного имени экспортируемых символов, компилятор собирается добавить украшения, такие как _...
(__cdecl) или ...@n
(__stdcall) к именам, и вы должны изучить эти соглашения, чтобы сделать что-нибудь полезное с GetProcAddress
,
Вам не нужно использовать LoadLibrary
а также GetProcAddress
чтобы вызвать функциональность в DLL.
Чаще вы будете создавать свои DLL, каждая со своей точкой входа. В настоящий момент предположим, что вы хотите проанализировать командную строку, выбрать DLL и вызвать ее точку входа без аргументов. Вы получите что-то вроде этого:
void DLL_a();
void DLL_b();
void DLL_c();
int main(int argc, char **argv) {
// we'll assume DLL_a is the default:
if (argc < 2)
DLL_a();
// For now, we'll do a *really* trivial version of parsing the command line
// to choose the right DLL:
if (argv[1][0] == 'A')
DLL_a();
else if (argv[1]][0] == 'B')
DLL_b();
else if (argv[1][0] == 'C')
DLL_c();
else {
std::cerr << "Unrecognized argument\n";
return 1;
}
}
Когда вы связываете свой основной, вы будете указывать .lib
соответствующие каждой DLL, и вы, вероятно, захотите указать /delayload
флаг для компоновщика. Это означает, что DLL не будет загружена, пока функция в DLL не будет фактически вызвана. Если, например, вы хотите распространить версию вашей программы с ограниченной функциональностью, которая включает в себя только DLL A, она все равно сможет работать (без DLL B или C, присутствующей в системе пользователя), пока не будет функции из DLL B или C когда-либо называется. Если вы не укажете /delayload
загрузчик попытается сопоставить все библиотеки DLL с оперативной памятью при запуске программы, запустить их DllMain
чтобы инициализировать их для использования, сделайте то же самое рекурсивно для всех библиотек DLL, от которых они зависят, и т. д.
/delayload
имеет еще одно преимущество: он вообще не отображает другие библиотеки на адреса, если они никогда не используются. Похоже, что любой вызов будет использовать только одну DLL, так что это, вероятно, победа в вашем случае.