Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Lab3.docx
Скачиваний:
0
Добавлен:
01.05.2025
Размер:
3.22 Mб
Скачать

Создание класса, отвечающего за взаимодействие с плагинами

Мы подобрались к самому интересному. Сейчас мы напишем класс, который будет осуществлять взаимодействие между нашим сервером и плагинами. Добавим файл класса в проект. Так как это будет не COM-класс, то добавим его в самую внешнюю директорию проекта.

Добавим пару необходимых нам неймспейсов.

using System.Runtime.InteropServices;

using System.Reflection;

Интероп нам понадобится для взаимодействия с COM и импорта процедур из обычных (не .NET-овых) виндовых библиотек. Рефлекшен нам нужен будет для работой с плагинами, написанными на технологии .NET и не содержащими в себе COM-сервера.

Сначала немного подготовительной работы. Я решил быть няшей и создать здесь набор исключений, ведь возвращать ошибки через их код уже который год не модно, а мы же с вами модные программисты, не так ли? Тогда добавим в неймспейс следующие классы:

#region Исключения, связанные с плагинами

class CannotLoadDllAsPluginException : Exception

{

string dllPath;

public CannotLoadDllAsPluginException(string dllPath)

{ this.dllPath = dllPath; }

public string DllPath

{ get { return dllPath; } }

public override string Message

{ get { return "Невозможно загрузить dll как плагин сервера СПО: " + dllPath; } }

}

class InternalPluginErrorException : Exception

{

int errorCode;

public InternalPluginErrorException(int errorCode)

{ this.errorCode = errorCode; }

public int ErrorCode

{ get { return errorCode; } }

}

class PluginNotConnectedYetException : Exception

{

public PluginNotConnectedYetException()

{ }

}

#endregion

Лепота. Переходим непосредственно к работе с плагинами. Пропишем, что класс у нас наследуется от IDisposable. В методе Dispose (что вызывается деконструктором, если кто не знал) будем насильно отключать плагин от приложения в том случае, если он подключен.

class Plugin : IDisposable

Добавим в класс строку, которая будет содержать путь к библиотеке.

/// <summary> Путь к плагину. </summary>

string path;

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

#region Набор булевых признаков класса.

/// <summary> Показывает, написан ли плагин в управляемом коде, т.е. на семействе языков платформы .NET. </summary>

bool isManaged = false;

/// <summary> Показывает, содержит ли плагин COM-сервер. </summary>

bool isCOMServer = false;

/// <summary> Показывает, подключен ли плагин. </summary>

bool isConnected = false;

#endregion

Далее импортируем процедуры из библиотеки kernel32.dll, необходимые нам для работы с библиотеками, написанными на языках, не принадлежащих к семейству .NET, то есть в неуправляемом (unmanaged) коде. В принципе, сами эти импортируемые процедуры тоже написаны в неуправляемом коде, за подробностями импорта – сюда.

#region Импорт из kernel32.dll для взаимодействия с неуправляемыми библиотеками.

/// <summary>

/// Загружает библиотеку с заданным путём в память приложения и возвращает её хэндл.

/// </summary>

/// <param name="dllToLoad">Путь к библиотеке.</param>

/// <returns>Хэндл загруженной библиотеки.</returns>

[DllImport("kernel32.dll")]

private static extern IntPtr LoadLibrary(string dllToLoad);

/// <summary>

/// Получает указатель процедуры библиотеки по имени процедуры и хэндлу библиотеки.

/// </summary>

/// <param name="hModule">Хэндл библиотеки.</param>

/// <param name="procedureName">Имя процедуры.</param>

/// <returns>Возвращает указатель на процедуру.</returns>

[DllImport("kernel32.dll")]

private static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);

/// <summary>

/// Закрывает библиотеку и выгружает её из памяти приложения.

/// </summary>

/// <param name="hModule">Хэндл библиотеки.</param>

/// <returns>Возвращает булевый признак успешного завершения операции</returns>

[DllImport("kernel32.dll")]

private static extern bool FreeLibrary(IntPtr hModule);

#endregion

Если вы новенький в шарпе то вы могли бы заметить директиву #region. Её мы используем, чтобы обозначать блоки кода, которые можно сворачивать и разворачивать в редакторе.

