Windows Store apps开发[26]C++/CX Part 1 of [n]: 一个简单的类

来源:互联网 发布:2016真实挂机赚钱软件 编辑:程序博客网 时间:2024/05/06 03:16

注:本文由BeyondVincent(破船)原创首发

        转载请注明出处:BeyondVincent(破船)@DevDiv.com

请看上一篇文章,是关于本次系列的一个介绍:C++/CX Part 0 of [n]: C++/CX简介


    在这篇文章中,我将通过观察一个简单的Windows Runtime类来研究C++/CX的基础内容;我会忽略一些细节,但是别担心:在后面的博文中,我会回来继续进行介绍。本文中的代码是完整的,为了阅读方便,一些名称空间被忽略了;本文最后会给出一个Visual Studio解决方案,包含C++/CX和WRL模块的完整代码,以及一个测试程序。

    Alright。下面是一个简单的类:

public ref class Number sealed{public:    Number() : _value(0) { }    int GetValue()           { return _value;  }    void SetValue(int value) { _value = value; }private:    int _value;}

    实际上,这个类不能做什么事,但是这足以演示C++/CX和Windows Runtime的基本原理。在类的声明中,除了public ref和sealed,这个类看起来完全像一个普通的C++类。这个一个非常好的事情:我们的C++/CX代码看起来应该类似于C++代码,但是应该有足够的不同,以便于我们(以及编译器)尅区分出它的不同之处。

    那么,这个类中sealed,public,ref class的真正含义是什么呢?

    ref class是一个引用类型。Windows Runtime是在COM之上构建的,并且一个Windows Runtime引用类型是一个有效的COM类类型。我们每次与一个引用类型对象交互时,都是间接进行的(使用引用),通过一个指针指向由引用类型实现的一个接口。因此,我们永远都不能通过值与引用类型进行交互,它(接口)的实现是不透明的:只要已存在的接口保持不变,它的实现部分的修改,并不会影响到依赖于这个接口的其它组件(可以添加新功能,但是不能移除已有的功能)。

    定义类的public类型是为了让外部可见,其它类可能会引用或者使用这个类型。同样可以定义为private(默认的类型);如果定义为private,那么只有被该类内部可以引用。C++/CX创建的Metadata文件只能包含public类型的metadata。private类型的限制要比public类型少,因为private类型不出现在metadata中,因此不受Windows Runtime的一些规则约束。

    sealed是简单的指定该类不可以被作为基类。大多数Windows Runtime类型都是sealed的(目前,public类型中不是sealed的仅有继承自Windows::UI::Xaml::DependencyObject;这些类型用在XAML程序中)。

    在Visual C++ Language Reference 的“Ref classes and structs(C++/CX)”中对引用类型有很好的介绍。


接口

    上面已经说过,每次与引用类型对象交互时,都是通过引用类型实现的接口进行的。你可能已经注意到上面的Number类并没有实现任何接口:没有任何的基类。不过,幸运的是,编译器在这里会帮助我们进行一些处理,让Number类变得有用。

    编译器会自动的为该类生成一个名称为__INumberPublicNonVirtuals的接口。正如名字所暗示的,这个接口声明了Number类的所有public和nonvirtual成员函数。如果我们用C++/CX自行定义这个接口的话,看起来类似如下代码:

public interface struct __INumberPublicNonVirtuals{    int GetValue() = 0;    void SetValue(int) = 0;};

Number类之后会转换声明的这个接口。如果我们定义的类声明了新的public virtual成员(例如,不是override基类的virtual 成员函数)或任意的virtual,再或nonvirtual protected 成员,编译器将会为这些成员生成相应的接口。

    注意,这些自动生成的接口仅仅是这样的成员函数:在类的实现中的成员函数,没有被声明为接口。因此,如果我在这里声明了另外的一个接口:

public interface struct IGetNumberValue{    int GetValue() = 0;};

并且,在定义Number类时实现了这个接口,那么在自动生成的__INumberPublicNonVirtuals 中仅仅定义了SetValue成员函数。


