C++ [CX]第1部分[n]:一个简单类

看到了吗 C++ [CX]第0部分[N]简介 作为本系列的介绍。

null

在本文中,我们将通过查看一个简单的Windows运行时类来考虑C++的基本知识;我们将略过一些细节,但不要担心:我们会回来,并在未来的职位涵盖他们。本文中的代码是完整的,尽管为了简洁起见省略了一些名称空间限定;本文附带的VisualStudio解决方案包含C++和CX和WRL组件的完整代码,以及使用两者的简单测试应用程序。

好吧。下面是我们的简单课程:

  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运行时的一些基本原理。除了 public ref sealed 在类声明中,这个类看起来就像普通C++类。这是一件好事:我们的C++/CX代码应该看起来类似于类似的C++代码,但是应该和我们(编译器)有很大的不同。你能把它认出来是不同的。

那么,这个类是密封的,公共的,ref类的真正含义是什么呢?A ref class 是一个 引用类型 . Windows运行时构建在COM之上,Windows运行时引用类型实际上是COM类类型。每当我们与引用类型对象交互时,我们都会通过指向引用类型实现的接口的指针间接地(通过引用)进行交互。因为我们从不按值与引用类型交互,所以它的实现是不透明的:只要它的现有接口保持不变,就可以修改它的实现,而不会影响依赖它的其他组件(可以添加新功能,但不能删除)。

A public 类型在定义它的组件外部可见,其他组件可能引用并使用此类型。类型也可以是 private (默认值);私有类型只能从定义它的组件中引用。C++或CX组件的Windows元数据文件只包含公共类型的元数据。对私有类型的限制比对公共类型的限制少,因为私有类型不会出现在元数据中,因此不受Windows运行时的某些规则的约束。

这个 sealed 只指定该类不能用作基类。大多数公共Windows运行时类型都是密封的(当前,唯一可以密封的公共类型是从 Windows::UI::Xaml::DependencyObject ; 这些类型由XAML应用程序使用)。

这个 “REF类和结构(C++/CX)” 前面提到的Visual C++语言中的页有助于其他有关引用类型的有趣事实的综述。

接口

我在上面说过,每当我们与引用类型对象交互时,我们都是通过引用类型实现的接口进行交互的。你可能已经注意到它看起来像我们的 Number 类不实现任何接口:它根本没有基类列表。谢天谢地,编译器将在这里帮助我们 Number 上课不是没用的。

编译器将自动为我们的类生成一个名为 __INumberPublicNonVirtuals . 顾名思义,这个接口声明了我们的 Number 类型。如果我们在C++中使用这个定义,它看起来会是这样:

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

这个 Number 然后转换类以声明此接口。如果我们的类声明了任何新的公共虚拟成员(即,不重写基类虚拟成员函数)或任何虚拟或非虚拟受保护的成员,编译器也会为这些成员生成接口。

请注意,这些自动生成的接口仅声明尚未由类显式实现的接口声明的成员。例如,如果我们声明了另一个接口:

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

定义了我们的 Number 类实现此接口时,自动生成 __INumberPublicNonVirtuals 只会定义 SetValue 成员函数。

错误处理

在现代C++代码中,异常通常用于错误报告(不总是,但它们应该是默认的)。函数应直接返回其结果(作为返回值),并在发生故障时抛出异常。但是,异常不能跨不同的语言和运行时移植;异常处理机制变化很大。即使在C++代码异常中也会有问题,因为不同的编译器可能会不同地实现异常。

由于异常在不同语言之间不能很好地工作,所以Windows运行时不使用它们;相反,每个函数都返回一个错误代码(HRESULT),指示成功或失败。如果函数需要返回值,则通过out参数返回该值。这与COM使用的约定相同。在错误代码和该语言的自然错误处理设施之间进行翻译取决于每种语言的投影。

