Рассмотрим программную архитектуру микроядра для приложения.
У меня есть ядро и компонент.
Компонент представляет собой DLL, которая загружается ядром во время выполнения с использованием LoadLibrary
API в Windows; и, конечно же, экспортируемые функции могут быть вызваны с помощью GetProcAddress
,
Теперь компонент должен отправлять сообщения в ядро. Другими словами, компонент, который теперь является загруженной DLL, должен вызывать функции из ядра. Каков правильный механизм?
Это должно работать, смотрите здесь: https://stackoverflow.com/a/30475042/1274747
Для MSVC вы бы в основном использовали __declspec(dllexport)
в .exe. Компилятор / компоновщик генерирует библиотеку импорта для .exe, которая затем может быть связана с DLL, затем DLL будет использовать символы из .exe.
Другой вариант — решить эту проблему путем «инверсии зависимостей» — .exe не будет экспортировать символы, но предоставит (чисто виртуальный) интерфейс, который будет реализован внутри .exe и передан (через ссылку или указатель на интерфейс. ) в DLL после загрузки. Затем DLL может вызывать методы интерфейса, предоставленного внутри .exe. Но на самом деле, как вы говорите о микроядре, это зависит от того, будут ли накладные расходы на виртуальный вызов приемлемыми для вас (хотя при экспорте функции из .exe метод вызывается AFAIK также через указатель на функцию, поэтому я не ожидаю любая существенная разница).
РЕДАКТИРОВАТЬ
Я только что создал пример, который работает для меня (просто быстрый код, без особой полировки, обычно используются заголовки и т. Д.):
Файл «mydll.cpp»:
// resolved against the executable
extern "C" __declspec(dllimport)
int __stdcall getSum(int a, int b);extern "C" __declspec(dllexport)
int __stdcall callSum(int a, int b)
{
return getSum(a, b);
}
Файл «myexe.cpp»:
#include <iostream>
using namespace std;
#include <windows.h>
// export from the .exe
extern "C" __declspec(dllexport)
int __stdcall getSum(int a, int b)
{
return a + b;
}typedef int(__stdcall * callSumFn)(int a, int b);
int main()
{
HMODULE hLibrary = LoadLibrary(TEXT("MyDll.dll"));
if (!hLibrary)
{
cerr << "Failed to load library" << endl;
return 1;
}
callSumFn callSum = (callSumFn)GetProcAddress(hLibrary, "_callSum@8");
if (!callSum)
{
cerr << "Failed to get function address" << endl;
FreeLibrary(hLibrary);
return 1;
}
cout << "callSum(3, 4) = " << callSum(3, 4) << endl;
FreeLibrary(hLibrary);
return 0;
}
DLL связана с «MyExe.lib», который создается при сборке EXE. main()
вызывает callSum()
функция из DLL, которая в свою очередь вызывает getSum()
предоставлено EXE.
При этом я все же предпочел бы использовать «инверсию зависимостей» и передавать интерфейс в DLL — для меня это кажется более чистым, а также более гибким (например, управление версиями по наследованию интерфейса и т. Д.).
РЕДАКТИРОВАТЬ № 2
Что касается техники инверсии зависимостей, это может быть, например, что-то вроде этого:
Файл ikernel.hpp (предоставляется исполняемым файлом ядра, а не DLL):
#ifndef IKERNEL_HPP
#define IKERNEL_HPP
class IKernel
{
protected:
// or public virtual, but then there are differences between different compilers
~IKernel() {}
public:
virtual int someKernelFunc() = 0;
virtual int someOtherKernelFunc(int x) = 0;
};
#endif
Файл «mydll.cpp»:
#include "ikernel.hpp"
// if passed the class by pointer, can be extern "C", i.e. loadable by LoadLibrary/GetProcAddress
extern "C" __declspec(dllexport)
int __stdcall callOperation(IKernel *kernel, int x)
{
return kernel->someKernelFunc() + kernel->someOtherKernelFunc(x);
}
Файл «myexe.cpp»:
#include "ikernel.hpp"
#include <iostream>
using namespace std;
#include <windows.h>
// the actual kernel definition
class KernelImpl: public IKernel
{
public:
virtual ~KernelImpl() {}
virtual int someKernelFunc()
{
return 10;
}
virtual int someOtherKernelFunc(int x)
{
return x + 20;
}
};
typedef int(__stdcall * callOperationFn)(IKernel *kernel, int x);
int main()
{
HMODULE hLibrary = LoadLibrary(TEXT("ReverseDll.dll"));
if (!hLibrary)
{
cerr << "Failed to load library" << endl;
return 1;
}
callOperationFn callOperation = (callOperationFn)GetProcAddress(hLibrary, "_callOperation@8");
if (!callOperation)
{
cerr << "Failed to get function address" << endl;
FreeLibrary(hLibrary);
return 1;
}
KernelImpl kernel;
cout << "callOperation(kernel, 5) = " << callOperation(&kernel, 5) << endl;
FreeLibrary(hLibrary);
return 0;
}
Как сказано, это более гибко и ИМХО проще в обслуживании; ядро может предоставлять разные обратные вызовы для разных вызовов DLL. При необходимости DLL может также обеспечить реализацию некоторого интерфейса в качестве спецификатора ядром, который сначала будет извлечен из DLL, и ядро вызовет функции для него.
Другое удобство заключается в том, что DLL не нужно связывать с какой-либо библиотекой «ядра» (чистый виртуальный интерфейс не нужно экспортировать).
Обычно это работает даже на разных компиляторах (то есть исполняемый файл, скомпилированный другим компилятором, отличным от DLL, например, MSVC и GCC) — при условии, что реализация виртуальной таблицы одинакова. Это не обязательно, но на самом деле это обязательное условие для работы COM (компиляторы, обеспечивающие различную реализацию полиморфизма, не могут использовать вызовы Microsoft COM).
Но особенно в этом случае вы обязательно должны убедиться, что объекты, размещенные в DLL, не освобождены в EXE и наоборот (они могут использовать разные кучи). Если это необходимо, интерфейс должен предоставлять чисто виртуальный метод destroy (), который обеспечивает полиморфный вызов «delete this» в правильном контексте памяти. Но это может быть проблемой даже при непосредственном вызове функций (в общем случае не следует освобождать память () malloc () — ed на другой стороне). Также исключения C ++ не должны проходить границу API.
Подумайте о том, чтобы сделать свой дизайн наоборот: то есть «ядро» сделано в виде DLL и загружается приложением вашего компонента. Поскольку ядро предоставляет сервисы компоненту, а не наоборот, это имеет больше смысла.