Тернистая дорога через дебри C# и заросли C++/CX разработки для Windows Runtime в какой-то момент привела меня к библиотеке шаблонов WRL, облегчающей написание приложений и компонентов WinRT и COM. При работе именно с этой библиотекой мне захотелось узнать, что же может скрывает под собой код:
#include "pch.h"
#include "RAWinRT.WRL.h"
using namespace Microsoft::WRL::Wrappers;
using namespace Microsoft::WRL;
using namespace ABI::RAWinRT::WRL;
using namespace ABI::Windows::ApplicationModel::Background;
class ABI::RAWinRT::WRL::TestTask : public RuntimeClass < RuntimeClassFlags<WinRt>, IBackgroundTask >
{
InspectableClass(RuntimeClass_RAWinRT_WRL_TestTask, BaseTrust);
public:
STDMETHODIMP Run(IBackgroundTaskInstance *taskInstance) override
{
return S_OK;
}
};
ActivatableClass(TestTask);
и эти загадочные макросы, шаблоны, функции библиотеки.
И решил я начать с самой простого. Написать компонент Windows Runtime, имеющий единственный «класс» фоновой задачи, на Visual C++.
Если вам интересно, что из этого получилось, то добро пожаловать под кат.
Создание и настройка проекта компонента
Сначала я создал пустой файл решения в IDE Visual Studio 2013 и добавил в него проект DLL библиотеки для Windows Store приложения.
Для проекта я выбрал имя NMSPC.TestComponent, где NMSPC – некоторое пространство имён. Сделал это в демонстрационных целях, поскольку такое именование является достаточно частой практикой при создание проектов. Также, изменил пространство имён по умолчанию c NMSPC_TestComponent на соответствующее названию проекта.
Для файлов я предпочитаю более короткие названия, поэтому переименовал заголовочный файл и файл исходного кода на TetsComponent. Перед тем, как приступить к реализации компонента в коде, добавил несколько дополнительных файлов. TestComponent.def – файл определения экспортируемых динамической библиотекой функция, TestComponent.idl – файл описания интерфейсов.
Добавив эти файлы в проект, приступил к его настройке. Чтобы не менять настройки для каждой конфигурации по отдельности, мне достаточно было выбрать все конфигурации и платформы, а затем перейти к редактированию параметров. Была задана настройки уровня предупреждений, указан параметр генерации метаданных, изменен шаблон имени генерируемого MIDL компилятором заголовочного файла, добавлена компоновка с runtimeobject.lib и выбрана подсистема.
Далее, настроил дополнительный шаг построения проекта. Про него расскажу чуть-чуть подробнее.
Данный шаг предназначен для правильной генерации метаданных проекта. Командная строка была задана следующим образом:
del "$(OutDir)$(TargetName).winmd" && mdmerge -partial -i "$(OutDir)." -o "$(OutDir)Output" -metadata_dir "$(WindowsSDK_MetadataPath)" && del "$(OutDir)*.winmd" && copy /y "$(OutDir)Output*" "$(OutDir)" && rd /q /s "$(OutDir)Output"
Она состоит из небольшого количества последовательных шагов, каждый из которых выполняет некоторую задачу.
- Удаляем из папки назначения файл метаданных проекта NMSPC.TestComponent.winmd.
- Комбинируем наши файлы метаданных. Результат будет помещён в папку Output в $(OutDir).
- Копируем файлы метаданных из папки Ouput в папку $(OutDir).
- Удаляем папку Output вместе с содержимым.
Проделав все эти предварительные шаги, я наконец-то смог приступить к написанию кода.
DEF, MIDL, PCH
Любая «уважающая себя» библиотека компонента Windows Runtime должна экспортировать две очень важные функции DllGetActivationFactory и DllCanUnloadNow, которые используются средой исполнения. Экспорт данных функций был определён в файле TestComponent.def (также их необходимо будет реализовать в коде, но об этом чуть позднее).
EXPORTS
DllGetActivationFactory PRIVATE
DllCanUnloadNow PRIVATE
Далее, я описал интерфейс класса в файле TestComponent.idl.
import "Windows.ApplicationModel.Background.idl";
namespace NMSPC
{
namespace TestComponent
{
[version(1.0)]
[activatable(1.0)]
[marshaling_behavior(agile)]
[threading(both)]
runtimeclass TestBackgroundTask
{
[default] interface Windows.ApplicationModel.Background.IBackgroundTask;
};
}
}
Первой директивой импортируется файл с описание интерфейса фоновой задачи Windows::ApplicationModel::Background::IBackgroundTask. Так как этого файла достаточно для MIDL компилятора, то необходимость в импорте других файлов описания интерфейсов отсутствует (для платформы Windows Store 8.1 файлы описания интерфейсов и заголовочные файлы расположены в C:Program Files (x86)Windows Kits8.1Includewinrt). Пространство имён для класса было выбрано в соответствии с названием проекта NMSPC::TestComponent. С помощью атрибутов были заданы версия класса(version), признак наличия конструктора по-умолчанию(activatable), работа с потоками(threading) и маршалинг(marshaling_behavior). Скомпилировав данный с помощью MIDL компилятора, я получил заголовочный файл TetsComponent.h.
Для уменьшения времени компиляции, дополнительно вынес директивы включения заголовочных файлов activation.h и new в файл pch.h(который используется для генерации предварительно скомпилированных заголовочных файлов). Необходимость включения этих заголовочных файлов объясняется зависимостью от интерфейса IActivationFactory и константы std::nothrow.
#pragma once
#include "targetver.h"
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
#endif
#include <windows.h>
#include <activation.h>
#include <new>
Оставалось только реализовать класс, фабрику и экспортируемые функции в коде.
Код
Первым делом, я включил в файл кода TestComponent.cpp кроме файла предкомпилированных заголовков ещё и сгенерированный MIDL компилятором заголовочный файл TestComponent.h. По соглашению, все генерируемые MIDL компиляторам интерфейсы размещаются в пространстве имён ABI, поэтому интерфейсы для класса и его декларация будут располагаться в ABI::NMSPC::TestComponent, а интерфейсы для реализации фоновой задачи в ABI::Windows::ApplicationModel::Background(я не стал импортировать все пространство имён, вместо этого указал использование только отдельных интерфейсов).
#include "pch.h"
#include "TestComponent.h"
//Импортируем пространство имён нашего компонента
using namespace ABI::NMSPC::TestComponent;
//Импортируем интерфейсы из пространства имён ABI::Windows::ApplicationModel::Background
using ABI::Windows::ApplicationModel::Background::IBackgroundTask;
using ABI::Windows::ApplicationModel::Background::IBackgroundTaskInstance;
Класс реализации фоновой задачи получился достаточно простым. По сути, необходимо было реализовать интерфейсы IUnknown, IInspectable и IBackgroundTask.
//Класс реализации фоновой задачи.
//Реализует единственный "интерфейс" IBackgroundTask
class ABI::NMSPC::TestComponent::TestBackgroundTask sealed : public IBackgroundTask
{
//Переменная для подсчёта ссылок на текущий объект
ULONG m_count;
public:
TestBackgroundTask() throw()
: m_count(1)
{
//Увеличиваем общее количество экземпляров объектов библиотеки
InterlockedIncrement(&m_objectsCount);
}
~TestBackgroundTask() throw()
{
//Уменьшаем общее количество экземпляров объектов библиотеки
InterlockedDecrement(&m_objectsCount);
}
#pragma region IUnknown
//Реализация COM метода увеличения счетчика ссылок на объект
STDMETHODIMP_(ULONG) AddRef() throw() override final
{
//Увеличиваем количество ссылок на объект и возвращаем результат
return InterlockedIncrement(&m_count);
}
//Реализация COM метода уменьшения счетчика ссылок на объект
STDMETHODIMP_(ULONG) Release() throw() override
{
//Получаем результат после уменьшения количества ссылок на объект
auto const count = InterlockedDecrement(&m_count);
//Если количество стало равным нулю
if (0 == count)
{
//Уничтожаем объект
delete this;
}
//Возвращаем количество ссылок
return count;
}
//Реализация COM метода опроса на имплементацию заданного интерфейса
STDMETHODIMP QueryInterface(const IID& riid, void** ppvObject) throw() override final
{
//Проверка запроса на равенство реализуемым интерфейсам
//Проверяются три интерфеса так как IBackgroundTask наследует IInspectable
//А IInspectable наследует IUnknown
if (__uuidof(IUnknown) == riid || __uuidof(IInspectable) == riid || __uuidof(IBackgroundTask) == riid)
{
*ppvObject = this;
}
else
{
*ppvObject = nullptr;
//Возвращаем константу означающую, что данный интерфейс не поддерживается
return E_NOINTERFACE;
}
//Увеличиваем количество ссылок на объект
//Это стандартное соглашение
static_cast<IInspectable*>(*ppvObject)->AddRef();
return S_OK;
}
#pragma endregion
#pragma region IInspectable
//Реализация WINRT метода получения массива идентификаторов реализуемых интерфейсов
STDMETHODIMP GetIids(ULONG* iidCount, IID** iids) throw() override
{
//Выделяем память для одного GUID, т.к. наш класс реализует только один интерфейс
//Используетс функция CoTaskMemAlloc, т.к. вызывающий объект может очистить массив с помощью CoTaskMemFree
*iids = static_cast<GUID*>(CoTaskMemAlloc(sizeof(GUID)));
//Если указатель NULL
if (!*iids)
{
//Возвращаем ошибку отсутствия памяти
return E_OUTOFMEMORY;
}
//Устанавливаем количество реализуемых интерфейсов
*iidCount = 1;
//Инициализируем значение идентификатором интерфейса IBackgroundTask
(*iids)[0] = __uuidof(IBackgroundTask);
return S_OK;
}
//Реализация WINRT метода получения имени Runtime класса
STDMETHODIMP GetRuntimeClassName(HSTRING* className) throw() override final
{
//Проверяем результат возвращаемой функции
//Документация рекомендует возвращает E_OUTOFMEMORY в любом случае неудачи
//Если это не фабрика или статический интерфейс
if (S_OK != WindowsCreateString(
RuntimeClass_NMSPC_TestComponent_TestBackgroundTask,
_countof(RuntimeClass_NMSPC_TestComponent_TestBackgroundTask),
className))
{
return E_OUTOFMEMORY;
}
return S_OK;
}
//Реализация WINRT метода получения TrustLevel объекта
STDMETHODIMP GetTrustLevel(TrustLevel* trustLevel) throw() override final
{
*trustLevel = BaseTrust;
return S_OK;
}
#pragma endregion
#pragma region IBackgroundTask
//Реализация IBackgroundTask метода запуска фоновой задачи
STDMETHODIMP Run(IBackgroundTaskInstance* task_instance) throw() override final
{
//Просто пишем строку в отладочное окно
OutputDebugStringW(L"Hello from background task.rn");
return S_OK;
}
#pragma endregion
};
Теперь, когда класс был готов, нужно было написать класс фабрики объектов. Данный класс фабрики должен реализовывать интерфейс IActivationFactory, который определён в заголовочном файле activation.h. Данный интерфейс, помимо наследования IInspectable(а значит и IUnknown), определяет метод
virtual HRESULT STDMETHODCALLTYPE ActivateInstance(
/* [out] */ __RPC__deref_out_opt IInspectable **instance) = 0;
Также должна отличаться реализация метода GetRuntimeClassName, о чем говорится в документации к методу на MSDN:
https://msdn.microsoft.com/en-us/library/br205823(v=vs.85).aspx
//Класс реализации фабрики фоновых задач.
class TestBackgroundTaskFactory sealed : public IActivationFactory
{
//Переменная для подсчёта ссылок на текущий объект
ULONG m_count;
public:
TestBackgroundTaskFactory() throw()
: m_count(1)
{
//Увеличиваем общее количество экземпляров объектов библиотеки
InterlockedIncrement(&m_objectsCount);
}
~TestBackgroundTaskFactory() throw()
{
//Уменьшаем общее количество экземпляров объектов библиотеки
InterlockedDecrement(&m_objectsCount);
}
//Реализация COM метода увеличения счетчика ссылок на объект
STDMETHODIMP_(ULONG) AddRef() throw() override final
{
//Увеличиваем количество ссылок на объект и возвращаем результат
return InterlockedIncrement(&m_count);
}
//Реализация COM метода уменьшения счетчика ссылок на объект
STDMETHODIMP_(ULONG) Release() throw() override
{
//Получаем результат после уменьшения количества ссылок на объект
auto const count = InterlockedDecrement(&m_count);
//Если количество стало равным нулю
if (0 == count)
{
//Уничтожаем объект
delete this;
}
//Возвращаем количество ссылок
return count;
}
//Реализация COM метода опроса на имплементацию заданного интерфейса
STDMETHODIMP QueryInterface(const IID& riid, void** ppvObject) throw() override final
{
if (__uuidof(IUnknown) == riid || __uuidof(IInspectable) == riid || __uuidof(IActivationFactory) == riid)
{
*ppvObject = this;
}
else
{
*ppvObject = nullptr;
return E_NOINTERFACE;
}
static_cast<IInspectable*>(*ppvObject)->AddRef();
return S_OK;
}
//Реализация WINRT метода получения массива идентификаторов реализуемых интерфейсов
STDMETHODIMP GetIids(ULONG* iidCount, IID** iids) throw() override final
{
//Выделяем память для одного GUID, т.к. наш класс реализует только один интерфейс
//Используетс функция CoTaskMemAlloc, т.к. вызывающий объект может очистить массив с помощью CoTaskMemFree
*iids = static_cast<GUID*>(CoTaskMemAlloc(sizeof(GUID)));
//Если указатель NULL
if (*iids)
{
//Возвращаем ошибку отсутствия памяти
return E_OUTOFMEMORY;
}
//Устанавливаем количество реализуемых интерфейсов
*iidCount = 1;
//Инициализируем значение идентификатором интерфейса IBackgroundTask
(*iids)[0] = __uuidof(IActivationFactory);
return S_OK;
}
//Реализация WINRT метода получения имени Runtime класса
STDMETHODIMP GetRuntimeClassName(HSTRING*) throw() override final
{
//Возвращаем данную константу, т.к. вызовается метод фабрики
return E_ILLEGAL_METHOD_CALL;
}
//Реализация WINRT метода получения TrustLevel объекта
STDMETHODIMP GetTrustLevel(TrustLevel* trustLevel) throw() override final
{
*trustLevel = BaseTrust;
return S_OK;
}
//Реализация IActivationFactory метода инстанциирования экземпляра
STDMETHODIMP ActivateInstance(IInspectable** instance) throw() override final
{
//Если указатель равено null
if (nullptr == instance)
{
//Возвращаем ошибку
return E_INVALIDARG;
}
//Создаём объект
//При этом указываем признак того, что не надо генерировать исключение
*instance = new (std::nothrow) TestBackgroundTask();
//Возвращаем результат в зависимости от успешности создания объекта
return *instance ? S_OK : E_OUTOFMEMORY;
}
};
Внимательный читатель мог заметить странную деталь в конструкторах и деструкторах классов, а именно инкремент и декремент переменной m_objectsCount. Данную переменную я объявил сразу после директив using перед кодом классов. А используется она в экспортируемой библиотекой функции DllCanUnloadNow:
//Реализация экспортируемой функции опроса возможности выгрузки библиотеки
HRESULT WINAPI DllCanUnloadNow() throw()
{
//Возвращаем признак в зависимости от количества текущий экземпляров
return m_objectsCount ? S_FALSE : S_OK;
}
Кроме этой функции, была определена ещё одна DllGetActivationFactory, предназначенная для получения фабрики по идентификатору класса(в Windows Runtime это строка с включением всех пространств имён).
//Реализация экспортируемой функции получения фабрики объектов класса, имеющего идентификатор activatableClassId
HRESULT WINAPI DllGetActivationFactory(HSTRING activatableClassId, IActivationFactory **factory) throw()
{
//Проверяем идентфикатор класса и указатель на фабрику
if (WindowsIsStringEmpty(activatableClassId) || nullptr == factory)
{
//Если идентификатор не задан или указатель нулевой
return E_INVALIDARG;
}
//Проверяем на равенство строки идентификатора класса и определенного нами класса
if (0 == wcscmp(RuntimeClass_NMSPC_TestComponent_TestBackgroundTask, WindowsGetStringRawBuffer(activatableClassId, nullptr)))
{
//Инициализируем указатель
*factory = new (std::nothrow) TestBackgroundTaskFactory();
return *factory ? S_OK : E_OUTOFMEMORY;
}
*factory = nullptr;
return E_NOINTERFACE;
}
Перед тем, как рассказать об использовании компонента в C# приложении, упомяну ещё о явной реализации функции DllMain, определённой в файле dllmain.cpp. Я использовал её только в диагностических целях, но варианты использования могут быть отличными от моего.
#include "pch.h"
BOOL APIENTRY DllMain(HMODULE /* hModule */, DWORD ul_reason_for_call, LPVOID /* lpReserved */)
{
OutputDebugStringW(L"Hello from DLL.rn");
return TRUE;
}
На этом реализация библиотеки компонента была закончена. И я смог приступить к её практическому использованию в приложении.
C# приложение
Создав проект приложения NMSPC.CSTestAppp с помощью шаблона Blank App, я добавил в него ссылки на проект компонента и Microsoft Visual C++ 2013 Runtime Package.
Оставалось только отредактировать файл манифеста приложения, добавив в него определение фоновой задачи, и написать код, выполняющий регистрацию фоновой задачи.
Код разместил в методе OnLaunched класса App. Код простой: сначала удаляет все регистрации задач, потом создаёт объект-buiilder задачи, устанавливает триггер, указанный в манифесте, и регистрирует задачу.
foreach (var pair in BackgroundTaskRegistration.AllTasks)
{
pair.Value.Unregister(true);
}
var taskBuilder = new BackgroundTaskBuilder
{
Name = "TestBackgroundTask",
TaskEntryPoint = "NMSPC.TestComponent.TestBackgroundTask"
};
taskBuilder.SetTrigger(new SystemTrigger(SystemTriggerType.TimeZoneChange, true));
taskBuilder.Register();
Для того, чтобы иметь возможность перехода к точкам останова в коде на C++, установил в настройках отладки проекта приложения тип процесса Mixed(Managed and Native). Кстати, эта настройка также актуальна и для C++/CX приложений.
Теперь можно было запустить приложение в режиме отладки, выполнить код регистрации компонента и протестировать запуск фоновой задачи с помощью кнопки Lifecycle Events раздела Debug Locations.
Выполнив это, я увидел те самые заветные строки в окне Output, вывод которых был запрограммирован в C++ коде с помощью функции OutputDebugStringW.
Hello from DLL.
Hello from background task.
Заключение
Как оказалось, написать код компонента без использования WRL возможно. Решение этой задачи позволило лучше узнать механизмы исполнения и принципы взаимодействия компонентов среды Windows Runtime.
Исходный код доступен на GitHub
https://github.com/altk/RuntimeComponent
Автор: altk