Windows运行时组件技术研究

发布于 2020-11-08  375 次阅读


综述

Windows 运行时组件是自包含对象,可将其实例化,并可采用任一语言使用它,包括 C#、
Visual Basic、JavaScript 和 C++。

你可以使用 Visual Studio 和 C#、Visual Basic 或 C++ 创建可用于通用 Windows 平台
(UWP) 应用中的 Windows 运行时组件。

使用 C++/CX 创建 Windows 运行时组件

微软在其文档中写道:

建议使用/WinRT 作为C++/cx 替代方法。 C++ 它是新的标准 c + + 17 语言投影,适用于 Windows 运行时 Api,可从版本1803的最新 Windows 10 SDK 获得。 C++/WinRT 完全在标头文件中实现,旨在向您提供对新式 Windows API 的第一类访问。
使用C++/WinRT,可以使用任何符合标准的 c + + 17 编译器来使用和创作 Windows 运行时 api。 C++与 Windows 运行时的任何其他语言选项相比,/WinRT 通常性能更好,生成的二进制文件更小。 我们将继续支持 C++/CX 和 WRL,但强烈建议新应用程序使用 C++/WinRT。 有关详细信息,请参阅 C++/WinRT。

笔者查看了 C++/WinRT 的要求,发现:

针对 C++/WinRT、XAML、VSIX 扩展和 NuGet 包的 Visual Studio 支持
若要获取 Visual Studio 支持,需要 Visual Studio 2019 或 Visual Studio 2017(至少需要版本 15.6;建议至少使用 15.7)。 从 Visual Studio 安装程序中,安装“通用 Windows 平台开发” 工作负荷。 在“安装详细信息” > “通用 Windows 平台开发”中,选中“C++ (v14x) 通用 Windows 平台工具” 选项(如果尚未这样做)。 另外,请在 Windows 的“设置” > “更新和安全”& > “面向开发人员”中选择“开发人员模式”选项而非“旁加载应用”选项。
虽然我们建议使用最新版 Visual Studio 和 Windows SDK 进行开发,但如果你使用的 C++/WinRT 版本是 10.0.17763.0(Windows 10 版本 1809)之前的 Windows SDK 随附的,则至少需要在项目中使用 Windows SDK 目标版本 10.0.17134.0(Windows 10 版本 1803)才能使用上述 Windows 命名空间标头。

笔者当前使用的开发环境为VS2015,所以当前继续使用 C++/CX 进行开发。

大小写和命名规则

JavaScript

JavaScript 区分大小写。 因此,必须遵循以下大小写约定:

  • 引用 C++ 命名空间和类时,采用在 C++ 端上使用的相同大小写。
  • 调用方法时,使用 Camel 大小写格式,即使方法名在 C++ 端上是大写。 例如,C++ 方法
    GetDate() 必须作为 getDate() 从 JavaScript 调用。
  • 可激活类名和命名空间名称不能包含 UNICODE 字符。

.Net

.NET 语言遵循其正常大小写规则。

实例化对象

仅 Windows 运行时类型可以跨 ABI (application binary interface)边界传递。 如果
组件在公共方法中具有作为返回类型或参数的类型(例如 std::wstring),编译器将引发错
误。 Visual C++ 组件扩展 (C++/CX) 内置类型包括诸如整型和双精度型的常用标量及其
typedef 等效项 int32、float64 等。 有关详细信息,请参阅类型系统 (C++/CX)。

// ref class definition in C++
public ref class SampleRefClass sealed
{
    // Class members...

    // #include <valarray>
public:
    double LogCalc(double input)
    {
        // Use C++ standard library as usual.
        return std::log(input);
    }

};
//Instantiation in JavaScript (requires "Add reference > Project reference")
var nativeObject = new CppComponent.SampleRefClass();
//Call a method and display result in a XAML TextBlock
var num = nativeObject.LogCalc(21.5);
ResultText.Text = num.ToString();

C++/CX 内置类型、库类型和 Windows 运行时类型

可激活类(也称为 ref 类)是可通过其他诸如 JavaScript、C# 或 Visual Basic 语言实例
化的类。 若要能够通过其他语言使用,组件必须包含至少一项可激活类。

Windows 运行时组件可以包含多个公共的可激活类以及其他仅为组件内部所知的类。 将
WebHostHidden特性应用于C++不应向 JavaScript 显示的/cx 类型。

