用 C++ 创建 Windows 运行时组件
本文演示如何使用 C++ 创建 Windows 运行时组件,该组件是可从使用 JavaScript(或 C#、Visual Basic 或 C++)生成的 Windows 应用商店应用程序中调用的 DLL。生成此类组件的原因如下:
在复杂或计算密集型操作中发挥 C++ 的性能优势。
重用已编写并测试的代码。
在生成包含 JavaScript 或 .NET 项目和 Windows 运行时 组件项目的解决方案时,会将 JavaScript 项目文件和已编译的 DLL 合并到一个包中,以便在模拟器中进行本地调试或在受限设备上进行远程调试。也可以仅将该组件项目作为扩展 SDK 进行分发。有关更多信息,请参见如何:创建软件开发套件。
通常情况下,在对 C++ 组件进行编码时,可使用常规 C++ 库和内置类型,但抽象二进制接口 (ABI) 边界处的 C++ 库和内置类型除外,该处需与另一个 .winmd 包中的代码进行双向数据传递。然后,使用 Windows 运行时类型以及 Visual C++ 所支持的可用于创建和操作这些类型的特殊语法。此外,在 Visual C++ 代码中,使用 delegate 和 event 之类的类型实现可通过组件触发并在 JavaScript、Visual Basic 或 C# 中进行处理的事件。有关新 Visual C++ 语法的详细信息,请参见 Visual C++ 语言参考 (C++/CX)。
大小写和命名规则
JavaScript
JavaScript 区分大小写。因此,必须遵循以下大小写约定:
在引用 C++ 命名空间和类时,使用与 C++ 端相同的大小写。
即使方法名称在 C++ 端采用大写形式,在调用方法时,也使用 Camel 大小写格式。例如,C++ 方法必须作为 getDate() 从 GetDate() 中调用。
可激活的类名和命名空间名不能包含 UNICODE 字符。
.NET
.NET 语言遵循其常规大小写规则。
实例化对象
只有 Windows 运行时类型可以跨 ABI 边界传递。如果组件以 std::wstring 之类的类型作为公共方法中的返回类型或参数,编译器将引发错误。Visual C++ 组件扩展 (C++/CX) 内置类型包括常规标量(如 int 和 double),还有它们的 typedef 等效项 int32、float64 等。有关更多信息,请参见类型系统 (C++/CX)。
C++
// 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);
}
};
JavaScript
//Instantiation in JavaScript (requires "Add reference > Project reference")
var nativeObject = new CppComponent.SampleRefClass();
.NET
//Call a method and display result in a XAML TextBlock
var num = nativeObject.LogCalc(21.5);
ResultText.Text = num.ToString();
C++ 内置类型、库类型和 Windows 运行时类型
可激活类(又称为 ref 类)是可通过其他语言(如 JavaScript、C# 或 Visual Basic)进行实例化的类。若要通过其他语言使用,组件必须至少包含一个可激活类。
Windows 运行时组件可以包含多个公共可激活类以及只能在组件内部识别的其他类。所有公共类都必须位于与组件元数据文件相同的根命名空间中。将 [WebHostHidden] 特性应用于不应对 JavaScript 可见的 C++ 类型。
与类的情况一样,客户端代码使用 new(在 Visual Basic 中为 New)关键字创建组件的实例。
必须将可激活的类声明为 public ref class sealed。ref 类关键字告知编译器将该类创建为与 Windows 运行时兼容的类型,sealed 关键字规定不可继承该类。Windows 运行时 当前不支持通用继承模型;有限继承模型支持自定义 XAML 控件的创建。有关更多信息,请参见 Ref 类和结构 (C++/CX)。
对于 C++,所有数字基元都在默认命名空间中定义。Platform Namespace包含 Windows 运行时 类型系统特有的 C++ 类。这些类包括 Platform::String 类 和 Platform::Object 类。具体集合类型(如 Platform::Collections::Map 类 和 Platform::Collections::Vector 类)在 Platform::Collections 命名空间 中定义。这些类型实现的公共接口在 Windows::Foundation::Collections 命名空间 (C++/CX) 中定义。这些接口类型是由 JavaScript、C# 和 Visual Basic 使用的。有关更多信息,请参见类型系统 (C++/CX)。
返回内置类型的值的方法
C++
// #include <valarray>
public:
double LogCalc(double input)
{
// Use C++ standard library as usual.
return std::log(input);
}
JavaScript
//Call a method
var nativeObject = new CppComponent.SampleRefClass;
var num = nativeObject.logCalc(21.5);
document.getElementById('P2').innerHTML = num;
.NET
返回自定义值结构的方法
value struct 是可以包含默认情况下为公共的字段一个纯旧数据对象。value struct 通过值进行传递。
C++
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;}
}
};
}
JavaScript
若要跨 ABI 传递用户定义的值的结构,请定义一个与 C++ 中定义的值结构具有相同成员的 JavaScript 对象。然后,可将该对象作为参数传递给 C++ 方法,以便将其隐式转换为 C++ 类型。
// 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(未显示)。
C#
在 .NET 语言中,您仅创建在 C++ 元素中定义的类型的变量。
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++ 公共 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 语言就象在任何 .NET Framework 类中一样识 C++ 类中的重载。
DateTime
在 Windows 运行时 中,Windows::Foundation::DateTime 对象不过是一个表示在 1601 年 1 月 1 日之前或之后的 100 纳秒间隔数的 64 位带符号整数。没有针对 Windows:Foundation::DateTime 对象的方法。相反,每种语言按照对该语言是本机的方式来投影 DateTime:JavaScript 中的 Date 对象以及 .NET Framework 中的 System.DateTime 和 System.DateTimeOffset 类型。
C++
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;
}
};
JavaScript
在从 C++ 向 JavaScript 传递 DateTime 值时,JavaScript 接受该值作为 Date 对象,并默认将其显示为长型日期字符串。
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
当 .NET 语言将 System.DateTime 传递到 C++ 组件时,该方法将其接受为 Windows::Foundation::DateTime。当该组件将 Windows::Foundation::DateTime 传递到 .NET Framework 方法时,Framework 方法将其接受为 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();
}
集合和数组
集合始终作为句柄跨 ABI 边界传递给 Windows 运行时 类型,如 Windows::Foundation::Collections::IVector^ 和 Windows::Foundation::Collections::IMap^。例如,如果将句柄返回给 Platform::Collections::Map,它将隐式转换为 Windows::Foundation::Collections::IMap^。集合接口在不同于 C++ 类(可提供具体实现)的命名空间中定义。JavaScript 和 .NET 语言中使用该接口。有关更多信息,请参见集合 (C++/CX)和 Array 和 WriteOnlyArray (C++/CX)。
传递 IVector
C++
// 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;
}
JavaScript
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
.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
C++
// #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;
}
JavaScript
// 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
.NET 语言将 IMap 看作 IDictionary<K,V>。
private void GetDictionary()
{
var nativeObject = new CppComponent.CollectionExample();
IDictionary<int, string> d = nativeObject.GetMap();
ResultText.Text += d[2].ToString();
}
属性
Visual C++ 组件扩展 中的公共 ref 类使用 property 关键字可将公共数据成员作为属性公开。此概念与 .NET Framework 属性相同。trivial 属性与数据成员类似,因为它的功能都是隐式的。非 trivial 属性具有显式 get 和 set 访问器以及指定的私有变量,该变量是值的“后备存储”。在此示例中,私有成员 variable _propertyAValue 是 PropertyA 的后备存储。属性在其值更改后可能会引发事件,客户端应用程序在注册后可以接收该事件。
C++
//Properties
public delegate void PropertyChangedHandler(Platform::Object^ sender, int arg);
public ref class PropertyExample sealed
{
public:
PropertyExample(){}
// Event that is fired when PropetyA 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;
};
JavaScript
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
.NET 语言在本机 C++ 对象上访问属性就象在 .NET Framework 对象上一样。
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 运行时类型。可将委托与事件、回调及异步方法调用结合使用,以指定稍后将执行的操作。与函数对象一样,委托允许编译器验证函数的返回类型和参数类型,因此可提供类型安全。委托的声明类似于函数签名,实现类似于类定义,调用类似于函数调用。
添加事件侦听器
可以使用 event 关键字来声明指定委托类型的公共成员。客户端代码使用特定语言中提供的标准机制来订阅事件。
C++
public:
event SomeHandler^ someEvent;
此示例使用和前一个特性部分相同的 C++ 代码。
JavaScript
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
在 .NET 语言中,在 C++ 组件中订阅事件相当于在 .NET Framework 类中订阅事件:
//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 方法,令多个处理程序可仅订阅一个事件。
C++
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()));
}
//...
};
JavaScript
// 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 ");
.NET
在 C# 中,任意数量的事件处理程序可以使用 += 运算符订阅事件,如上例所示。
枚举
使用 public class enum 声明 C++ 中的 Windows 运行时枚举;此过程类似于标准 C++ 中的范围枚举。
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;
};
JavaScript
枚举值作为整数在 C++ 和 JavaScript 之间传递。你可以选择声明一个所包含的指定值与 C++ 枚举相同的 JavaScript 对象,并按如下方式使用它。
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];
.NET
C# 和 Visual Basic 都对枚举有语言支持。这些语言就象看待 .NET Framework 枚举一样看待 C++ 公共枚举类。
异步方法
若要使用其他 Windows 运行时对象公开的异步方法,请使用 task 类(并发运行时)。有关更多信息,请参见 Asychronous Programming in C++和任务并行(并发运行时)。
若要在 C++ 中实现异步方法,请使用在 ppltasks.h 中定义的 create_async 函数。有关更多信息,请参见用 C++ 为 Windows 应用商店应用程序创建异步操作。有关示例,请参见 演练:用 C++ 创建一个基本的 Windows 运行时组件,然后从 JavaScript 中调用该组件。.NET 语言就象使用在 .NET Framework 中定义的任何异步方法一样使用 C++ 异步方法。
异常
可以引发由 Windows 运行时定义的任何异常类型。不能从任何 Windows 运行时异常类型中派生自定义类型。但是,你可以引发 COMException 并提供可由捕获异常的代码访问的自定义 HRESULT。在 COMException 中无法指定自定义消息。
调试提示
在调试包含组件 DLL 的 JavaScript 解决方案时,可以对调试器进行设置,以启用逐句调试脚本或逐句调试组件中的本机代码,但不能同时启用两者。若要更改设置,请在**“解决方案资源管理器”中选择 JavaScript 项目节点,然后依次选择“属性”、“调试”、“调试器类型”**。
确保在包设计器中选择适当功能。例如,如果你尝试使用 Windows 运行时 API 打开文件,请确保在包设计器的**“功能”窗格中选中“文档库访问”**复选框。
如果 JavaScript 代码看起来无法识别组件中的公共属性或方法,请确保在 JavaScript 中使用 camel 大小写形式。例如,LogCalc C++ 方法在 JavaScript 中必须作为 logCalc 来引用。
如果从解决方案中删除 C++ Windows 运行时 组件项目,还必须从 JavaScript 项目中手动删除项目引用。否则,将无法执行后续的调试或生成操作。如果需要,随后可向 DLL 中添加程序集引用。
请参见
概念
演练:用 C++ 创建一个基本的 Windows 运行时组件,然后从 JavaScript 中调用该组件