在 Win32 应用程序中使用 C++/WinRT 运行时组件 - 第一部分





5.00/5 (8投票s)
C++/WinRT 简介
引言
C++/WinRT 基于 C++ 标准 17,是当前在 Windows 环境中创建应用程序 (C++) 的最现代化的技术。它兼容 C++ 标准 17 意味着它也可以用于经典的 C++ 应用程序,这些应用程序将能够访问最现代的操作系统 API。
因此,我们可以创建由 UWP-C++/WinRT - Win32 - .NET 项目组成的“混合”解决方案,这些项目可以相互交互,让我们能够利用各种技术的优势。
直到不久以前,除了 COM 或通过 C++ / CX (C++ Extended) 创建代理之外,所有这些都无法实现。C++ / CX 是微软创建的一种 C++ 方言,其语法类似于 C# 语言。
C++/CX 已被彻底放弃,今后我们将使用 C++/WinRT,回归使用 C++ 标准。
C++/WinRT 到底是什么?
语言实现与语言投影
首先,有必要弄清楚什么是 WinRT 语言投影。
我们可以将语言投影定义为操作系统以一种统一的(事实上是相同的)和自动的方式公开 Windows Runtime API 的能力,而无论用于实现应用程序或 API 本身使用的是哪种编程语言。
从技术角度来看,操作系统读取元数据并通过 COM 激活正确的语言投影。
其背后的思想与组件对象模型 (COM) 类似,事实上,即使在 COM 中,一个组件一旦发布,无论用什么语言编写,都可以被用任何兼容 COM 语言编写的应用程序透明地使用。因此,在 WinRT 中,就像在 COM 中一样,应用程序可以用各种语言编写(语言实现:C++、C#、VB、Delphi 等)。
每种语言,或者更确切地说,每个编译器,在这种情况下,不仅会生成 DLL 或 EXE,还会生成 `.winmd` 文件(WinMD)或 Windows 元数据,其中包含已实现类型(元数据)的机器码描述。
当需要时,元数据会被软件开发工具和/或运行时操作系统读取和使用,通过激活特定于给定语言的语言投影;因此,在 C++ 的情况下,当操作系统遇到 C++/WinRT 代码(编译成 `.winmd` 文件)时,它将为 C++ 语言触发语言投影。
当然,对于应用程序开发时使用的每种相应语言,也会发生同样的情况。
从技术上讲,C++/WinRT 是一个用于 Windows Runtime Platform 的 C++ 模板库(标准 17),它基于 CRTP(奇偶递归模板模式)模式,并完全以头文件形式分发。
Windows Runtime 组件的目的是,通常,被各种应用程序“消费”;这一特性将 WinRT 组件与 COM 对象联系起来。(“通常”是指这是其自然目的,但也没有阻止你创建一个 WinRT 组件并仅在单个应用程序中消费它;显然,在这种情况下,没必要创建一个 WinRT 组件)。
WinRT 组件可以被用各种语言编写的应用程序消费的事实,意味着(正如 COM 的情况一样),需要用一种标准化的通用语言 **描述** 类型、属性、类、方法等。
与 COM 一样,这种描述将被写入一个或多个 IDL 文件中。
因此,即使在 C++/WinRT 中,一个必须被我们自己的应用程序以及其他应用程序消费的运行时类,也必须在 `.idl` 文件中 **声明**。
这是必需的,以便在 `.idl` 文件中声明的类型随后能被 MIDL 工具导出到编译后的 `.winmd` 文件中。
C++/WinRT 中 IDL 文件的初始结构
在 Visual Studio 2019 中创建一个新的 WinRT 组件项目,我们也会找到一个 IDL 文件:`Class.idl`;其内容如下
namespace UWPUtils
{
[default_interface]
runtimeclass Class
{
Class();
Int32 MyProperty;
}
}
在类的声明中,我们可以注意到它被 `[default_interface]` 属性修饰,并使用 `runtimeclass` 关键字定义(其含义显而易见)。
同样在 `.idl` 文件中,我们看到有一个默认构造函数;对于在 `.idl` 文件中声明的每个构造函数,编译器都会生成实现类型和投影类型,实际上,它为我们的源代码编写了一个现成的桩。
在 `.idl` 文件中定义的构造函数将由应用程序在其 **编译单元** 之外消费运行时类时使用。
属性,还是非属性,这是个问题
关于属性
在 Visual Studio 提供的初始 `.idl` 文件中,除了默认构造函数外,还定义了一个属性(`MyProperty`)。然而,它并没有指定该属性是只读的还是可写的。
当未指定时,MIDL 工具将生成相应的 `get` 和 `set` 方法(即 `get` 和 `set` 是隐含的)。
熟悉标准 C++ 的人可能会感到困惑,因为这个 `.idl` 文件看起来更像是引用了某种其他语言(主要是 Java 或 C#),而不是 C++ 应用程序;事实上,在标准 C++ 语言中,不可能像我们在 C#、Java、Delphi 等语言中理解属性那样,在类中定义属性,并附带相应的 getter 和 setter;因此,在 C++/WinRT 中,对于 IDL 文件中描述的每个属性,MIDL 工具都会生成一个 `get` 和 `set` 方法(见下文示例)。
声明一个 C# 类
// Class with properties in C#
class DemoStudent
{
// default ctor
public DemoStudent() { }
//properties
public string FirstName { get; set; }
public string LastName { get; set; }
//other code
................
................
}
声明相同的类在标准 C++ 17 中
// Class without properties in standard C++ 17
class DemoStudent
{
public:
//default ctor
DemoStudent() = default;
//default dtor
virtual ~DemoStudent() = default;
// getter/setter or accessor/mutator if you prefer
void set_FirstName(const std:wstring& name);
const std::wstring get_FirstName() const;
void set_LastName(const std:wstring& last_name);
const std::wstring get_LastName() const;
//other code
................
................
private:
std::wstring m_name;
std::wstring m_last_name;
//other code
................
................
};
如果我们稍稍偏离标准 C++,并使用 Microsoft Visual C++,我们可以用 C++ 语言的扩展来定义属性
// Class with properties in standard C++ 17 thanks to Visual C++ extension
class DemoStudent()
{
public:
//Default ctor/dtor
DemoStudent() = default;
virtual ~DemoStudent() = default;
//getters/setters
std::wstring get_FirstName(){ return m_FirstName;}
void put_FirstName(std::wstring fname){ m_FirstName = fname;}
std::wstring get_LastName(){ return m_LastName;}
void put_LastName( std::wstring lname){ m_LastName = lname;}
// Properties declaration:
// FirstName
// LastName
__declspec(property(get = get_FirstName, put = put_FirstName)) std::wstring FirstName;
__declspec(property(get = get_LastName, put = put_LastName)) std::wstring LastName;
//other code
................
................
private:
std::wstring m_FirstName;
std::wstring m_LastName;
};
C++/WinRT 中的属性
在 C++/WinRT 中,属性不在类中定义,而是在相应的 `.idl` 文件中定义,并且在编译时 MIDL 会生成 setter 和 getter。
带属性的 C++/WinRT 类
// file DemoStudent.idl
// Interface declaration in MIDL 3.0
namespace UWPUtils
{
[default_interface]
runtimeclass DemoStudent
{
//default ctor
DemoStudent();
//Properties
String FirstName; //implicit get;set;
String LastName; //implicit get;set;
//other code
................
................
}
}
// file DemoStudent.h
// Class implementation with properties in C++/WinRT
namespace winrt::UWPUtils::implementation
{
struct DemoStudent : DemoStudentT<demostudent>
{
//default ctor
DemoStudent() = default;
//get
hstring FirstName();
//set
void FirstName(hstring const& value);
//get
hstring LastName();
//set
void LastName(hstring const& value);
//other code
................
................
};
}
namespace winrt::UWPUtils::factory_implementation
{
struct DemoStudent : DemoStudentT<demostudent, implementation::demostudent="">
{
};
}
运行时类的结构
在上面的 `.h` 文件中,我们可以看到运行时类实际上是一个 `struct`;众所周知,区别在于在 `struct` 中,成员和方法默认是 `public` 的。
我们还可以看到,我们的运行时类(上面的 `struct DemoStudent`)继承了一个模板基类(`DemoStudentT`),该基类以运行时类本身作为参数。
我们知道,在 C++ 中,当一个类继承了一个模板类,而该模板类的参数中包含派生类本身时,我们就遇到了奇偶递归模板模式 **(CRTP)** 或 F-bound 多态模式(见下文示例),而 **C++/WinRT 正是基于这种惯用法**。
// Curiously Recurring Template Pattern (CRTP) o F-bound polymorphism pattern sample
// A derived class inherits from a template class with itself as template argument
// Template base class
template <class t="">
class BaseT
{
public:
BaseT() = default;
virtual ~BaseT() = default;
std::wstring Run() {
static_cast<t*>(this)->Execute();
return L"Method Execute called from base!";
}
};
// CRTP derived class
class Derived : public BaseT<derived>
{
public:
Derived() = default;
virtual ~Derived() = default;
void Execute() { result = L"Method Execute called from derived!"; Print(); }
private:
std::wstring result = L"";
void Print() { std::wcout << result << std::endl; }
};
//------------------------------------------------
// main
int main()
{
auto d = std::make_unique<derived>();
std::wstring str = d->Run();
std::wcout << str << std::endl;
d = nullptr;
d.reset();
return 0;
};
//------------------------------------------------
//output:
Method Execute called from derived!
Method Execute called from base!
实现类型 如前所述,在我们的例子中,有必要在 `.idl` 文件中声明我们的运行时类,完成此操作后,在构建时,工具链(`midl.exe` 和 `cppwinrt.exe`)会为我们生成一个实现类型。
这就是我上面提到的 `struct` 的存根。
Visual Studio 生成的存根 `struct`,基于我们在 `.idl` 文件中声明的运行时类,已准备就绪,一部分保存在 `.h` 文件中,另一部分保存在 `.cpp` 文件中;我们可以选择将这两个文件从 `sources` 文件夹复制/粘贴到我们项目的文件夹中。
生成的两个文件位于 `.. project\Generated Files\sources\` 文件夹中,其内容如下
// file DemoStudent.h
#pragma once
#include "DemoStudent.g.h"
// Note: Remove this static_assert after copying these generated source files to your project.
// This assertion exists to avoid compiling these generated source files directly.
//static_assert(false, "Do not compile generated C++/WinRT source files directly");
namespace winrt::UWPUtils::implementation
{
struct DemoStudent : DemoStudentT<demostudent>
{
DemoStudent() = default;
hstring FirstName();
void FirstName(hstring const& value);
hstring LastName();
void LastName(hstring const& value);
};
}
namespace winrt::UWPUtils::factory_implementation
{
struct DemoStudent : DemoStudentT<demostudent, implementation::demostudent="">
{
};
}
// file DemoStudent.cpp
#include "pch.h"
#include "DemoStudent.h"
#include "DemoStudent.g.cpp"
// Note: Remove this static_assert after copying these generated source files to your project.
// This assertion exists to avoid compiling these generated source files directly.
//static_assert(false, "Do not compile generated C++/WinRT source files directly");
namespace winrt::UWPUtils::implementation
{
hstring DemoStudent::FirstName()
{
throw hresult_not_implemented();
}
void DemoStudent::FirstName(hstring const& value)
{
throw hresult_not_implemented();
}
hstring DemoStudent::LastName()
{
throw hresult_not_implemented();
}
void DemoStudent::LastName(hstring const& value)
{
throw hresult_not_implemented();
}
}
Visual Studio 生成的其余类型实现(假设文件名为 `demostudent`)如下例所示,位于文件中
- DemoStudent.g.h
- DemoStudent.g.cpp
- module.g.cpp
//file DemoStudent.g.h
// WARNING: Please don't edit this file. It was generated by C++/WinRT v2.0.201113.7
#pragma once
#include "winrt/UWPUtils.h"
namespace winrt::UWPUtils::implementation
{
template <typename d="">
struct __declspec(empty_bases) DemoStudent_base : implements<d, uwputils::demostudent="">
{
using base_type = DemoStudent_base;
using class_type = UWPUtils::DemoStudent;
using implements_type = typename DemoStudent_base::implements_type;
using implements_type::implements_type;
hstring GetRuntimeClassName() const
{
return L"UWPUtils.DemoStudent";
}
};
}
namespace winrt::UWPUtils::factory_implementation
{
template <typename d="">
struct __declspec(empty_bases)
DemoStudentT : implements<d, windows::foundation::iactivationfactory="">
{
using instance_type = UWPUtils::DemoStudent;
hstring GetRuntimeClassName() const
{
return L"UWPUtils.DemoStudent";
}
auto ActivateInstance() const
{
return make<t>();
}
};
}
#if defined(WINRT_FORCE_INCLUDE_DEMOSTUDENT_XAML_G_H) || __has_include("DemoStudent.xaml.g.h")
#include "DemoStudent.xaml.g.h"
#else
namespace winrt::UWPUtils::implementation
{
template <typename d="">
using DemoStudentT = DemoStudent_base<d, i...="">
}
#endif
// file DemoStudent.g.cpp
// WARNING: Please don't edit this file. It was generated by C++/WinRT v2.0.201113.7
void* winrt_make_UWPUtils_DemoStudent()
{
return winrt::detach_abi
(winrt::make<winrt::uwputils::factory_implementation::demostudent>());
}
WINRT_EXPORT namespace winrt::UWPUtils
{
DemoStudent::DemoStudent() :
DemoStudent(make<uwputils::implementation::demostudent>())
{
}
}
// file module.g.cpp
// WARNING: Please don't edit this file. It was generated by C++/WinRT v2.0.201113.7
#include "pch.h"
#include "winrt/base.h"
void* winrt_make_UWPUtils_DemoStudent();
bool __stdcall winrt_can_unload_now() noexcept
{
if (winrt::get_module_lock())
{
return false;
}
winrt::clear_factory_cache();
return true;
}
void* __stdcall winrt_get_activation_factory([[maybe_unused]] std::wstring_view const& name)
{
auto requal = [](std::wstring_view const& left, std::wstring_view const& right) noexcept
{
return std::equal(left.rbegin(), left.rend(), right.rbegin(), right.rend());
};
if (requal(name, L"UWPUtils.DemoStudent"))
{
return winrt_make_UWPUtils_DemoStudent();
}
return nullptr;
}
int32_t __stdcall WINRT_CanUnloadNow() noexcept
{
#ifdef _WRL_MODULE_H_
if (!::Microsoft::WRL::Module<::Microsoft::WRL::InProc>::GetModule().Terminate())
{
return 1;
}
#endif
return winrt_can_unload_now() ? 0 : 1;
}
int32_t __stdcall WINRT_GetActivationFactory(void* classId, void** factory) noexcept try
{
std::wstring_view const name{ *reinterpret_cast<winrt::hstring*>(&classId) };
*factory = winrt_get_activation_factory(name);
if (*factory)
{
return 0;
}
#ifdef _WRL_MODULE_H_
return ::Microsoft::WRL::Module<::Microsoft::WRL::InProc>::GetModule().
GetActivationFactory(static_cast<hstring>(classId),
reinterpret_cast<::IActivationFactory**>(factory));
#else
return winrt::hresult_class_not_available(name).to_abi();
#endif
}
catch (...) { return winrt::to_hresult();
}
总而言之,必须承认,乍一看,Visual Studio 生成的代码可能令人望而生畏,但实际上,它并没有什么复杂的。
从 `DemoStudent.g.h` 文件开始,在命名空间之后,我们找到了一个可变参数模板类(即一个 `struct`,在此例中,但函数也可以是可变参数模板)的声明,它支持任意数量的参数(就像 C 的 `printf` 函数一样),其名称为 `DemoStudent_base`。
这是我们的基本类(`struct`)。
`DemoStudent_base` 结构继承了 `winrt::implements` 结构,后者也是一个可变参数模板类(`struct`),它包含(除其他参数外)`UWPUtils::DemoStudent`。
`winrt::implements` 结构是每个运行时类或激活工厂直接或间接继承的 **基结构**;它实现了各种基本接口,包括 `IUnknown`、`IInspectable` 等。
以下代码是一系列 `using new_type_name =` 或现有类型的别名(C++ 17)以及命名空间的使用。
下面,我们找到了一个 `GetRuntimeClassName()` 方法,它返回一个 `hstring` 值。
`hstring` (`winrt::hstring`) 是一个包含一系列 UNICODE UTF-16 字符的 `struct`;也就是说,文本 `string`。
最后,我们找到了 `ActivateInstance` 方法,该方法根据传递的 `T` 参数,通过调用模板函数 `make
- 如果我们正在创建一个被实现该组件的编译单元之外的应用所消费的 `winrt` 组件:返回(投影)实现类型的默认接口。
- 如果我们正在同一个编译单元内实现和消费一个运行时类:返回投影类型的实例。
对于 `DemoStudentT` 结构也是如此,但这次是在 `factory_implementation` 命名空间中。
最后,在实现命名空间中,使用指令(C++ 17)`using DemoStudentT = DemoStudent_base
历史
- 2022 年 1 月 13 日:初始版本