Затем добавим делегаты и методы, которые мы будем использовать для работы с плагинами в неуправляемом коде:

#region Делегаты для привзяки к неуправляемым процедурам.

private delegate string GetCOMClassNameDelegate();

private delegate int ConnectDelegate(object iZigzagControl, ref object iPluginConnectionInfo);

private delegate int DisconnectDelegate();

private delegate int PerformActionDelegate(int actionId);

/// <summary>

/// Вызов процедуры получения имени COM-класса плагина, содержащейся в unmanaged-библиотеке.

/// </summary>

private GetCOMClassNameDelegate GetCOMClassNameUnmanaged;

/// <summary>

/// Вызов процудуры подключение к плагину, содержащейся в unmanaged-библиотеке.

/// </summary>

private ConnectDelegate ConnectUnmanaged;

/// <summary>

/// Вызов процудуры отключения от плагина, содержащейся в unmanaged-библиотеке.

/// </summary>

private DisconnectDelegate DisconnectUnmanaged;

/// <summary>

/// Вызов процудуры выполнения действия плагина с заданным идентификатором, содержащейся в unmanaged-библиотеке.

/// </summary>

private PerformActionDelegate PerformActionUnmanaged;

#endregion

Каждый делегат задаёт набор параметров и возвращаемое значение, то есть сигнатуру функции. Если вы шарите в C++, то можно сказать, что делегат – это типобезопасный аналог указателя на функцию. Таким образом, объявление private GetCOMClassNameDelegate GetCOMClassNameUnmanaged фактически создаёт функцию с параметрами и возвращаемым значением, указанным в объявлении делегата. В нашем случае функция GetCOMClassNameUnmanaged не будет иметь параметров и будет возвращать строку. Следовательно, подобное объявление какбы создаёт переменную со значением функции. Я хочу узнать больше о делегатах и потому перехожу по этой ссылке.

Вызывать мы будем в основном GetCOMClassNameUnmanaged. Работу с unmanaged-плагинами без COM-серверов я совсем не тестировал.

Зададим ряд переменных для вызова методов из управляемых библиотек, не содержащих COM-сервера. Переменные имеют тип MethodInfo, объявленный в System.Reflection, содержащий в себе метаданные о методе. Из этого типа можно вызывать (invoke) методы с заданным набором аргументов. Сейчас это несколько устаревшая техника и все нормальные люди, использующие Framework 4 и выше, применяют для этого новый тип dynamic, но пока будем ортодоксальны.

#region Переменные для вызова методов из управляемых библиотек, не содержащих COM-сервера.

//Сделаны для поддержки Framework 3.5. В Framework 4 можно использовать тип dynamic и не обламываться.

private MethodInfo connectMethod, disconnectMethod, performActionMethod, getComClassNameMethod;

#endregion

Зададим список приватных полей для хранения информации о плагине.

#region Информация о плагине, полученная из его IPluginConnectionInfo

/// <summary> Информация о плагине, полученная при подключении. </summary>

IPluginConnectionInfo pluginConnectionInfo;

/// <summary> Имя плагина. </summary>

private string pluginName;

/// <summary> Описание плагина. </summary>

private string pluginDescription;

/// <summary> Список действий плагина. </summary>

private List<IPluginAction> actions = new List<IPluginAction>();

#endregion

И, наконец, ещё тройка переменных.

/// <summary> Содержит COM-объект с плагином. Используется как для плагинов в управляемом коде, так и для плагинов в коде неуправляемом. </summary>

private IPlugin COMPlugin;

/// <summary> Хэндл библиотеки с неуправляемым плагином. </summary>

private IntPtr unmanagedPluginDllHandle;

/// <summary> Экземлпяр класса ZigzagControl. </summary>

private IZigzagControl iZigzagControl;

Думаю, с ними всё ясно по описанию. Добавим публичные поля для доступа к данным о плагине извне.

#region Поля класса

/// <summary> Получает имя плагина. </summary>

public string Name

{

get

{ return pluginName; }

}

/// <summary> Получает описание плагина. </summary>

public string Description

{

get

{ return pluginDescription; }

}