所有公共类必须驻留在与组件元数据文件具有相同名称的相同根命名空间中。 例如,名为
A.B.C.MyClass 的类仅可在名为 A.winmd、A.B.winmd 或 A.B.C.winmd 的元数据文件中定
义的情况下才可以实例化。 DLL 名称不需要匹配 .winmd 文件名。

就像对任意类一样,客户端代码使用 new(在 Visual Basic 中是 New)关键字创建组件实
例。

可激活类必须声明为 public ref class sealed。 ref class 关键字告知编译器将类创建
为 Windows 运行时兼容类型,而 sealed 关键字指定该类无法继承。 Windows 运行时当前无
法支持一般化的继承模型;有限的继承模型支持创建自定义 XAML 控件。 有关详细信息,请参
阅 Ref 类和结构 (C++/CX)。

对于C++/cx,所有数值基元都在默认命名空间中定义。 平台命名空间包含C++特定于Windows
运行时类型系统的/cx 类。 这些类包括 Platform::String 类和 Platform::Object 类。
诸如 Platform::Collections::Map 类和 Platform::Collections::Vector 类的具体集
合类型在 Platform::Collections 命名空间中定义。 这些类型实现的公共接口在
Windows::Foundation::Collections 命名空间 (C++/CX) 中定义。 JavaScript、C# 和
Visual Basic 使用的正是这些接口类型。 有关详细信息,请参阅类型系统 (C++/CX)。

返回内置类型值的方法

    // #include <valarray>
public:
    double LogCalc(double input)
    {
        // Use C++ standard library as usual.
        return std::log(input);
    }
//Call a method
var nativeObject = new CppComponent.SampleRefClass;
var num = nativeObject.logCalc(21.5);
document.getElementById('P2').innerHTML = num;

返回自定义值结构的方法

namespace CppComponent
{
    // Custom struct
    public value struct PlayerData
    {
        Platform::String^ Name;
        int Number;
        double ScoringAverage;
    };

    public ref class Player sealed
    {
    private:
        PlayerData m_player;
    public:
        property PlayerData PlayerStats
        {
            PlayerData get(){ return m_player; }
            void set(PlayerData data) {m_player = data;}
        }
    };
}

若要跨 ABI 传递用户定义的值结构,请定义一个 JavaScript 对象,该对象具有与在/Cx 中C
++定义的值结构相同的成员。 然后,可以将该对象作为自变量传递给C++/cx 方法,以便将对
象隐式转换为C++/cx 类型。

// Get and set the value struct
function GetAndSetPlayerData() {
    // Create an object to pass to C++
    var myData =
        { name: "Bob Homer", number: 12, scoringAverage: .357 };
    var nativeObject = new CppComponent.Player();
    nativeObject.playerStats = myData;

    // Retrieve C++ value struct into new JavaScript object
    var myData2 = nativeObject.playerStats;
    document.getElementById('P3').innerHTML = myData.name + " , " + myData.number + " , " + myData.scoringAverage.toPrecision(3);
}

另一种方法是定义实现 IPropertySet 的类(未显示)。
在 .NET 语言中,只需创建一个在C++/cx 组件中定义的类型的变量。

private void GetAndSetPlayerData()
{
    // Create a ref class
    var player = new CppComponent.Player();

    // Create a variable of a value struct
    // type that is defined in C++
    CppComponent.PlayerData myPlayer;
    myPlayer.Name = "Babe Ruth";
    myPlayer.Number = 12;
    myPlayer.ScoringAverage = .398;

    // Set the property
    player.PlayerStats = myPlayer;

    // Get the property and store it in a new variable
    CppComponent.PlayerData myPlayer2 = player.PlayerStats;
    ResultText.Text += myPlayer.Name + " , " + myPlayer.Number.ToString() +
        " , " + myPlayer.ScoringAverage.ToString();
}

重载的方法

C++/Cx 公共 ref 类可以包含重载方法,但 JavaScript 的功能有限,可以区分重载方法。
例如,它可以区分以下签名之间的区别:

public ref class NumberClass sealed
{
public:
    int GetNumber(int i);
    int GetNumber(int i, Platform::String^ str);
    double GetNumber(int i, MyData^ d);
};

但是它无法区分以下内容之间的区别:

int GetNumber(int i);
double GetNumber(double d);

在不明确的情况下,通过在标头文件中将
Windows::Foundation::Metadata::DefaultOverload 属性应用到方法签名,你可以确保
JavaScript 始终调用特定重载。

