Managed plugin для Native программы (на примере Winamp General Purpose plugin)
Ожидает приглашения
Не так давно на Хабре был цикл статей, посвящённый вызову managed функций из unmanaged модулей. Там вскользь упомянули про поддержку прямого подключения managed dll к unmanaged модулю. Однако, для этого необходимо либо править дизассемблированный код своей библиотеки, либо использовать сторонние решения.
Однажды на моей работе появилась необходимость написать плагин к native программе. Вот там-то и понадобилась эта возможность — подключать managed dll к unmanaged модулю.
В этой статье я хочу рассмотреть пример простого плагина, написанного на C#, для проигрывателя Winamp.
Давайте вначале разберём, что же Winamp ждёт от dll, которая хочет быть General Purpose плагином.
Для этого скачаем и установим Winamp SDK.
После установки изучим файл Winamp SDK\Winamp\GEN.H. Если кратко, то Winamp вызывает процедуру
Однажды на моей работе появилась необходимость написать плагин к native программе. Вот там-то и понадобилась эта возможность — подключать managed dll к unmanaged модулю.
В этой статье я хочу рассмотреть пример простого плагина, написанного на C#, для проигрывателя Winamp.
Введение
Давайте вначале разберём, что же Winamp ждёт от dll, которая хочет быть General Purpose плагином.
Для этого скачаем и установим Winamp SDK.
После установки изучим файл Winamp SDK\Winamp\GEN.H. Если кратко, то Winamp вызывает процедуру
winampGetGeneralPurposePlugin
от которой на выходе ждёт ссылку на следующую структуру:typedef struct {
int version;
char *description;
int (*init)();
void (*config)();
void (*quit)();
HWND hwndParent;
HINSTANCE hDllInstance;
} winampGeneralPurposePlugin;
* This source code was highlighted with Source Code Highlighter.
В свою очередь, в этой структуре содержатся ссылки на три метода: int init()
, void config()
и void quit()
. По названию можно догадаться, когда Winaмp будет их вызывать.
Итак, нам нужно реализовать всё это на C#.
Реализация
Описание структур и типов
Для начала надо описать структуру и делегаты, присутствующий в этой структуре.
namespace gen_managed
{
internal delegate int InitHandler();
internal delegate void VoidHandler();
[StructLayout(LayoutKind.Sequential, Size = 1)]
internal class WinampGenPlugnInfo
{
public Int32 version;
[MarshalAs(UnmanagedType.LPStr)]
public string description;
public InitHandler init;
public VoidHandler config;
public VoidHandler quit;
public IntPtr hwndParent;
public IntPtr hDllInstance;
}
}
* This source code was highlighted with Source Code Highlighter.
В приведённом фрагменте описываются два делегата: InitHandler
и VoidHandler
. Соответственно, первый делегат определяет сигнатуру метода Init
, второй делегат определяет сигнатуры методов Quit
и Config
.
Далее следует определение самой структуры.
Хорошо, структура и делегаты описаны, теперь самое интересное, как сделать метод, который будет виден native модулю?
Реализация методов и экспорт
Для начала напишем этот самый метод как обычно.
private static WinampGenPlugnInfo _infoPlaceholder;
private static IntPtr _infoPtr;
[ExportDllAttribute.ExportDll("winampGetGeneralPurposePlugin")]
public static IntPtr MainEntry()
{
_infoPlaceholder = new WinampGenPlugnInfo()
{
config = new VoidHandler(config),
description = "Managed Winamp demo plugin",
hDllInstance = IntPtr.Zero,
hwndParent = IntPtr.Zero,
init = new InitHandler(init),
quit = new VoidHandler(quit),
version = 0x10
};
_infoPtr = Marshal.AllocHGlobal(Marshal.SizeOf(_infoPlaceholder));
Marshal.StructureToPtr(_infoPlaceholder, _infoPtr, true);
return _infoPtr;
}
* This source code was highlighted with Source Code Highlighter.
Немного пояснений. _infoPlaceholder
сделан полем класса для того, чтобы сборщик мусора не уничтожил делегаты, объявляемые в нём. Описание методов init
, quit
и config
будет дано чуть попозже. 0x10 — волшебное число версии этой структуры, определяемое всё в том же Winamp SDK\Winamp\GEN.H.
После заполнения полей структуры мы получаем её unmanaged handle, который запоминаем в поле класса и возвращаем Winamp'у.
Теперь непосредственно об экспорте. Если не очень хочется углубляться в ассемблер самостоятельно, то можно скачать на CodeProject готовый класс, обрабатывающий дизассемблированный код за нас.
Итак, для того, чтобы объявить метод видимым для native кода вначале необходимо сбилдить DLLExport. При этом необходимо убедиться, что в файле app.config проекта DllExport правильно прописаны пути к ildasm.exe и ilasm.exe на вашей машине.
После этого надо подключить ссылку на ExportDllAttribute.dll к проекту с плагином для Winamp. После чего над объявлением метода MainEntry()
поместить аттрибут [ExportDllAttribute.ExportDll("winampGetGeneralPurposePlugin")]
. Таким образом мы пока что всего лишь пометили метод, который хотим экспортировать. И задали для него Native название.
Чтобы действительно его экспортировать необходимо запустить программу ExportDll.exe из проекта ExportDll указав ей полный путь к dll файлу с библиотекой. Если заранее переписать ExportDll.exe и ExportDllAttribute.dll в папку ExportDll нашего проекта, то можно настроить PostBuild events следующим образом: $(ProjectDir)ExportDll\ExportDll.exe $(TargetPath).
Если все пути прописаны правильно, то ExportDll.exe дизассемблирует библиотеку, удалит из неё ссылку на ExportDllAttribute, произведёт необходимые изменения для объявления native export и соберёт её обратно.
Итак, у нас есть C# библиотека, которая экспортирует одну из своих функций для native приложений.
Давайте вернёмся к оставленным на потом функциям init
, config
и quit
.
Собственно, вот они:
private static Int32 init()
{
WinampGenPlugnInfo infoCurrent = new WinampGenPlugnInfo();
Marshal.PtrToStructure(_infoPtr, infoCurrent);
_winampHwnd = infoCurrent.hwndParent;
MessageBox.Show("We've just been loaded into Winamp. Winamp HWND = " + _winampHwnd.ToString());
return 0;
}
private static void config()
{
MessageBox.Show("Winamp managed plugin demo", "Demo plugin", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
private static void quit()
{
}
* This source code was highlighted with Source Code Highlighter.
Функция init
переводит ссылку на структуру с информацией, которую мы отдавали Winamp'у обратно в эту структуру. А Winamp за это время успел записать туда немного полезных сведений. Например, свой HWND. Его-то мы сохраним и выведем в MessageBox. Если всё хорошо, функция init
должна возвратить 0.
Функция config
сделана для демонстрации (если пройти в Winamp -> Preferences -> Plugins -> General Purpose, выбрать наш плагин под названием Managed Winamp demo plugin и нажать Configure Selected Plugin, то мы должны увидеть сообщение, которое выводится в нашей функции config).
Заключение
В таком виде, конечно же, этот плагин ничем не интересен. Однако, это только начало. Для себя я уже написал плагин, который расширяет Winamp новыми возможностями таскбара Windows 7. Такими, например, как кнопки во всплывающем окне предпросмотра. Если эта статья вызовет интерес, то продолжу рассказ про то, как написать такой плагин. В нём, кстати, тот самый HWND, что был сохранён в процедуре init
, нам очень понадобится :)
Я бы выложил исходники из этой статьи, да некуда.
_________
Текст подготовлен в ХабраРедакторе