/// <summary> Получает список действий, которые плагин может выполнить. </summary>

public List<IPluginAction> ActionsList

{

get

{ return new List<IPluginAction>(actions.ToArray()); }

}

/// <summary> Получает состояние подключенности плагина. </summary>

public bool IsConnected

{

get

{ return isConnected; }

}

#endregion

Отлично, есть всё, что нужно, чтобы переходить к реализации функционала. Закодим конструктор класса.

/// <summary>

/// Конструктор класса Plugin.

/// </summary>

/// <param name="dllPath">Путь к библиотеке, содержащей плагин.</param>

public Plugin(string dllPath)

Сначала сохраним путь и проверим наличие файла.

path = dllPath;

System.IO.FileInfo fi = new System.IO.FileInfo(dllPath);

if (!fi.Exists)

throw new System.IO.FileNotFoundException("dll-файл плагина не найден.", dllPath);

Попытаемся найти в библиотеке сборку. Если библиотека написана в управляемом коде, то метод найдёт сборку и перейдёт на return, иначе программа выбросит исключение. Дальнейшие комментарии – прямо по коду.

try

{

// Грузим либу.

Assembly pluginAssembly = Assembly.LoadFile(path);

isManaged = true;

// Будем считать, что управляемая библиотека должна содержать тип ZigzagPlugin

// Если библиотека содержит внутренний COM-сервер, то этот тип содержит метод string GetCOMClassName(), который возвращает имя COM-класса при его вызове.

// В противном случае он содержит все те методы, которые объявлены в IPlugin.

Type spoServerPluginType = pluginAssembly.GetType("ZigzagPlugin"); //Получим тип через reflection.

getComClassNameMethod = spoServerPluginType.GetMethod("GetCOMClassName", System.Type.EmptyTypes); //Пошмонаем его на наличие нужных нам методов.

// В GetMethod передаётся имя искомой функции и массив типов её аргументов

if (getComClassNameMethod != null)

{

// Если метод, возвращающий имя COM-класса найден, стало быть либа содержит COM-сервер и остальные три метода искать нет смысла -- получим их через IPlugin в тот момент, когда потребуются, а не сейчас.

isCOMServer = true;

return;

}

// В противном случае -- поищем остальные методы.

connectMethod = spoServerPluginType.GetMethod("Connect", new Type[] { typeof(object), typeof(object).MakeByRefType() });

disconnectMethod = spoServerPluginType.GetMethod("Disconnect", System.Type.EmptyTypes);

performActionMethod = spoServerPluginType.GetMethod("PerformAction", new Type[] { typeof(int) });

if ((connectMethod != null) && (disconnectMethod != null) &&

(performActionMethod != null))

return; // Все необходимые методы нашлись, работа конструктора завершена.

}

catch (Exception)

{ }

В случае, если библиотека написана в неуправляемом коде, либо как-то криво написана в управляемом (в том, например, смысле криво, что не хватает каких-то необходимых для работы плагина методов, если методы названы неприавильно, имеют другой набор параметров, etc), программа перейдёт дальше этого блока try-catch. Работа с unmanaged-библиотекой аналогичная.

isManaged = false;

try