此 JavaScript 始终调用属性化重载:

var nativeObject = new CppComponent.NumberClass();
var num = nativeObject.getNumber(9);
document.getElementById('P4').innerHTML = num;

.NET

.NET 语言识别C++/cx ref 类中的重载,就像在任何 .net 类中一样。

DateTime

在 Windows 运行时中,Windows::Foundation::DateTime 对象仅是一个 64 位有符号的整
数,代表 1601 年 1 月 1 日前或后 100 纳秒间隔的数字。
Windows:Foundation::DateTime 对象上没有方法。 相反,每个语言以该语言的本机方式投
影日期时间: JavaScript 中的 Date 对象和 .NET 中的 system.string 和
System.object 类型。

public  ref class MyDateClass sealed
{
public:
    property Windows::Foundation::DateTime TimeStamp;
    void SetTime(Windows::Foundation::DateTime dt)
    {
        auto cal = ref new Windows::Globalization::Calendar();
        cal->SetDateTime(dt);
        TimeStamp = cal->GetDateTime(); // or TimeStamp = dt;
    }
};

将日期时间值从C++/cx 传递到 JavaScript 时,JavaScript 会接受它作为日期对象,并默
认显示为长格式日期字符串。

function SetAndGetDate() {
    var nativeObject = new CppComponent.MyDateClass();

    var myDate = new Date(1956, 4, 21);
    nativeObject.setTime(myDate);

    var myDate2 = nativeObject.timeStamp;

    //prints long form date string
    document.getElementById('P5').innerHTML = myDate2;

}

当 .NET 语言将 system.string 传递到C++/cx 组件时,该方法会将其接受为 Windows::
Foundation::Datetime。 当组件将 Windows::Foundation::DateTime 传递到 .NET 方法
时,框架方法会将其作为 DateTimeOffset 接受。

private void DateTimeExample()
{
    // Pass a System.DateTime to a C++ method
    // that takes a Windows::Foundation::DateTime
    DateTime dt = DateTime.Now;
    var nativeObject = new CppComponent.MyDateClass();
    nativeObject.SetTime(dt);

    // Retrieve a Windows::Foundation::DateTime as a
    // System.DateTimeOffset
    DateTimeOffset myDate = nativeObject.TimeStamp;

    // Print the long-form date string
    ResultText.Text += myDate.ToString();
}

集合与数组

集合始终作为 Windows 运行时类型(例如 Windows::Foundation::Collections::IVector^ 和 Windows::Foundation::Collections::IMap^)的句柄在 ABI 边界上传递。 例如,如果将句柄返回到 Platform::Collections::Map,它会隐式转换为 Windows::Foundation::Collections::IMap^。 集合接口在单独于提供具体实现的C++/cx 类的命名空间中定义。 JavaScript 和 .NET 语言使用接口。 有关详细信息,请参阅集合 (C++/CX) 和数组和 WriteOnlyArray (C++/CX)。

传递IVector

// Windows::Foundation::Collections::IVector across the ABI.
//#include <algorithm>
//#include <collection.h>
Windows::Foundation::Collections::IVector<int>^ SortVector(Windows::Foundation::Collections::IVector<int>^ vec)
{
    std::sort(begin(vec), end(vec));
    return vec;
}
var nativeObject = new CppComponent.CollectionExample();
// Call the method to sort an integer array
var inVector = [14, 12, 45, 89, 23];
var outVector = nativeObject.sortVector(inVector);
var result = "Sorted vector to array:";
for (var i = 0; i < outVector.length; i++)
{
    outVector[i];
    result += outVector[i].toString() + ",";
}
document.getElementById('P6').innerHTML = result;

.NET 语言将 IVector<T> 视为 IList<T>

private void SortListItems()
{
    IList<int> myList = new List<int>();
    myList.Add(5);
    myList.Add(9);
    myList.Add(17);
    myList.Add(2);

    var nativeObject = new CppComponent.CollectionExample();
    IList<int> mySortedList = nativeObject.SortVector(myList);

    foreach (var item in mySortedList)
    {
        ResultText.Text += " " + item.ToString();
    }
}

传递IMap