错误处理

    在现代C++代码中,异常(exceptions)通常用于错误报告(也不全是,但是默认情况下应该是这样的)。一个函数希望能直接返回一个结果(以值的方式),如果发生了失败,那么会抛出一个异常。然而,异常不兼容不同的语言和运行时;针对不同的语言和运行时异常处理的机制有很大的区别。即使在C++代码里面,异常的兼容性也可能会出现问题,因为不同的编译器实现的异常也可能会不同。

    在夸越不同的语言之间时,异常不能很好的工作,因而Windows Runtime并没有采用异常;取而代之的是每一个函数返回一个错误码(一个HRESULT)来表示成功或者失败。如果一个函数需要返回一个值,那么值的返回需要通过out参数。这与COM机制有点类似。这取决于每一种语言在映射错误码与此语言对应的处理机制有关It's up to each language projection to translate between error codes and the natural error handling facility for that language。)。

    在捕获和重新抛出异常时,会有一定的开销,但是及时在一个简单的场景(不同语言写的模块进行交互时)优点也是很明显的。仔细想想,例如,在一个C#组件中的某个函数调用一个用C++/CX写的组件中的函数,C++代码可以通过继承Platform::Exception来抛出一个异常,如果异常没有被处理,那么在ABI边界处会被转换为一个HRESULT,并将该结果返回给C#代码。CLR接着会将错误的HRESULT转换至一个托管异常,该异常会被抛出,相应的C#代码会将其捕获。

    在这两个组件--C#组件和C++组件间,有非常重要的一个事,错误的处理都很自然的使用了异常,及时它们使用的异常处理机制不相同。在后面的博文中,我会详细介绍在C++/CX边界会得到什么样的异常转换,以及转换是如何发生的(这个主题比较复杂,需要进行单独介绍)。就现在来说,我们只需要知道转换是自动发生的,就够了。


成员函数

在这里我特意将Number类写的非常的简单:GetValue和SetValue都没有抛出异常,调用这两个函数都总会是成功的。因此我使用它们来演示编译器是如何将我们的C++/CX成员函数转换为真实的ABI函数。GetValue要更加有趣点,因为它会返回一个值,所以我们来看看它吧:

int GetValue(){    return _value;}

编译器会使用正确的ABI签名来生成一个新的函数;例如,

