Pull to refresh

Создания Windows Runtime компонента на Visual C++

Reading time11 min
Views6.8K
Тернистая дорога через дебри 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"

Она состоит из небольшого количества последовательных шагов, каждый из которых выполняет некоторую задачу.
  1. Удаляем из папки назначения файл метаданных проекта NMSPC.TestComponent.winmd.
  2. Комбинируем наши файлы метаданных. Результат будет помещён в папку Output в $(OutDir).
  3. Копируем файлы метаданных из папки Ouput в папку $(OutDir).
  4. Удаляем папку 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 Kits\8.1\Include\winrt). Пространство имён для класса было выбрано в соответствии с названием проекта 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.\r\n");
		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.\r\n");
	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
Tags:
Hubs:
Total votes 8: ↑6 and ↓2+4
Comments4

Articles