// #include <map>
//#include <collection.h>
Windows::Foundation::Collections::IMap<int, Platform::String^> ^GetMap(void)
{
    Windows::Foundation::Collections::IMap<int, Platform::String^> ^ret =
        ref new Platform::Collections::Map<int, Platform::String^>;
    ret->Insert(1, "One ");
    ret->Insert(2, "Two ");
    ret->Insert(3, "Three ");
    ret->Insert(4, "Four ");
    ret->Insert(5, "Five ");
    return ret;
}
// Call the method to get the map
var outputMap = nativeObject.getMap();
var mStr = "Map result:" + outputMap.lookup(1) + outputMap.lookup(2)
    + outputMap.lookup(3) + outputMap.lookup(4) + outputMap.lookup(5);
document.getElementById('P7').innerHTML = mStr;

.NET 语言查看 IMap 和 IDictionary<K, V>。

private void GetDictionary()
{
    var nativeObject = new CppComponent.CollectionExample();
    IDictionary<int, string> d = nativeObject.GetMap();
    ResultText.Text += d[2].ToString();
}

属性

/Cx 组件扩展中C++的公共 ref 类通过使用 property 关键字将公共数据成员作为属性公开。
概念与 .NET 属性相同。 普通属性类似于数据成员,因为其功能是隐式的。 特殊属性具有显
式获取和设置的访问器和作为值的“备份存储”的已命名私有变量。 在此示例中,私有成员变量
_propertyAValue 是 PropertyA 的后备存储。 属性可以在它的值更改时引发某个事件,并
且客户端应用可注册为接收该事件。

//Properties
public delegate void PropertyChangedHandler(Platform::Object^ sender, int arg);
public ref class PropertyExample  sealed
{
public:
    PropertyExample(){}

    // Event that is fired when PropertyA changes
    event PropertyChangedHandler^ PropertyChangedEvent;

    // Property that has custom setter/getter
    property int PropertyA
    {
        int get() { return m_propertyAValue; }
        void set(int propertyAValue)
        {
            if (propertyAValue != m_propertyAValue)
            {
                m_propertyAValue = propertyAValue;
                // Fire event. (See event example below.)
                PropertyChangedEvent(this, propertyAValue);
            }
        }
    }

    // Trivial get/set property that has a compiler-generated backing store.
    property Platform::String^ PropertyB;

private:
    // Backing store for propertyA.
    int m_propertyAValue;
};
var nativeObject = new CppComponent.PropertyExample();
var propValue = nativeObject.propertyA;
document.getElementById('P8').innerHTML = propValue;

//Set the string property
nativeObject.propertyB = "What is the meaning of the universe?";
document.getElementById('P9').innerHTML += nativeObject.propertyB;

.NET 语言访问本机C++/cx 对象上的属性,就像它们在 .net 对象上一样。

private void GetAProperty()
{
    // Get the value of the integer property
    // Instantiate the C++ object
    var obj = new CppComponent.PropertyExample();

    // Get an integer property
    var propValue = obj.PropertyA;
    ResultText.Text += propValue.ToString();

    // Set a string property
    obj.PropertyB = " What is the meaning of the universe?";
    ResultText.Text += obj.PropertyB;

}

委派(delegate)和事件

委派是表示函数对象的 Windows 运行时类型。 你可以将委派与事件、回调和异步方法调用结
合起来,以指定稍后要执行的操作。 像函数对象一样,委派通过支持编译器验证返回类型和函
数的参数类型来提供类型安全。 委派的声明类似于函数签名、实现类似于类定义,而调用类似
于函数调用。

添加事件侦听器

你可以使用事件关键字声明指定的委派类型的公共成员。 客户端代码通过使用在特定语言中提
供的标准机制订阅事件。

public:
    event SomeHandler^ someEvent;

此示例使用与之前属性部分相同的 C++ 代码。

function Button_Click() {
    var nativeObj = new CppComponent.PropertyExample();
    // Define an event handler method
    var singlecasthandler = function (ev) {
        document.getElementById('P10').innerHTML = "The button was clicked and the value is " + ev;
    };

    // Subscribe to the event
    nativeObj.onpropertychangedevent = singlecasthandler;

    // Set the value of the property and fire the event
    var propValue = 21;
    nativeObj.propertyA = 2 * propValue;

}

在 .NET 语言中,订阅C++组件中的事件与订阅 .net 类中的事件相同:

//Subscribe to event and call method that causes it to be fired.
private void TestMethod()
{
    var objWithEvent = new CppComponent.PropertyExample();
    objWithEvent.PropertyChangedEvent += objWithEvent_PropertyChangedEvent;

    objWithEvent.PropertyA = 42;
}