捕获和重新引用异常会带来开销,但即使在涉及用不同语言编写的组件之间交互的简单场景中,这种好处也是显而易见的。例如,考虑C++中调用C++中的组件中的函数的函数。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运行时函数在x86上使用stdcall调用约定,因此 __stdcall 需要批注。默认情况下,非变量成员函数通常使用thiscall调用约定(调用约定注释只在x86上起作用;x64和ARM都有一个单独的调用约定。)

我们已经命名了包装器函数 __abi_GetValue ; 这是编译器给它生成的接口上的函数的名称;在类中,它使用了一个长得多的名称和许多下划线,以确保它不会与任何用户声明的函数或从其他类或接口继承的函数冲突。函数名在运行时并不重要,因此函数名并不重要。在运行时,通过vtable lookup调用函数,由于编译器正在生成包装函数,因此它知道要将哪些函数指针放入vtables中。

为我们的 SetValue 函数遵循相同的模式,但没有添加out参数,因为它不返回值。

一个简单的类,没有C++ +CX

到目前为止,我们已经有足够的信息来实现我们的目标 Number 使用WRL和C++,不使用C++ +CX类。

当使用C++ +CX时,C++编译器将生成组件的Windows元数据(WINMD)文件和定义所有类型的DLL。当我们不使用C++或CX时,我们需要使用IDL来定义需要在元数据中使用的任何东西和使用。 中型轻轨 从IDL生成C++头文件和Windows元数据文件。对于那些有COM经验的人来说,这应该是非常熟悉的。

首先,我们需要为 Number 类型。这相当于 __INumberPublicNonVirtuals 当我们使用C++时,编译器会自动生成的接口。在这里,我们将只命名我们的接口 INumber :

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

我们的 INumber 接口派生自 IInspectable 接口。这是所有Windows运行时接口派生的基本接口;在C++中,每个接口都隐含地从 IInspectable .

我们还需要定义 Number 在IDL文件中初始化自身,因为它是公共的,因此需要在元数据文件中结束:

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

这个 activatable 这里使用的属性指定这个类是默认可构造的。对象构造如何工作的细节将在以后的文章中讨论。就目前而言,知道这一点就足够了 activatable 属性将在元数据文件中生成所需的元数据以报告 Number 类作为默认可构造的。

这就是IDL文件中所需要的全部内容。如果将IDL文件生成的WIMD文件与我们的C++/CX组件所生成的WiMD文件进行比较,您会发现它们基本相同:接口具有不同的名称,并且在C++ /CX组件中应用了一些额外的属性,但它们在其他方面是相同的。

    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运行时类所需的许多平凡、重复的工作。这个 RuntimeClass 类模板将类将实现的一组接口作为其参数,并提供 IInspectable 接口成员函数。这个 InspectableClass 宏以类的名称和类的信任级别作为参数;这些是实现 IInspectable 接口。

根据上面的讨论,两个成员函数的定义与预期一致:而不是直接使用 __stdcall 修饰符,我们使用 STDMETHODCALLTYPE ,扩展到 __stdcall 当使用VisualC++和Windows头文件时,如果使用不同的编译器,可以将其更改为其他内容。你也可以用 STDMETHOD 宏。

最后,因为我们的 Number 类型是默认可构造的,并且由于默认构造函数是公共的,这意味着其他组件可以创建此类的实例,因此我们需要实现使其他组件能够调用我们类的构造函数所需的逻辑。这涉及到实现一个工厂类和注册工厂,以便我们可以在被请求时返回工厂实例。这里需要做大量的工作,但是由于我们只有一个默认的构造函数,所以我们可以简单地使用 ActivatableClass WRL宏:

    ActivatableClass(Number)

就这样!WRL组件需要的代码是C++的三倍以上,但这只是一个简单的小组件。随着事情变得越来越复杂,当我们使用Windows运行时的更多特性时,我们将看到基于WRL的代码比基于C++的CX代码更快速地发展,变得更加复杂。

在下一篇文章中,我们将讨论帽子( ^ )在以后的文章中,我们将详细介绍构造、异常处理和其他有趣的主题。

ASimpleClassSolution.zip文件

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享