{

//Здесь логика работы аналогичная, только уже не через reflection, а через interop.

//Грузим либу.

unmanagedPluginDllHandle = LoadLibrary(path);

//Ищем функции.

IntPtr getComClassNameMethodPtr = GetProcAddress(unmanagedPluginDllHandle, "GetCOMClassName");

if (getComClassNameMethodPtr != IntPtr.Zero)

{

// Если метод, возвращающий имя COM-класса найден, стало быть либа содержит COM-сервер и остальные

// три метода искать нет смысла -- получим их через IPlugin в тот момент, когда потребуются, а не сейчас.

isCOMServer = true;

GetCOMClassNameUnmanaged = (GetCOMClassNameDelegate)Marshal.GetDelegateForFunctionPointer(getComClassNameMethodPtr, typeof(GetCOMClassNameDelegate));

return;

}

// Это не COM, ищем другие методы.

IntPtr connectMethodPtr = GetProcAddress(unmanagedPluginDllHandle, "Connect");

IntPtr disconnectMethodPtr = GetProcAddress(unmanagedPluginDllHandle, "Disconnect");

IntPtr performActionPtr = GetProcAddress(unmanagedPluginDllHandle, "PerformAction");

if ((connectMethodPtr != IntPtr.Zero) && (disconnectMethodPtr != IntPtr.Zero) && (performActionPtr != IntPtr.Zero))

{

#region Работа этой секции кода не тестировалась

ConnectUnmanaged = (ConnectDelegate)Marshal.GetDelegateForFunctionPointer(connectMethodPtr, typeof(ConnectDelegate));

DisconnectUnmanaged = (DisconnectDelegate)Marshal.GetDelegateForFunctionPointer(disconnectMethodPtr, typeof(DisconnectDelegate));

PerformActionUnmanaged = (PerformActionDelegate)Marshal.GetDelegateForFunctionPointer(performActionPtr, typeof(PerformActionDelegate));

return; // Все необходимые методы нашлись, работа конструктора завершена.

#endregion

}

}

catch (Exception)

{ }

Если и здесь чего-то не получилось, то это может значить только то, что плагин нам подсунули палёный – выбросим исключение.

throw new CannotLoadDllAsPluginException(path);

Конструктор завершен. Добавим небольшой вспомогательный метод, который заполняет поля класса информацией из структуры IPluginConnectionInfo.

/// <summary>

/// Заполняет поля с информацией о плагине значениями из IPluginConnectionInfo

/// </summary>

/// <param name="info"></param>

private void FillInfo(IPluginConnectionInfo info)

{

pluginName = info.PluginName;

pluginDescription = info.PluginDescription;

IPluginAction[] pluginActions = info.AvailableActions;

actions = new List<IPluginAction>(pluginActions);

}

Теперь напишем метод для подключения плагина к приложению.

/// <summary>

/// Подключает плагин к приложению.

/// </summary>

/// <param name="iZigzagControl">Экземпляр класса ZigzagControl.</param>

/// <returns>Возвращает 0, если плагин подключился успешно, иначе возвращает код ошибки.</returns>

public int Connect(IZigzagControl iZigzagControl)

Сделаем сначала вот такую заготовку, чтобы нельзя было подключить один и тот же плагин дважды.

this.iZigzagControl = iZigzagControl;

if (!isConnected)

{

}

else

return 0;

Далее в этом методе всё будет писаться внутри этой конструкции if. Инициализируем некоторые переменные.

int result = 0;

object connectionInfo = null;

pluginConnectionInfo = null;

Теперь обработаем тот вариант, в котором наш плагин содержит внутренний COM-сервер. Пояснения в комментариях к коду.

if (isCOMServer)

{

// Преимуществом технологии COM является то, что можно единообразно обращаться как с managed, так и c unmanaged-кодом. Различия будут лишь в способе вызова метода получения имени COM-класса

// Сначала получим имя класса.

string typeName;

if (isManaged)

typeName = (string)getComClassNameMethod.Invoke(null, null);

else

typeName = GetCOMClassNameUnmanaged();

if ((typeName == null) || (typeName == ""))

return -404;

Type type = Type.GetTypeFromProgID(typeName); // Получим экземпляр класса по его имени.

object plugin = Activator.CreateInstance(type); // Создадим его. Будет использован конструктор по умолчанию.

COMPlugin = plugin as IPlugin; // Прикастим к нашему интерфейсу.

result = COMPlugin.Connect(iZigzagControl, ref connectionInfo); // Подключим.

pluginConnectionInfo = (IPluginConnectionInfo)connectionInfo; // Получим информацию о нём.

if ((pluginConnectionInfo == null) || (result != 0))

throw new InternalPluginErrorException(result);

FillInfo(pluginConnectionInfo); // Заполним поля нашего класса полученной информации.

isConnected = true;

return result; // Готово.

}

В случае, если плагин не содержит COM-сервера, обработаем варианты managed- и unmanaged-плагина.

if (isManaged)