//Event handler method
private void objWithEvent_PropertyChangedEvent(object __param0, int __param1)
{
    ResultText.Text = "the event was fired and the result is " +
         __param1.ToString();
}

为一个事件添加多个事件侦听器

JavaScript 具有支持多个处理程序订阅单一事件的 addEventListener 方法。

public delegate void SomeHandler(Platform::String^ str);

public ref class LangSample sealed
{
public:
    event SomeHandler^ someEvent;
    property Platform::String^ PropertyA;

    // Method that fires an event
    void FireEvent(Platform::String^ str)
    {
        someEvent(Platform::String::Concat(str, PropertyA->ToString()));
    }
    //...
};
// Add two event handlers
var multicast1 = function (ev) {
    document.getElementById('P11').innerHTML = "Handler 1: " + ev.target;
};
var multicast2 = function (ev) {
    document.getElementById('P12').innerHTML = "Handler 2: " + ev.target;
};

var nativeObject = new CppComponent.LangSample();
//Subscribe to the same event
nativeObject.addEventListener("someevent", multicast1);
nativeObject.addEventListener("someevent", multicast2);

nativeObject.propertyA = "42";

// This method should fire an event
nativeObject.fireEvent("The answer is ");

在 C# 中,任意数量的事件处理程序可以使用 += 运算符订阅事件,如之前示例所示。

枚举

/Cx 中C++的 Windows 运行时枚举是使用公共类枚举声明的;它类似于标准C++中的范围枚
举。

public enum class Direction {North, South, East, West};

public ref class EnumExampleClass sealed
{
public:
    property Direction CurrentDirection
    {
        Direction  get(){return m_direction; }
    }

private:
    Direction m_direction;
};

枚举值作为整数在C++/Cx 和 JavaScript 之间传递。 您可以选择性地声明一个
JavaScript 对象,该对象包含与C++/cx 枚举相同的命名值,并按如下所示使用该对象。

var Direction = { 0: "North", 1: "South", 2: "East", 3: "West" };
//. . .

var nativeObject = new CppComponent.EnumExampleClass();
var curDirection = nativeObject.currentDirection;
document.getElementById('P13').innerHTML =
Direction[curDirection];

C# 和 Visual Basic 均支持枚举语言。 这些语言会看到C++一个公共枚举类,就像它们会看到 .net 枚举一样。

异步方法

若要使用其他 Windows 运行时对象公开的异步方法,请使用任务类(并发运行时)。 有关详
细信息,请参阅任务并行度(并发运行时)。
若要在/cx 中C++实现异步方法,请使用 ppltasks.h 中定义的create_async函数。 有关详
细信息,请参阅在/cx 中C++为 UWP 应用创建异步操作。 有关示例,请参阅创建C++/cx
Windows 运行时组件并从 JavaScript 或C#调用该组件的演练。 .NET 语言使用C++/cx 异步
方法,就像它们是在 .net 中定义的任何异步方法一样。

Exceptions(异常)

你可以引发任何由 Windows 运行时定义的异常类型。 你无法从任何 Windows 运行时异常类
型中派生自定义类型。 但是,你可以引发 COMException 并提供可由捕获异常的代码访问的自
定义 HRESULT。 无法在 COMException 中指定自定义消息。

调试提示

在调试具有组件 DLL 的 JavaScript 解决方案时,你可以将调试器设置为在组件中支持单步
调试脚本或单步调试本机代码,但无法设置为同时进行。 若要更改设置,请在解决方案资源管
理器中选择 JavaScript 项目节点,然后选择“属性”、“调试”、“调试器类型”。

请确保在程序包设计器中选择相应的功能。 例如,如果你要尝试使用 Windows 运行时 API 打
开用户的“图片”库中的图像文件,请确保在清单设计器中的“功能”窗格中选中“图片库”复选框

如果 JavaScript 代码似乎无法识别组件中的公共属性或方法,请确保在 JavaScript 中使用
的是 Camel 大小写格式。 例如,在 JavaScript 中C++,LogCalc/cx 方法必须被引用为
LogCalc。

如果从解决方案中C++删除/cx Windows 运行时组件项目,还必须从 JavaScript 项目中手动
删除项目引用。 如果此操作无法完成,将阻止后续调试或生成操作。 如有必要,你可以稍后
向 DLL 添加程序集引用。


朝闻道,夕死可矣