看过类型是如何定义在CLR和ILAsm中的,让我们进入下一个问题:这些类型以及它们的派生是如何被分配到程序项——字段、变量等等。定义程序项类型的结果的结构被称为这些项的签名。签名创建于编码引用到各种各样的类和值类型;我将在本章详细讨论签名。
CLR中的基本类型
所有的类型必须要被定义于某处。Microsoft .NET Framework类库定义了上百个类型,而且其它程序集基于这个类库中的类型生成了它们自己的类型。一些定义在这个类库中的类型被CLR认为是基本类型并且在签名中给出特殊的编码。这样做只是为了性能——理论上,签名只能创建于类型符号,假设每个类型都定义在某处并因此有一个符号。但是解析所有这些类型就会轻易发现它们引用了很多琐碎的项诸如一个4字节整数或一个布尔值,几乎不能将其认为是一种在运行时的明智的工作方式。
基本数据类型
术语“基本数据类型”涉及到了定义在.NET Framework类库的类型,这个类型给出了特定而且独立使用在签名中的类型编码。因为所有这些类型都定义在Mscorlib程序集中并且都属于System命名空间,在为一个类型提供类库的类型名称的时候,我省略了前缀[mscorlib]System。
独立的类型代码定义在头文件CorHdr.h的CorElementType枚举中。所有这些代码的名称开始于ELEMENT_TYPE_元素,而我在本章将这个元素省略或者简写为E_T_。
表8-1描述了基本数据类型和它们相应的ILAsm符号。
表8-1定义在CLR中的基本数据类型
编码 |
常量名称 |
.NET Framework类型名称 |
ILAsm符号 |
注释 |
0x01 |
VOID |
Void |
void |
|
0x02 |
BOOLEAN |
Boolean |
bool |
单字节值,true=1,false=0 |
0x03 |
CHAR |
Char |
char |
2字节无符号整数,表示一个Unicode字符 |
0x04 |
I1 |
Sbyte |
int8 |
1字节有符号整数,与C/C++中的char相同 |
0x05 |
U1 |
Byte |
unsigned int8 |
1字节无符号整数 |
0x06 |
I2 |
Int16 |
int16 |
2字节有符号整数 |
0x07 |
U2 |
UInt16 |
unsigned int16 |
2字节无符号整数 |
0x08 |
I4 |
Int32 |
int32 |
4字节有符号整数 |
0x09 |
U4 |
UInt32 |
unsigned int32 |
4字节无符号整数 |
0x0A |
I8 |
Int64 |
int64 |
8字节有符号整数 |
0x0B |
U8 |
UInt64 |
unsigned int64 |
8字节无符号整数 |
0x0C |
R4 |
Single |
float32 |
4字节浮点数 |
0x0D |
R8 |
Double |
float64 |
8字节浮点数 |
0x16 |
TYPRDBYREF |
TypeReference |
typeref |
类型引用,携带了指向一个类型的引用和识别这个被引用类型的信息 |
0x18 |
I |
IntPtr |
native int |
指针大小的整数值;大小取决于目标平台,解释了关键字native的用途 |
0x19 |
U |
UIntPtr |
native unsigned int |
指针大小的无符号整数值 |
数据指针类型
CLR将指向分配在垃圾收集堆上的对象(称为对象引用;参见“类的表示”)开始位置的指针与其它指针区别开。
在CLR中定义了两种数据指针类型:托管指针和非托管指针。它们的区别是托管指针是由CLR的垃圾收集子系统管理的,并且即使在垃圾收集过程期间,被引用的项在内存上移动,托管指针仍然保持有效的;然而,非托管指针只有与“不可移动的”项结合才能被安全地使用。
这两种指针类型必须紧跟在这些指针指向的引用类型之后。正如类型由引用类型所构造,指针没有相应的定义在.NET Framework类库的类型并且不可以被装箱。表8-2描述了两个指针类型以及它们的ILAsm符号。它们都没有一个.NET Framework关联的类型。
表8-2定义在CLR中的指针类型
编码 |
常量名称 |
ILAsm符号 |
注释 |
0x0F |
PTR |
<type>* |
指向<type>的非托管指针 |
0x10 |
BYREF |
<type>& |
指向<type>的托管指针 |
注意:虽然ILAsm符号将指针字符放在了被指向的类型之后,在签名E_T_PTR和E_T_BYREF中总是位于引用类型引用类型之前。
这两种类型的指针受标准指针算法的支配:可以从一个指针中加上或减去一个整数值,产生一个新指针;而一个指针可以从另一个指针中减去,产生一个一个整数值。在C/C++和IL的指针算法之间的区别是,在IL中——并由此在ILAsm中(参见清单8-2)——指针的增加和减少总是以字节指定,不管该指针所表示的项的大小。
清单8
通过同样的符号......现在,这仅仅是一个普通的表达式。我并不是指一个元数据符号。(我想在本书中,我最好格外小心像“通过同样的符号”和“应用程序的符号”这样的术语。)同样地,IL中这两个指针的delta总是以字节——而不是以这个项所指向的——所表示。
在IL中使用非托管的指针并不被认为是安全的(可验证的)。因为没有限制的使用C风格的指针算法允许任何人访问任何东西,IL代码,解除了对非托管的指针,被认为是不可验证的,并且在代码来自一个可信任的源(例如一个本地驱动器)的时候可以运行。
托管指针是听话的、接受管理的指针,为CLR类型控制和垃圾收集子系统所完全拥有。这些指针只能用在一个很小的范围中,下面介绍了这个范围的边界:
l 托管指针总是指向一个现有的项——一个字段、一个元素数组、一个本地变量,或一个方法参数。
l 元素数组和字段不能带有托管指针类型。
l 托管指针类型只能用于方法特性——本地变量、参数或返回类型,而且所有这些项都与栈相关联并不是一个简单的事情。
l 指向“托管内存”(垃圾收集堆,包括了对象实例盒数组)的托管指针可以被转换为非托管指针。
l 不指向“托管内存”的托管指针可以被转换为非托管指针,但是这样的转换生成了不可信任的IL代码。
l 一个托管指针的基础类型不可以是另一个指针,但是它可以是一个对象引用。
托管指针是不同于对象引用的。在第7章中,描述了值类型的装箱和拆箱,你看到它使用了装箱来为一个值类型创建一个对象引用。使用一个简单的引用——就是说,一个托管的指针——并不是足够的。
区别在于一个对象引用指向一个对象的开始(方法表),然而一个托管指针指向这个对象的内部——这个项的值(数据)部分。当你得到一个指向一个值类型实例的托管指针时,你会寻址这个数据部分。你可以只得到这些,因为值类型的实例,不是作为对象,并没有方法表。
当你装箱一个值类型的实例时,你创建了一个对象,一个类的实例,包括复制自这个值类型实例的自身方法表和数据部分。这个对象由一个对象引用所表示。
函数指针类型
第7章简要描述了托管函数指针的使用并将它们与委托类型比较。托管函数指针通过E_T_FNPTR类型所表示,由值0x1B指出并且没有关联到.NET Framework的一个类型。
就如同一个数据指针类型,函数指针类型是一个并不单独存在的结构化类型,并且必须紧跟在它所指向的方法的完整签名。(托管方法在本章稍后讨论;参加“签名”。)
对于一个方法函数的ILAsm符号如下所示:
method <call_conv> <return_type> * (<type>[,<type>*])
这里<call_conv>是一个调用约定,<return_type>是一个返回类型,而括号中的<type>序列是一个参数列表。你将在“签名”章节发现更多细节。
向量和数组
CLR承认两种类型的数组:向量和多维数组,正如表8-3所描述的。向量是一个一维的下限为0的数组。多维数组,我将其称为arrays,可以有大于1的维数以及非0的下限。这两种类型都是结构化类型,所以它们都没有关联到.NET Framework的一个类型。
表8-3 CLR支持的数组
编码 |
常量名称 |
ILAsm符号 |
注释 |
0x1D |
SZARRAY |
<type>[ ] |
<type>向量 |
0x14 |
ARRAY |
<type>[<bounds>[,<bounds>*]] |
<type>数组 |
所有的向量都是派生于抽象类[mscorlib]System.Array的对象(类的实例)。
向量的编码是简单的:基础类型的编码紧跟在E_T_SZARRAY之后,它可以是除了void之外的任意值。向量的大小是编码的一部分。由于数组和向量都是对象引用,所以仅仅声明一个数组是不足够的——而必须创建它的一个实例,要为向量使用指令newarr,或者调用一个数组构造函数。在这一点上就是说向量或数组实例的大小要详细指出。因此,一个数组的大小是一个数组实例的特性,而不是这个数组类型的特性。
数组编码是更加高级的:
E_T_ARRAY<underlying_type><rank><num_sizes><size1>...<sizeN>
<num_lower_bounds><lower_bound1>...<lower_boundM>
这里下面的描述是正确的:
<underlying_type> 不可以是void
<rank> 是数组维数的数字。(K>0)
<num_sizes> 是一个指定了维数大小的数字(N <= K)
<sizen>是一个无符号的指定了大小的整数(n = 1,...,N)
<num_lower_bounds>是一个指定了下限的数字(M <= K)
<lower_boundm> 是一个有符号的指定了下限的整数(m = 1,...,M)。
在前面所有的无符号整数值,都是根据第5章讨论的长度压缩公式进行压缩。为了节省你向后翻三章的时间,我将在表8-4中重复这个公式。
表8-4 无符号整数的长度压缩公式
值的范围 |
压缩大小 |
压缩值(Big Endian) |
0-0x7F |
1字节 |
<value> |
0x80-0x3FFF |
2字节 |
0x8000|<value> |
0x4000-0x1FFFFFFF |
4字节 |
0xC0000000|<value> |
有符号整数值(下限值)根据一种不同的压缩过程来进行压缩。首先这个有符号的整数被编码为一个无符号的整数:获取原始整数的绝对值,左移1位,并根据原始值的最重要的位来设置最不重要的位。然后压缩根据表8-4显示的工具被应用。
如果一个维数的大小和/或下限被指定,它们不是被假定为0;而是被标记为没有被指定。大小和下限的规范不可以有“漏洞”——就是说,如果你有一个5维数组并想指定它的第三维的大小(或下限),你必须还要指定第一维和第二维的大小(或下限)
ILAsm中的一个数组规范就像这样:
下面是一个示例:
int32[..., ...] // 二维数组,未定义下限和大小
int32[2...5] //一维数组,下限为2并且大小为4
int32[0..., 0...] // 二维数组,下限为0并且未定义大小
如果在多维数组的声明中,对于一个维度,既没有指定它的下限也没有指定它的上限,省略符号就可以被忽略掉。从而,int32[…, …]和int32[ , ]表示同样的意思:一个二维的数组,而没有指定下限和大小。
可是,这种等价在一维数组的情形中并不工作。int32[]符号表示一个向量(<E_T_SZARRAY><E_T_I4>),而int32[…]表示一个维度为1、下限和大小都没有定义的数组(<E_T_ARRAY><E_T_I4><1><0><0>)。
CLR将多维数组和向量的向量视为完全不同的。int32[ , ]和int32[ … ]的规格,导致了不同的编码类型,以不同的方式创建,并且在创建的时候布局也是不一样的:
int32[ , ]:这个规格的编码为<E_T_ARRAY><E_T_I4><2><0><0>,由一个对数组构造函数的单独调用所创建,并被排列为一个连续的二维int32数组。
int32[ … , … ]:这个规格的编码为<E_T_SZARRAY><E_T_SZARRAY><E_T_I4>,由一系列newarr指令所创建,并被排列为一个向量的向量的引用,每一个指向了一个连续的int32向量,不能保证关于每个向量的位置。向量的向量经常用于描述交错数组(jagged arrays),在第二个维度的大小改变对第一维索引的依赖时。
修饰符
在表8-5描述了4中内嵌的CLR类型编码,并不代表任何特定的数据或指针类型,而是用作数据或指针类型的修饰符。这些类型都没有关联到.NET Microsoft的类型。
表8-5 定义在CLR中的修饰符
编码 |
常量名称 |
ILAsm符号 |
注释 |
0x1F |
CMOD_REQD |
modreq(<class_ref>) |
必须的自定义修饰符 |
0x20 |
ARRAY |
modopt(<class_ref>) |
可选的自定义修饰符 |
0x41 |
SENTINEL |
… |
一个vararg方法的调用中可选参数的开始 |
0x45 |
PINNED |
pinned |
将一个本地变量标记为垃圾收集不可移动的 |
修饰符modreq和modopt指出当前项关联到什么——参数、返回类型或者字段,例如,必须以特殊的方式处理。这些修饰符紧跟在TypeDef或TypeRef之后,并且对应到这些符号的类指出了处理当前项的特殊方式。
紧跟在modreq和modopt之后的符号根据下面的算法进行压缩。正如你可能记得的,一个未编码的(外部的)元数据符号是一个4字节无符号整数,在其高位字节上是这个符号的类型而在其3个低位字节上是一个RID。碰巧这些符号出现在签名中并因此需要的压缩仅由3种类型组成:TypeDef、TypeRef或TypeSpec。(参见本章后面的“签名”获取更多TypeSpec的信息。)因为如此,只有2位,而不是一个完整的字节,是符号类型所需要的:00表示TypeDef,01用于TypeRef,10指定了TypeSpec。符号压缩的过程类似于用于压缩有符号整数的过程:这个符号的RID部分是左移两位,而2位类型的编码则放置在最不重要的位上。这个压缩的结果仅仅是用于无符号整数,根据表8-4所示的公式。
修饰符modreq和modopt主要由除了CLR之外的工具使用,例如编译器或程序分析器。modreq修饰符指出这个修饰符必须要被考虑在内,尽管modopt指出这个标志符是可选的并且可以忽略的。ILAsm编译器出于它的内部意图而没有使用这些修饰符。
由CLR识别的modreq和modopt修饰符的唯一用途是,这些修饰符被应用于方法的返回类型或参数的时候,这些方法从事于托管或非托管的封送。例如,为了指定一个托管方法必须有cdecl的调用约定,当它被封送为非托管的,你可以使用下面的关联到方法的返回类型的符号:
modopt ([mscorlib]System.Runtime.CompilerServices.CallConvCdecl)
当使用于托管或非托管的封送的上下文中的时候,modreq和modopt修饰符是等效的。
虽然modreq和modopt修饰符对它们所关联的托管类型的项没有任何影响,但是带有修饰符的签名和不带修饰符的签名被认为是不同的。同样适用于只在由这些修饰符引用的类中不同的签名。这就允许,例如,具有int和long类型参数的方法重载。在C++中,int和long是两种不同的类型,但是对于CLR来说它们是一样的——32位有符号证书(E_T_I4)。因此为了区别这两种类型,C++编译器发布long为modopt([mscorlib]System.Runtime.CompilerServices.IsLong)int32。另一个由C++编译器经常使用的修饰符是modopt([mscorlib]System.Runtime.CompilerServices.IsConst),为了区别,例如,C类型的int*和const int*。自定义修饰符被引进到C++类型系统,但是它们不是特定于C++的。其它高级别语言可能也需要区别某些类型,从CLR的观点看是不可分辨的。
Sentinel修饰符在第1章介绍过,在我们分析声明和调用带有一个变量长度的参数列表(vararg方法)的方法时。Sentinel表示了提供给vararg方法调用的可选参数的开始。这个修饰符只可以出现在下面一种上下文中:在调用站点,因为vararg方法的可选参数在这样一个方法被声明时还没有被指定。CLR将出现在其它任意上下文中的sentinel视为一个错误。在调用站点的方法参数只能包括一个sentinel,而且只有提供了可选参数,才可以使用sentinel。
Pinned修饰符只适用于本地变量。它的使用意味着由本地变量引用的对象不可以被垃圾收集重新部署并且必须贯穿方法执行是原位不动的。如果一个代表了对象引用的本地变量或者一个托管指针是“pinned”,那么将其转换为一个非托管的指针并且取消这个非托管指针的引用就是安全的,因为非托管指针在被取消引用得时候,仍然被保证为有效的。(它在取消引用的情况下是安全的,但它仍然是不可信任的,正如一个非托管指针的用法):
本地类型
当托管代码调用非托管的方法或者将托管字段暴露为非托管代码时,提供关于托管类型应该如何分组为非托管类型并且和如何得到非托管类型的分组,有时是非常必要的。非托管类型由CLR所识别,也被称为“本地的”(native),并列表在CorHdr.h中的枚举CorNativeType里。所有在这个枚举中的常量都具有以_NATIVE_TYPE_*开头的名称,出于这次讨论的意图,我省略了名称的这些部分或者将它们简写为N_T_。同样的常量也列表在.NET Framework类库的枚举System.Runtime.InteropServices.UnmanagedType中。
一些本地类型被废弃了并且被CLR互操作子系统所忽略。但是这些本地类型没有全部过期,ILAsm一定有表示它们的方式——并且由于ILAsm表示这些类型,我不能不把过期的类型和其它的一起列出来,正如你在表8-6中看到的所有类型。
表8-6 定义在CLR中的本地类型
编码 |
常量名称 |
.NET Framework类名 |
ILAsm符号 |
注释 |
0x01 |
VOID |
|
void |
过期的并且因此不应该再使用;由IlAsm识别,但是被CLR互操作子系统忽略 |
0x02 |
BOOLEAN |
Bool |
bool |
4字节布尔值;true=nonzero,false=0 |
0x03 |
I1 |
I1 |
int8 |
有符号1字节整数 |
0x04 |
U1 |
U1 |
unsigned int8,uint8 |
无符号1字节整数 |
0x05 |
I2 |
I2 |
int16 |
有符号2字节整数 |
0x06 |
U2 |
U2 |
unsigned int16,uint16 |
无符号2字节整数 |
0x07 |
I4 |
I4 |
int32 |
有符号4字节整数 |
0x08 |
U4 |
U4 |
unsigned int32,uint32 |
无符号4字节整数 |
0x09 |
I8 |
I8 |
int64 |
有符号8字节整数 |
0x0A |
U8 |
U8 |
unsigned int64,uint64 |
无符号8字节整数 |
0x0B |
R4 |
R4 |
float32 |
4字节浮点型 |
0x0C |
R8 |
R8 |
float64 |
4字节浮点型 |
0x0D |
SYSCHAR |
|
syschar |
过期的 |
0x0E |
VARIANT |
|
VARIANT |
过期的 |
0x0F |
CURRENCY |
Currency |
currency |
货币值 |
0x10 |
PTR |
|
* |
过期的;使用native int |
0x11 |
DECIMAL |
|
decimal |
过期的 |
0x12 |
DATE |
|
date |
过期的 |
0x13 |
BSTR |
Bstr |
bstr |
Visual Basic风格的Unicode字符串,在COM互操作中使用 |
0x14 |
LPSTR |
LPStr |
lpstr |
指向一个0休止符的ANSI字符串 |
0x15 |
LPWSTR |
LPSWStr |
lpwstr |
指向一个0休止符的Unicode字符串 |
0x16 |
LPTSTR |
LPTStr |
lptstr |
指向一个0休止符的ANSI或Unicode字符串 |
0x17 |
FIXEDSYSSTRING |
ByValTStr |
fixed sysstring[<size>] |
具有<size>字节固定大小的系统字符串 |
0x18 |
OBJECTREF |
|
objectref |
过期的 |
0x19 |
IUNKNOWN |
IUnknown |
iunknown |
Iunknown接口指针 |
0x1A |
IDISPATCH |
IDispatch |
idispatch |
IDispatch接口指针 |
0x1B |
STRYCT |
Struct |
struct |
C风格的结构,用于封送格式化的托管类型 |
0x1C |
INTF |
Interface |
interface |
接口指针 |
0x1D |
SAFEARRAY |
SafeArray |
safearray <variant_type> |
<variant_type>类型的安全数组 |
0x1E |
FIXEDARRAY |
ByValArray |
fixed array[<size>] |
具有<size>字节固定大小的数组 |
0x1F |
INT |
IntPtr |
int |
有符号指针大小的整数 |
0x20 |
UINT |
UIntPtr |
unsigned int,uint |
无符号指针大小的整数 |
0x21 |
NESTEDSTRUCT |
nested struct |
过期的;使用struct |
|
0x22 |
BYVALSTR |
VBByRefStr |
byvalstr |
在固定长度缓冲中的Visual Basic风格的字符串 |
0x23 |
ANSIBSTR |
AnsiBStr |
ansi bstr |
Visual Basic风格的ANSI字符串 |
0x24 |
TBSTR |
RBSTr |
tbstr |
bstr或ansi bstr,依赖于平台 |
0x25 |
VARIANTBOOL |
VariantBool |
variant bool |
2字节布尔值;true=-1,false=0 |
0x26 |
FUNC |
FunctionPtr |
method |
函数指针 |
0x28 |
ASANY |
AsAny |
as any |
对象;在CLR定义的类型 |
0x2A |
ARRAY |
LPArray |
<n_type>[<sizes>] |
有本地类型<n_type>组成的固定大小的数组 |
0x2B |
LPSTRUCT |
LPStruct |
lpstruct |
指向一个C风格的结构 |
0x2C |
CUSTOMMARSHALER |
CustomMarshaler |
custom(<class_str>,<cookie_str>) |
自定义封送 |
0x2D |
ERROR |
Error |
error |
映射int32到VT_HRESULT |
ILAsm符号中的<sizes>参数符号用于N_T_ARRAY,显示在表8-6中,可以是空或者可以被格式化为<size> + <size_param_number>:
如果<size>为空,本地数组的大小派生于被封送的托管数组的大小。
<sizes>参数指定了在数组项中的本地数组大小。基于0的方法参数数量< size_param_number >指定了哪些数组指定了本地数组的大小。本地数组的大小是<size>加上由方法参数<size_param_number>指定的额外大小。
一个自定义的封送声明(如表8-6所示)有两个参数,它们都是带引号的字符串。<class_str>参数是表示自定义封送的类名称,使用了字符串约定Reflection.Emit。<cookie_str>参数是一个在运行时传递到自定义封送的参数字符串(cookie)。这个字符串标志了所需封送的形式,而且它的符号是特点于自定义封送机制的。
变量类型
变量类型(在COM很流行)定义在Wtypes.h文件中的VARENUM枚举里,它是和Microsoft Visual Studio一起分布的。并不是所有的变量类型都适于成为安全数组类型,根据Wtypes.h,但是ILAsm仍然为所有的类型提供了符号,正如表8-7所示。这看起来有点奇怪,考虑到出现在ILAsm中的变量类型只是在安全数组规范的上下文中,但是我们不应该忘记ILAsm主要的应用程序是测试程序的生成,这包括了已知的,程序错误。
表8-7 定义在CLR中的变量类型
编码 |
常量名称 |
适用于安全数组吗? |
ILAsm符号 |
0x00 |
VT_EMPTY |
否 |
<empty> |
0x01 |
VT_NULL |
否 |
null |
0x02 |
VT_I2 |
是 |
int16 |
0x03 |
VT_I4 |
是 |
int32 |
0x04 |
VT_R4 |
是 |
float32 |
0x05 |
VT_R8 |
是 |
float64 |
0x06 |
VT_CY |
是 |
currency |
0x07 |
VT_DATE |
是 |
date |
0x08 |
VT_BSTR |
是 |
bstr |
0x09 |
VT_DISPATCH |
是 |
idispatch |
0x0A |
VT_ERROR |
是 |
error |
0x0B |
VT_BOOL |
是 |
bool |
0x0C |
VT_VARIANT |
是 |
variant |
0x0D |
VT_UNKNOWN |
是 |
iunknown |
0x0E |
VT_DECIMAL |
是 |
decimal |
0x10 |
VT_I1 |
是 |
int8 |
0x11 |
VT_UI1 |
是 |
unsigned int8, uint8 |
0x12 |
VT_UI2 |
是 |
unsigned int16, uint16 |
0x13 |
VT_UI4 |
是 |
unsigned int32, uint32 |
0x14 |
VT_I8 |
否 |
int64 |
0x15 |
VT_UI8 |
否 |
unsigned int64, uint64 |
0x16 |
VT_INT |
是 |
int |
0x17 |
VT_UINT |
是 |
unsigned int, uint |
0x18 |
VT_VOID |
否 |
void |
0x19 |
VT_HRESULT |
否 |
hresult |
0x1A |
VT_PTR |
否 |
* |
0x1B |
VT_SAFEARRAY |
否 |
safearray |
0x1C |
VT_CARRAY |
否 |
carray |
0x1D |
VT_USERDEFINED |
否 |
userdefined |
0x1E |
VT_LPSTR |
否 |
lpstr |
0x1F |
VT_LPWSTR |
否 |
lpwstr |
0x24 |
VT_RECORD |
是 |
record |
0x40 |
VT_FILETIME |
否 |
filetime |
0x41 |
VT_BLOB |
否 |
blob |
0x42 |
VT_STREAM |
否 |
stream |
0x43 |
VT_STORAGE |
否 |
storage |
0x44 |
VT_STREAMED_OBJECT |
否 |
streamed_object |
0x45 |
VT_STORED_OBJECT |
否 |
stored_object |
0x46 |
VT_BLOB_OBJECT |
否 |
blob_object |
0x47 |
VT_CF |
否 |
cf |
0x48 |
VT_CLSID |
否 |
clsid |
0x1000 |
VT_VECTOR |
是 |
<v_type>vector |
0x2000 |
VT_ARRAY |
是 |
<v_type> [ ] |
0x4000 |
VT_BYREF |
是 |
<v_type> & |