{

object[] args = new object[] { iZigzagControl, connectionInfo }; // Создаем массив с аргументами.

result = (int)connectMethod.Invoke(null, args); // Вызываем исполнение метода.

pluginConnectionInfo = (IPluginConnectionInfo)args[1]; // Если метод выполнился без ошибок, то во втором аргументе будет инфа о плагине.

if ((pluginConnectionInfo == null) || (result != 0))

throw new InternalPluginErrorException(result);

FillInfo(pluginConnectionInfo); //Заполняем поля класса

isConnected = true;

return result;

}

else

{

#region Работа этой секции кода не тестировалась

result = ConnectUnmanaged(iZigzagControl, ref connectionInfo);

pluginConnectionInfo = (IPluginConnectionInfo)connectionInfo;

if ((pluginConnectionInfo == null) || (result != 0))

throw new InternalPluginErrorException(result);

FillInfo(pluginConnectionInfo);

isConnected = true;

return result;

#endregion

}

Метод подключения закончен. Переходим к методу отключения.

/// <summary>

/// Отключает плагин от приложения.

/// </summary>

/// <returns>Возвращает 0, если плагин подключился успешно, иначе возвращает код ошибки.</returns>

public int Disconnect()

Сначала обернём весь код большим try-catch и if-ом.

try

{

if (isConnected)

{

}

else

return 0;

}

catch (Exception)

{ return 1; }

Как-то коды ошибок не унифицированы и в разнобой, ну да ладно, если вам будет не лень – унифицируете. Я сейчас слишком сонный для работы с числами. А далее план такой же, как и в предыдущем методе. Сначала прорабатываем вариант с COM-плагином.

int result = 0;

if (isCOMServer)

{

result = COMPlugin.Disconnect();

if (!isManaged)

FreeLibrary(unmanagedPluginDllHandle);

if (result != 0)

throw new InternalPluginErrorException(result);

isConnected = false;

return result;

}

А потом, в случае, если это не COM, отрабатываем отдельно для managed и unmanaged.

if (isManaged)

{

result = (int)disconnectMethod.Invoke(null, null);

if (result != 0)

throw new InternalPluginErrorException(result);

isConnected = false;

return result;

}

else

{

#region Работа этой секции кода не тестировалась

result = DisconnectUnmanaged();

if (result != 0)

throw new InternalPluginErrorException(result);

FreeLibrary(unmanagedPluginDllHandle);

isConnected = false;

return result;

#endregion

}

Метод отключения тоже готов. Далее – метод вызова действия плагина.

/// <summary>

/// Выполняет действие, доступное из плагина.

/// </summary>

/// <param name="actionId">Идентификатор действия.</param>

/// <returns>Возвращает 0, если плагин подключился успешно, иначе возвращает код ошибки.</returns>

public int PerformAction(int actionId)

Тут всё аналогично и просто, потому просто приведу здесь текст метода:

if (isConnected)

{

int result = 0;

if (isCOMServer)

{

// В случае, если плагин имеет COM-сервер, просто запрашиваем выполнение действия и контроллируем результат.

result = COMPlugin.PerformAction(actionId);

if (result != 0)

throw new InternalPluginErrorException(result);

return result;

}

//Иначе, в зависимости от управляемости кода, запрашиваем выполнение его другими путями.

if (isManaged)

{

try

{

result = (int)performActionMethod.Invoke(null, new object[] { actionId });

if (result != 0)

throw new InternalPluginErrorException(result);

}

catch (TargetInvocationException)

{ result = -100; }

return result;

}

else

{

#region Работа этой секции кода не тестировалась

result = PerformActionUnmanaged(actionId);

if (result != 0)

throw new InternalPluginErrorException(result);

return result;

#endregion

}

}

else

throw new PluginNotConnectedYetException();

И осталось только реализовать IDisposable. Напоминаю, метод Dispose вызывается сборщиком мусора тогда, когда экземпляр готовится к уничтожению, когда на него уже не осталось ссылок. Здесь мы будем отключать плагин в том случае, если он подключен.

#region Члены IDisposable

/// <summary>

/// Деструктор класса, который будет автоматически освобождать объект после того, как не осталось ни одной ссылки на него.

/// </summary>

public void Dispose()

{

if (isConnected)

Disconnect();

}

#endregion

Класс поддержки плагинов готов, поздравляю вас.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]