Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

27 Dec 2019

再读MIDL3.0

[MIDL3.0][Microsoft Interface Definition Language 3.0 reference]可以说是搭配C++/WinRT而出品的。使用IDL这种中间语言来描述接口,而不是用C++或者其他语言的类型来描述接口,大概是为了实现接口的通用吧。每种语言都有一些和自己特性相关的数据类型,在其他语言中可能看不到。通过IDL来描述一个公共的集合,这样作成的模块让各个语言都有办法可以访问。

Introduction to Microsoft Interface Definition Language 3.0

这篇文档对MIDL3.0做了比较细致的介绍。MIDL是一种用于接口定义的语言,各种元素主要是用来描述接口。首先是接口这个类型,可以看出是一个函数指针集合,可以用来定义方法、事件以及部属(大都是通过函数的方式实现的吧)。

使用过面向对象编程的都知道,接口可以继承和组合,形成复合接口。这主要得益于虚函数表这种实现方式,通过在虚函数表移动指针,使其指向不同的部分,可以获得不同的接口集合(QueryInterface相关)。

runtimeclass和接口对象,可以看成是接口的一种实现,其实例可以被激活。和接口不同的是,runtimeclass不能像接口那样泛参数化;另外runtimeclass默认是sealed,不能派生子类。

MIDL3.0中有一类特别有意思的类型,叫做属性(attribute),主要目的是用来扩展接口的元数据,增加其描述能力:

[attributeusage(target_runtimeclass_member)]
attribute HelpAttribute
{
    HelpAttribute(String classUri);
    String ClassUri { get; };
    String MemberTopic { get; set; };
}

上面的属性Help可以给runtimeclass的成员加上帮助文档链接

[Help("https://docs.contoso.com/.../Widget")]
runtimeclass Widget
{
    [Help("https://docs.contoso.com/.../Widget", MemberTopic="Display")]
    void Display(String text);
}

通过花括号可以让多个成员共享一个属性:

runtimeclass Widget
{
    [Custom()]
    {
        void Display(String text);
        void Print();
        Single Rate;
    }
}

As shown in the first example, you use the [attributeusage()] attribute on your attribute definition. Valid target values are target_all, target_delegate, target_enum, target_event, target_field, target_interface, target_method, target_parameter, target_property, target_runtimeclass, target_runtimeclass_member, and target_struct. You can include multiple targets within the parentheses, separated by commas.

Other attributes you can apply to an attribute are [allowmultiple] and [attributename(“”)].

MIDL3.0中的其他类型主要是为了描述接口而服务的,不再赘述。

Assigning members to interfaces

说明如何从runtimeclass自动生成接口定义(包括runtimeclas自身,以及激活该runtimeclass的相应的工厂类)。看完可以加深对MIDL3.0中一切皆接口的理解。

Predefined attributes (MIDL 3.0)

[allowforweb]用来指示一个runtimeclass可以用于webview(不是很需要,如果这个webview具有all访问权限)

[constructor_name]用于显示指定一个runtimeclass的实例化工厂类型。

A factory interface is only applicable to a sealed class with non-default constructors, or to an unsealed class with default and/or non-default constructors.

例子:

[constructor_name("Windows.UI.Xaml.Documents.IBlockFactory", 07110532-4f59-4f3b-9ce5-25784c430507)]
...
unsealed runtimeclass Block : Windows.UI.Xaml.Documents.TextElement
{
    ...
    protected Block();
    ...
}

[contract]契约属性,WinRT用这个属性来管理其日益庞大的API数目。

每个WinRT API其实对应着ABI中的函数。ABI函数的名字必须唯一。可惜有时候一个API名字(构造函数或者重载的方法)可能需要多个ABI来对应,这时候就需要[method_name]来甄别。你也可以使用method_name属性来给一个方法起别名:

unsealed runtimeclass Block : Windows.UI.Xaml.Documents.TextElement
{
    ...
    [method_name("CreateInstance")] protected Block();
    ...
}

[static_name]可以用来指定和划分静态成员所属的接口,以便把不同的静态成员划分到不同的接口:

[static_name("Windows.UI.Xaml.Documents.IBlockStatics", f86a8c34-8d18-4c53-aebd-91e610a5e010)]
...
unsealed runtimeclass Block : Windows.UI.Xaml.Documents.TextElement
{
    ...
    static Windows.UI.Xaml.DependencyProperty LineHeightProperty{ get; };
    static Windows.UI.Xaml.DependencyProperty LineStackingStrategyProperty{ get; };
    static Windows.UI.Xaml.DependencyProperty MarginProperty{ get; };
    static Windows.UI.Xaml.DependencyProperty TextAlignmentProperty{ get; };

    [static_name("Windows.UI.Xaml.Documents.IBlockStatics2", af01a4d6-03e3-4cee-9b02-2bfc308b27a9)]
    {
        static Windows.UI.Xaml.DependencyProperty HorizontalTextAlignmentProperty{ get; };
    }
    ...
}

[interface_name]可以用来指定和划分成员所属的接口,以便把不同的成员划分到不同的接口:

[interface_name("Windows.UI.Xaml.Documents.IBlock", 4bce0016-dd47-4350-8cb0-e171600ac896)]
...
unsealed runtimeclass Block : Windows.UI.Xaml.Documents.TextElement
{
    ...
    Double LineHeight;
    Windows.UI.Xaml.LineStackingStrategy LineStackingStrategy;
    Windows.UI.Xaml.Thickness Margin;
    Windows.UI.Xaml.TextAlignment TextAlignment;

    [interface_name("Windows.UI.Xaml.Documents.IBlock2", 5ec7bdf3-1333-4a92-8318-6caedc12ef89)]
    {
        Windows.UI.Xaml.TextAlignment HorizontalTextAlignment;
    }
    ...
}