HRESULT __stdcall __abi_GetValue(int* result){   // Error handling expressly omitted for exposition purposes   *result = GetValue();   return S_OK;}

上面封装的这个函数有一个out参数,该参数通过添加到参数列表以返回值,这个函数实际的返回值变为了HRESULT。Windows Runtime函数使用stdcall调用x86上面的约定(convention),因此__stdcall是必须的。默认情况下,不可变参数的成员函数通常使用__stdcall来调用约定。(约定的调用只发生在x86上;x64和ARM都有独自的调用约定:注:搞不懂作者在这里说的什么意思

    在这里,我将封装的函数命名为__abi_GetValue;这个名字是编译器针对接口给该函数生成的。在类里面,使用很长的名字,并有一些下划线,是为了确保不跟用户声明的函数或者从其它类or接口继承来的函数有冲突。函数的名字在运行时是没有关系的,因此把函数命名为什么,不会有关系的。在运行时,函数是通过查找虚函数表(vtalble)进行调用的,因此编译器知道其生成的封装函数的指针位于虚函数表中的位置。

    我们的SetValue函数封装也会根据类似的模式生成,会有异常处理,但是没有out参数,因为该函数没有返回值。


一个简单的类(没有C++/CX)

    我们已经讨论了好多内容了,现在我们已经有足够多的信息通过使用C++,并在WRL的帮助下(不使用C++/CX)来实现我们的Number类。

    当使用C++/CX时,C++编译器会生成Windows Metadata(WinMD)文件,以及定义所有类型的DLL。当不使用C++/CX时,我们需要使用IDL定义所有的东西,包括在metadata中的结束处理,使用midlrt生成一个C++头文件,以及从IDL生成Windows Metadata文件。如果对COM比较熟悉的话,那么会感觉这非常相似。

    首先需要为Number类型定义一个接口。这个接口相当于使用C++/CX时,编译器自动生成的__INumberPublicNonVirtuals接口。在这里,我将接口名称命名为INumber:

[exclusiveto(Number)][uuid(5b197688-2f57-4d01-92cd-a888f10dcd90)][version(1.0)]interface INumber : IInspectable{    HRESULT GetValue([out, retval] INT32* value);    HRESULT SetValue([in] INT32 value);}

    INumber接口继承自IInspectable接口。所有的Windows Runtime接口都需要继承自该基类接口;在C++/CX中,每一个接口都隐含的继承自IInspectable。

    我们同样需要在IDL文件中定义Number类,由于该类是public的,因此需要在metadata文件中结束(end up):

[activatable(1.0)][version(1.0)]runtimeclass Number{    [default] interface INumber;}

    在这里指定activatable属性说明该类是缺省构造的。一个对象的构造详情我会在后面的博文中介绍。现在,只需要知道activatable属性会引起在在metadata文件中生成需要的元数据(metadata),以表明Number类是缺省构造的。

    上面就是IDL文件中的所需内容。如果你将IDL文件与C++/CX生成的WinMD文件进行对比,会发现它们几乎是相同的:除了接口名称不同,以及在C++/CX模块中有一些额外的属性(用于类型)外,其它地方都是相同的。


    在C++中类的定义是简单的:

    

class Number : public RuntimeClass<INumber>{    InspectableClass(RuntimeClass_WRLNumberComponent_Number, BaseTrust)public:    Number() : _value(0) { }    virtual HRESULT STDMETHODCALLTYPE GetValue(INT32* value) override    {        *value = _value;        return S_OK;    }    virtual HRESULT STDMETHODCALLTYPE SetValue(INT32 value) override    {        _value = value;        return S_OK;    }private:    INT32 _value;};

    上面的RuntimeClass类模板和InspectableClass宏都来自WRL:它们一起处理许多普通的、重复性的工作,以实现一个Windows Runtime类。RuntimeClass类模板以一组接口作为它的参数,该组接口是Number类将要实现的,以及为IInspectable接口成员函数提供一个缺省的实现。InspectableClass宏将类的名称和类的信任级别作为参数;这是实现IInspectable接口所需要的。

    两个成员函数的定义跟预想的一样,上面已经讨论过:使用STDMETHODCALLTYPE代替直接使用__stdcall修饰符,当使用Visual C++和Windows头时涉及到__stdcall,但是当使用不同的编译器时,这个不能扩展到别的一些东西。当然,这里同样可以使用STDMETHOD宏。

    最后,由于我们的Number类是缺省构造的,而缺省构造函数是public的,也就是说其它组件可以创建该类的一个实例,所以我们需要在逻辑上做一些实现,让别的组件可以调用我们这个类的构造函数。这包括实现一个工厂类,以及注册该工厂类,这样我们才可以在被调用时返回一个工厂的实例。这里就需要有一些工作需要处理。但是我们只有一个缺省构造函数,因此我们可以简单的使用WRL的InspectableClass宏:

ActivatableClass(Number)

    好了,上面就是所有内容了!可以看出,关于代码部分,WRL组件需要的时间至少是C++/CX组件的三倍,并且这仅仅是一个小的,简单的模块。如果事情变得更加的复杂,那时我们也需要使用Windows Runtime的更多特性,那么我们也会看到基于WRL的代码增长速度和复杂度都要比基于C++/CX代码快得多。

    在下一篇文章中,我将详细的讨论hats(^),并且在后面的一些文章中,我会详细的介绍构造、异常处理,以及其它一些感兴趣的主题。


ASimpleClassSolution.zip

译者(BeyondVincent(破船))注:

本文从开始翻译到结束翻译大约持续了一周,我通过多次的阅读原文,想完全理解作者的意图,但是很遗憾,有个别地方,还是不能理解,如果你看到这篇文章,感觉不对的地方,希望指正!谢谢!!!期间多次想放弃本文的翻译【有些地方实在是太难以理解了,部分原因要归于我对COM、WRL等的不了解】,就怕翻译出来的不对,误导读者,还好,坚持下来了,收获也不小!

原文在这里:

C++/CX Part 1 of [n]: A Simple Class

原创粉丝点击