在默认不生成接口的情况下,interface_name还可以用来强制让runtimeclass生成接口:

[interface_name("Windows.UI.Xaml.IStateTriggerBase", 48b20698-af06-466c-8052-93666dde0e49)]
unsealed runtimeclass StateTriggerBase
{
    protected void SetActive(Boolean IsActive);
};

上面的StateTriggerBase不具备构造函数,不可以被激活,所以默认不生成构造函数?

也可以用[default_interface],而不是interface_name

[default_interface]
unsealed runtimeclass StateTriggerBase
{
    protected void SetActive(Boolean IsActive);
};

使用[default]可以将runtimeclass聚合的多个接口中的一个指定为默认接口:

// Declaring an external interface as the default
runtimeclass C : [default]I { ... }

// Declaring a specific exclusiveto interface as the default.
// This is very unusual.
runtimeclass C
{
    ...

    [default][interface_name(...)]
    {
        ...
    }
}

Advanced topics, and shorthand

如果使用Windows.Foundation.Collections命名空间中的泛参数化的类型,可以不用指定其命名空间,MIDL编译器会自动查找:

  • IIterable Windows.Foundation.Collections.IIterable
  • IIterator Windows.Foundation.Collections.IIterator
  • IKeyValuePair<K, V> Windows.Foundation.Collections.IKeyValuePair<K, V>
  • IMap<K, V> Windows.Foundation.Collections.IMap<K, V>
  • IMapChangedEventArgs Windows.Foundation.Collections.IMapChangedEventArgs
  • IMapView<K, V> Windows.Foundation.Collections.IMapView<K, V>
  • IObservableMap<K, V> Windows.Foundation.Collections.IObservableMap<K, V>
  • IObservableVector Windows.Foundation.Collections.IObservableVector
  • IVector Windows.Foundation.Collections.IVector
  • IVectorView Windows.Foundation.Collections.IVectorView
  • MapChangedEventHandler<K, V> Windows.Foundation.Collections.MapChangedEventHandler<K, V>
  • VectorChangedEventHandler Windows.Foundation.Collections.VectorChangedEventHandler

一个runtimeclass不用重新声明其所要实现的接口的方法,如果声明了和接口中同名的方法,那么这两个方法会被认为时不同的方法,在ABI中对应两个函数。

下面几个属性让你能够不使用MIDL3.0编译器默认生成的名字,而使用自定义的名字,这在维护既有接口,保证向后兼容性的时候有用武之地:

  • [interface_name("fully.qualified.name", UUID)]
  • [constructor_name("fully.qualified.name", UUID)]
  • [static_name("fully.qualified.name", UUID)]
  • [return_name("name")]
  • [method_name("name")]

如果不指定上面属性中的UUID,MIDL3.0编译器会帮助自动生成一个UUID。

如果使用了错误的属性,比如该使用constructor_name的时候使用了interface_name,MIDL不会报错。需要检查生成的结果。

对于没有成员的空runtimeclass,MIDL3.0编译器会报错。解决办法之一是给其加上[default_interface] 属性。对于没有成员的空接口,MIDL3.0编译器也会报错,解决办法之一是给其加上[uuid(...)]

/enum_class选项可以让MIDL3.0编译器生成局部化的enum (enum class),这种enum不可以用于公开类型。

你可以使用unsealed来让一个runtimeclass变得可以composable(被其他runtimeclass继承)。unsealed runtimeclass unsealed可以指示让这个类使用COM聚合(aggregation)。

Interpreting error messages包含对部分MIDL错误的解读。

一个委调(Delegate)如果返回HRESULT,那么可能存在歧义性,因为MIDL之前的版本会给原本返回void的委调使用HRESULT做返回值,用来传递异常。这种情况在MIDL3.0中不存在。所以MIDL编译器会判断是否采用MIDL3.0还是以往的方式来解读。如果编译器无法下判断,那么会退回旧有的解读方式。如果让要给委调返回HRESULT是你的真实意图,那么需要在MIDL3.0中使用下面的语法:

HRESULT AmbiguousDelegate(MyEnum e, [out, retval] HRESULT* result)

JavaScript映射对MIDL中的输出(out)参数有特殊处理,如果函数只有一个输出参数,并且返回值为void,那么JS的返回值就是这个参数的值;其他情况下,JS映射会从这个函数返回一个对象,这个对象的部属含有函数真实的返回值,以及一系列out输出参数。

Transition to MIDL 3.0 from classic MIDLRT

MIDLRT又叫做MIDL 2.0,参考MIDL 1.0, 2.0, and 3.0.。MIDL3.0和MIDLRT可以共存于同一个文件,但是如果没有必要的话,最好不要这么做。

Reserved keywords (MIDL 3.0)

这篇文档列举了MIDL3.0中的保留字(包括那些用在方括号中的保留字),还真是不少,有好几页之多。

Troubleshooting Microsoft Interface Definition Language 3.0 issues

对于用于COM的IDL,可以参考Microsoft Interface Definition Language

文档列举了一些错误排除相关的症状和消除手段。

其他

(完)

comments powered by Disqus