Chapter 7

image

Namespaces and Classes

As earlier chapters have discussed, the common language runtime computational model is inherently object-oriented. The concept of class—or, to use more precise runtime terminology, the concept of a type—is the central principle around which the entire computational model is organized. The type of an item—a variable, a constant, a parameter, and so on—defines both data representation and the behavioral features of the item. Hence, one type can be substituted for another only if both these aspects are equivalent for both types. For instance, a derived type can be interpreted as the type from which it is derived.

The Ecma International/ISO standard specification of the common language infrastructure divides types into value types and reference types, depending on whether an item type represents a data item itself or a reference (an address or a location indicator) to a data item.

Reference types include object types, interface types, and pointer types. Object types—classes—are types of self-describing values, either complete or partial. Types with partial self-describing values are called abstract classes. Interface types are always types of partial self-describing values. Interfaces usually represent subsets of behavioral features exposed by classes; a class is said to implement the respective interface. Pointer types are simply references to items, indicating item locations.

This is what the Ecma International/ISO specification says, and I am not going to argue the fine points of the theory, such as why classes and interfaces are self-describing and value types are not or why the way of passing the items between functional units—by value or by reference—all of a sudden becomes the inherent attribute of the items themselves.

The common language runtime object model supports only single type inheritance, and multiple inheritance is simulated through the implementation of one or more interfaces. As a result, the runtime object model is absolutely hierarchical, with the System.Object class at the root of the tree (see Figure 7-1). Interface types, however, are not part of the type hierarchy because they are inherently incomplete and have no implementation of their own.

9781430267614_Fig07-01.jpg

Figure 7-1. The common language runtime type hierarchy

The interfaces play an interesting role: they serve as promissory notes of a class. When class X is derived from class Y, X inherits all members of Y, so inheritance directly affects the structure of the derived class. But when you say that class X implements interface IY, you promise only that class X will expose all the methods described in IY, which might be viewed as a constraint imposed on class X. Class X does not inherit anything from the interface IY it implements, except a “debt” of implementing the methods of IY.

All types (except interfaces) are derived eventually from System.Object. This chapter examines types and their declarations, dividing the types into five categories: classes, interfaces, value types, enumerations, and delegates. These categories are not mutually exclusive (for example, delegates are classes, and enumerations are value types) but the types of each category have distinct features.

Class Metadata

From a structural point of view, all five categories of types have identical metadata representations. Thus, we can talk about class metadata, or type metadata, in a general sense.

Class metadata is grouped around two distinct concepts: type definition (TypeDef) and type reference (TypeRef). TypeDefs and related metadata describe the types declared in the current module, whereas TypeRefs describe references to types that are declared somewhere else. Since it obviously takes more information to adequately define a type than to refer to one already defined, TypeDefs and related metadata are far more complex than TypeRefs.

When defining a type, you should supply the following information:

  • The full name of the type being defined
  • Flags indicating special features the type should have
  • The type from which this type is derived
  • The interfaces this type implements
  • How the loader should lay out this type in memory
  • Whether this type is nested in another type—and if so, in which one
  • Where fields and methods of this type (if any) can be found

When referencing a type, only its name and resolution scope need be specified. The resolution scope indicates where the definition of the referenced type can be found: in this module, in another module of this assembly, or in another assembly. In the case of referencing the nested types, the resolution scope is another TypeRef.

Figure 7-2 shows the metadata tables that engage in type definition and referencing but not the tables related to the identification of type members—fields and methods, for example, and their attributes. The arrows denote cross-table referencing by means of metadata tokens. In the following sections, you’ll have a look at all the metadata tables involved.

9781430267614_Fig07-02.jpg

Figure 7-2. Metadata tables that engage in type definition and referencing

I must point out that three tables in the lower part of Figure 7-2 (TypeSpec, GenericParam, and GenericParamConstraint) and their associated links have entered the picture (no pun intended) in version 2.0. They are related to generic types and will be discussed in Chapter 11.

TypeDef Metadata Table

The TypeDef table is the main table containing type definition information. Each record in this table has six entries:

  • Flags (4-byte unsigned integer): Binary flags indicating special features of the type. The TypeDef flags are numerous and important, so this chapter discusses them separately; see “Class Attributes.”
  • Name (offset in the #Strings stream): The name of the type. This entry must not be empty. Remember class Odd.or.Even from Chapter 1? Odd.or.Even was its full name. The Name of that class was Even—part of the full name to the right of the rightmost dot.
  • Namespace (offset in the #Strings stream): The namespace of the type, part of the full name to the left of the rightmost dot. Class Odd.or.Even from Chapter 1 had Namespace Odd.or. The Namespace entry can be empty, if the full name of the class does not contain dots. The namespace and the name constitute the full name of the type.
  • Extends (coded token of type TypeDefOrRef): A token of the type’s parent—that is, of the type from which this type is derived. This entry must be set to 0 for all interfaces and for one class, the type hierarchy root class System.Object. For all other types, this entry should carry a valid reference to the TypeDef, TypeRef, or TypeSpec table. The TypeSpec table can be referenced only if the parent type is an instantiation of a generic type (see Chapter 11).
  • FieldList (record index [RID] in the Field table). An index to the Field table, marking the start of the field records belonging to this type.
  • MethodList (RID in the Method table). An index to the Method table, marking the start of the method records belonging to this type.

TypeRef Metadata Table

The TypeRef metadata table has a much simpler structure than the TypeDef table because it needs to carry only data necessary to identify the referenced type unambiguously, so the CLR loader could resolve the reference at execution time. Each record in this table has three entries:

  • ResolutionScope (coded token of type ResolutionScope): An indicator of the location of the type definition. This entry is set to 0 if the referenced type is defined somewhere in the current assembly or to 4 (compressed token 1—the Module token) if the referenced type is defined in the same module. Besides these two rather special cases, in general ResolutionScope can be a token referencing the ModuleRef table if the type is defined in another module of the same assembly, a token referencing the AssemblyRef table if the type is defined in another assembly, or a token referencing the TypeRef table if the type is nested in another type. Having TypeRefs for the types defined in the same module does not constitute a metadata error, but it is redundant and should be avoided if possible.
  • Name (offset in the #Strings stream). The name of the referenced type. This entry must not be empty.
  • Namespace (offset in the #Strings stream). The namespace of the referenced type. This entry can be empty. The namespace and the name constitute the full name of the type.

InterfaceImpl Metadata Table

If the defined type implements one or several interfaces, the corresponding TypeDef record is referenced by one or several records of the InterfaceImpl metadata table. This table serves as a lookup table (describing not some metadata entities but rather relations between entities described in other tables), providing information about “what is implementing what,” and it is ordered by implementing type. The InterfaceImpl table has only two entries in each record:

  • Class (RID in the TypeDef table). An index in the TypeDef table, indicating the implementing type.
  • Interface (coded token of type TypeDefOrRef). A token of the implemented type, which can reside in the TypeDef, TypeRef, or TypeSpec table. The TypeSpec table can be referenced only if the implemented interface is an instantiation of a generic interface (see Chapter 11). The implemented type must be marked as an interface.

NestedClass Metadata Table

If the defined type is nested in another type, its TypeDef record is referenced in another lookup table: the NestedClass metadata table. (For more information about nesting, see “Nested Types” later in this chapter.) Like the InterfaceImpl table, the NestedClass table is a lookup table, and records of which describe some “links” between other tables. Being a lookup table, the NestedClass table has only two entries per record:

  • NestedClass (RID in the TypeDef table). An index of the nested type (the nestee).
  • EnclosingClass (RID in the TypeDef table). An index of the type in which the current type is nested (the encloser, or nester).

Since types of both entries are RIDs in the TypeDef table, the nested type and its encloser cannot be defined in different modules or assemblies.

ClassLayout Metadata Table

Usually, the loader has its own ideas about how to lay out the type being loaded: it may add fillers between the fields of the class for alignment, or even shuffle the fields. Certain types, however, must be laid out in a specific manner (for example, suppose you want to introduce a value type describing a COFF header, which has a very definite structure and layout, or you want to create such a simple thing as a union), and they carry metadata information regarding these specifics.

The ClassLayout metadata table provides additional information about the packing order and total size of the type. In Chapter 1, for example, when I declared a “placeholder” type without any internal structure, I used such additional information—the total size of the type.

A record in the ClassLayout metadata table has three entries:

  • PackingSize (2-byte unsigned integer): The alignment factor in bytes. This entry must be set to 0 or to a power of 2, from 1 to 128. If this entry is not zero, its value will be used as the alignment factor for fields instead of a “natural” alignment characteristic of the field types (“natural” alignment usually coincides with the size of the type or nearest greater power of 2). For example, if PackingSize is set to 2, and you have two fields (a byte and a pointer) then your layout will include a byte (first field), another byte (filler), and a pointer; the pointer in this case will be 2-byte aligned, which is a bad thing on almost all processor architectures. If, however, the PackingSize value is greater than the “natural” alignment of a field, the “natural” alignment is used; if, for example, PackingSize is set to 2, and you have 2 one-byte fields, then your layout will include just 2 bytes (first field, second field) without any filler between them.
  • ClassSize (4-byte unsigned integer): The total requested layout size of the type. If the type has instance fields and the summary size of these fields, aligned by PackingSize, is different from ClassSize, the loader allocates the larger of the two sizes for the type.
  • Parent (RID in the TypeDef table): An index of the type definition record to which this layout belongs. The ClassLayout table should not contain any duplicate records with the same Parent entry value.

Namespace and Full Class Name

It is time to talk seriously about names in the common language runtime and ILAsm. So far, in Chapter 6, you’ve encountered only names that were in fact filenames and hence had to conform to well-known filename conventions. From now on, however, you’ll need to deal with names in general, so it is important to know the rules.

ILAsm Naming Conventions

Names in ILAsm are either simple or composite. Composite names are composed of simple names and special connection symbols such as a dot. For example, System and Object are simple names, and System.Object is a composite name. The length of either kind of name in ILAsm is not limited syntactically, but metadata rules impose certain limitations on the name length.

The simplest form of a simple name is an identifier, which in ILAsm must begin with an alphabetic character or one of the following characters:

#, $, @, _

and continue with alphanumeric characters or one of the following:

?, $, @, _, `

(the last symbol is not an apostrophe; it is a backtick).

These are examples of valid ILAsm identifiers:

  • Object
  • _Never_Say_Never_Again_
  • men@work
  • GType`1

image Caution  One obvious limitation on ILAsm identifiers is that an ILAsm identifier must not match any of the (rather numerous) ILAsm keywords.

The common language runtime accepts a wide variety of names with very few limitations. Certain names—for example, .ctor (an instance constructor), .cctor (a class constructor, a.k.a. type initializer), and _Deleted* (a metadata item marked for deletion during an edit-and-continue session)—are reserved for internal use by the runtime. Generally, however, the runtime is liberal about names. As long as a name serves its purpose (identifying a metadata item unambiguously) and cannot be misinterpreted, it is perfectly fine. This liberalism, of course, includes names beginning with wrong (from the ILAsm point of view) symbols and names continuing with wrong symbols, not to mention the names that happen to match ILAsm keywords.

To cover this variety, ILAsm offers an alternative way to present a simple name: as a single-quoted literal. For example, these are valid ILAsm simple names:

  • '123'
  • 'Space Between'
  • '&%!'

One of the most frequently encountered kinds of composite names is the dotted name, a name composed of simple names separated by a dot:

<dotted_name> ::= <simple_name>[.<simple_name>*]

Examples of dotted names include the following:

  • System.Object
  • '123'.'456'.'789'
  • Foo.Bar.'&%!'

Namespaces

Simply put, namespaces are the common prefixes of the full names of classes. The full name of a class is a dotted name; the last simple name it contains is the class name, and the rest is the namespace of the class.

It takes longer, perhaps, to explain what namespaces are not. Namespaces are not metadata items—they do not have an associated metadata table, and they cannot be referenced by tokens. Namespaces also have no direct bearing on assemblies. The name of an assembly might or might not match in full or in part the namespace(s) used in the assembly. One assembly might use several namespaces, and the same namespace can be used in different assemblies (an assembly using a namespace means an assembly defining classes with names belonging to this namespace).

So why does the metadata model even bother with namespaces and class names instead of simply using the full class names? The answer is simple: economy of space. Let’s suppose you define two classes with the full names Foo.Bar and Foo.Baz. Since the names are different, in the full-name model you would have to store two full names in the string heap: Foo.BarFoo.Baz. But if you split the full names into namespaces and names, you need to store only FooBarBaz. This is quite a difference when you consider the number of possible classes.

Namespaces in ILAsm are declared in the following way:

.namespace MyNamespace
{
   ...
   // Classes declared here
   // Have full name "MyNamespace.<simple_name>"
}

Namespaces can be nested, as shown here:

.namespace MyNamespace
{
   ...
   // Classes declared here
   // Have full name "MyNamespace.<simple_name>"
   .namespace X
   {
     ...
     // Classes declared here
     // Have full name "MyNamespace.X.<simple_name>"
   }
}

Namespaces can also be unnested. This is how the IL disassembler versions 1.0 and 1.1 used to represent namespaces in the disassembly text:

.namespace MyNamespace
{
   ...
   // Classes declared here
   // Have full name "MyNamespace.<simple_name>"
}
.namespace MyNamespace.X
{
   ...
   // Classes declared here
   // Have full name "MyNamespace.X.<simple_name>"
}

In versions 2.0 and later, it is recommended that you use full class names instead of the specification of namespaces, and the IL disassembler version 2.0+ follows this pattern. The .namespace directive is still recognized by the IL assembler for backward-compatibility reasons.

Full Class Names

As the preceding section explained, a full class name in a general case is a dotted name, composed of the class’s namespace and the name of the class. The loader resolves class references by their full names and resolution scopes, so the general rule is that no classes with identical full names must be defined in the same module. For multimodule assemblies, an additional (less strict) rule prohibits defining public classes (classes visible outside the assembly) with identical full names in the same assembly.

In ILAsm, a class is always referenced by its full name, even if it is referenced from within the same namespace. This makes class referencing context independent.

ILAsm v1.0 and v1.1 did not allow dotted names as class names, but you could bypass this restriction by quoting the dotted name, thus turning it into a simple name and avoiding a syntax error, like so:

.namespace X
{
   .class public'Y.Z'
   {
      ...
   }
}

And a class is always referenced by its full name, so a class with a dotted name will not pose any resolution problems (it will be referenced as X.Y.Z anyway), and the module will compile and work. But if you disassemble the module, you’ll find that the left part of the dotted name of the class has migrated to the namespace, courtesy of the metadata emission API.

.namespace X.Y
{
   .class public Z
   {
      ...
   }
}

Although this is not what you intended, it has no dire consequences—just a case of mild confusion. If you know and expect this effect and don’t get confused that easily, you can even forgo the namespace declarations altogether and declare classes by their full names, to match the way they are referenced.

.class public'X.Y.Z'
{
   ...
}

That’s exactly how it is done in ILAsm v2.0+, only without single quotes around the full class name, because ILAsm v2.0+ allows dotted names as class or method names.

The reason for switching from the namespace/name model of class declaration to the full-name model in ILAsm v2.0+ is twofold. First, this way the classes are declared and referenced uniformly by their full names. Second, this resolves the problem of naming the nested classes: if namespace A contains declaration of class B, which contains declaration of nested class C, what is the full name of the nested class? A.C? A.B.C? (Actually, it’s C, because the encloser’s namespace has nothing to do with the nested class’s namespace).

The common language runtime imposes a limitation on the full class name length, specifying that it should not exceed 1,023 bytes in UTF-8 encoding. The ILAsm compiler, however, does not enforce this limitation. Single quotes, should they be used for simple names in ILAsm, are a purely lexical tool and don’t make it to the metadata; thus, they don’t contribute to the total length of the full class name.

Class Attributes

An earlier section (“Class Metadata”) listed the various pieces of information included in a type definition. In the simplest case, when only the TypeDef metadata table is involved, the ILAsm syntax for a type definition is as follows:

.class<flags> <dotted_name> extends<class_ref> {
      ...
}

The <dotted_name> value specified in the .class directive defines the TypeDef’s Namespace and Name entries, <class_ref> specified in the extends clause defines the Extends entry, and <flags> defines the Flags entry.

Flags

The numerous TypeDef flags can be divided into several groups, as described here.

  • Visibility flags (binary mask 0x00000007)
    • private (0x00000000): This type is not visible outside the assembly. This is the default.
    • public (0x00000001): This type is visible outside the assembly.
    • nested public (0x00000002): This nested type has public visibility.
    • nested private (0x00000003): This nested type has private visibility; it is not visible outside the enclosing class.
    • nested family (0x00000004): This nested type has family visibility—that is, it is visible to descendants of the enclosing class only.
    • nested assembly (0x00000005): This nested type is visible within the assembly only.
    • nested famandassem (0x00000006): This nested type is visible to the descendants of the enclosing class residing in the same assembly.
    • nested famorassem (0x00000007): This nested type is visible to the descendants of the enclosing class either within or outside the assembly and to every type within the assembly with no regard to “lineage.”
  • Layout flags (binary mask 0x00000018)
    • auto (0x00000000): The type fields are laid out automatically, at the loader’s discretion. This is the default.
    • sequential (0x00000008): The loader shall preserve the order of the instance fields.
    • explicit (0x00000010): This type layout is specified explicitly, and the loader shall follow it. (See Chapter 9 for information about field declaration.)
  • Type semantics flags (binary mask 0x000005A0)
    • interface (0x00000020): This type is an interface. If this flag is not specified, the type is presumed to be a class or a value type; if this flag is specified, the default parent (the class that is assumed to be the parent if the extends clause is not specified, usually [mscorlib]System.Object) is set to nil.
    • abstract (0x00000080): This class is abstract—for example, it has abstract member methods. As such, this class cannot be instantiated and can be used only as a parent of another type or types. This flag is invalid for value types.
    • sealed (0x00000100): No types can be derived from this type. All value types and enumerations must carry this flag.
    • specialname (0x00000400): This type has a special name. How special it is depends on the name itself. This flag indicates to the metadata API and the loader that the name has a meaning in which they might be interested—for instance, _Deleted*.
  • Type implementation flags (binary mask 0x00103000)
    • import (0x00001000): This type (a class or an interface) is imported from a COM type library.
    • serializable (0x00002000): This type can be serialized into sequential data by the serializer provided in the Microsoft .NET Framework class library.
    • windowsruntime (0x00004000): This type is a Windows Runtime type (see Chapter 18 for details). This flag was introduced in v4.5.
    • beforefieldinit (0x00100000): This type can be initialized (its .cctor run) any time before the first access to a static field. If this flag is not set, the type is initialized before the first access to one of its static fields or methods or before the first instantiation of the type. I discuss this flag and its effect on type initialization in more detail in Chapter 10.
  • String formatting flags (binary mask 0x00030000)
    • ansi (0x00000000): When interoperating with native methods, the managed strings are by default marshaled to and from ANSI strings. Managed strings are instances of the System.String class defined in the .NET Framework class library. Marshaling is a general term for data conversion on the managed and unmanaged code boundaries. (See Chapter 18 for detailed information.) String formatting flags specify only default marshaling and are irrelevant when marshaling is explicitly specified. This flag, ansi, is the default flag for a class and hence represents a “default default” string marshaling.
    • unicode (0x00010000): By default, managed strings are marshaled to and from Unicode (UTF-16).
    • autochar (0x00020000): The default string marshaling is defined by the underlying platform.
  • Reserved flags (binary mask 0x00040800)
    • rtspecialname (0x00000800): The name is reserved by the common language runtime and has a special meaning. This flag is legal only in combination with the specialname flag. The keyword rtspecialname has no effect in ILAsm and is provided for informational purposes only. The IL disassembler uses this keyword to show the presence of this reserved flag. Reserved flags cannot be set at will; this flag, for example, is set automatically by the metadata emission API when it emits an item with the specialname flag set and the name recognized as specific to the common language runtime, for example .ctor or .cctor.
    • <no keyword> (0x00040000): This type has declarative security metadata associated with it. This flag is set by the metadata emission API when respective declarative security metadata is emitted.
  • Semantics pseudoflags (no binary mask): These are not true binary flags that define the Flags entry of a TypeDef record but rather are lexical pseudoflags modifying the default parent of the class.
    • value: This type is a value type. The default parent is System.ValueType.
    • enum: This type is an enumeration. The default parent is System.Enum.

Class Visibility and Friend Assemblies

Flag public means that the class is visible and can be referenced outside the assembly where it is declared. Flag private means the opposite, so probably a more proper name for this flag would be assembly. Starting with the version 2.0 of the common language runtime, it is possible to declare certain assemblies “friends” of the current assembly by using custom attribute System.Runtime.CompilerServices.InternalsVisibleToAttribute. If assembly A declares assembly B as its “friend,” then all classes and members inside A that have assemblywide visibility and accessibility become visible and accessible to assembly B. At the same time, these classes and members remain invisible and inaccessible to other assemblies.

There are significant differences between “friend” assemblies of the managed world and friend classes and functions of unmanaged C++. First, in the managed world the granularity of friendship does not go below the assembly level, while in unmanaged C++ the friendship is defined at the class or function level. Second, in unmanaged C++ a friend class or method has full access to all members of this class, including private members, while in the managed world a friend assembly has access only to internal (assemblywide) classes and members but not to private or protected ones.

Class References

The nonterminal symbol <class_ref> in the extends clause represents a reference to a type and translates into a TypeDef, a TypeRef, or a TypeSpec (if the parent is an instantiation of a generic type). The general syntax of a class reference is

<class_ref> ::= [<resolution_scope>]<full_type_name>

where

<resolution_scope> ::= [<assembly_ref_alias> ]
      | [.module<module_ref_name> ]

Note that the square brackets in the definition of <resolution_scope> are syntactic elements; they do not indicate that any portion of the definition is optional. The previous syntax does not describe instantiations of generic types, which are presented in Chapter 11.

Here are a few examples of class references:

[mscorlib]System.ValueType // Type is defined in another assembly
[ .module Second.dll]Foo.Bar// Type is defined in another module
Foo.Baz                    // Type is defined in this module

If the resolution scope of a class reference points to an external assembly or module, the class reference is translated into a TypeRef metadata token, with the full type name providing values for the Name and Namespace entries and the resolution scope providing an AssemblyRef or a ModuleRef token for the ResolutionScope entry.

If the resolution scope is not defined—that is, if the referenced type is defined somewhere in the current module—the class reference is translated into the respective TypeDef token.

Parent of the Type

Having resolved the class reference to a TypeRef or TypeDef token, I thus provided the value for the Extends entry of the TypeDef record under construction. This token references the type’s parent—that is, the type from which the current type is derived.

The type referenced in the extends clause must not be sealed and must not be an interface; otherwise, the loader will fail to load the type. When a type is sealed, no types can be derived from it.

If the extends clause is omitted, the ILAsm compiler assigns a default parent depending on the flags specified for the type:

  • interface: No parent. The interfaces are not derived from other types.
  • value: The parent is [mscorlib]System.ValueType.
  • enum: The parent is [mscorlib]System.Enum.
  • None of the above: The parent is [mscorlib]System.Object.

If the extends clause is present, the value and enum flags are ignored, and the interface flag causes a compilation error. This difference in ILAsm’s reaction to erroneous flags can be easily explained: the value and enum are pseudoflags, like hints for the IL assembler, while the interface flag is a true metadata flag, and in combination with extends clause it represents invalid metadata.

If the type layout is specified as sequential or explicit, the type’s parent must also have the corresponding layout, unless the parent is [mscorlib]System.Object, [mscorlib]System.ValueType, or [mscorlib]System.Enum. The rationale is that the type might inherit fields from its parent, and the type cannot have a mixed layout; that is, it cannot have some fields laid out automatically and some laid out explicitly or sequentially. However, an autolayout type can be derived from a type having any layout; in this case, information about the parent’s field layout plays no role in laying out the instance fields of the derived type.

Interface Implementations

If the type being defined implements one or more interfaces, the type declaration has an additional clause, the implements clause, as shown here:

.class<flags> <dotted_name>
   extends<class_ref>
   implements<class_refs> {
      ...
}

The nonterminal symbol <class_refs> simply means a comma-separated list of class references, like so:

<class_refs> ::= <class_ref>[,<class_ref>*]

For example,

.class public MyNamespace .MyClass
   extends MyNamespace.MyClassBase
   implements MyNamespace.IOne,
              MyNamespace.ITwo,
              MyNamespace.IThree {
              ...
}

The types referenced in the implements clause must be interfaces. A type implementing an interface must provide the implementation for all of the interface’s instance methods. The only exception to this rule is an abstract class.

The implements clause of a type declaration creates as many records in the InterfaceImpl metadata table as there are class references listed in this clause. In the preceding example, three InterfaceImpl records would be created.

And, while an interface cannot extend any type, including another interface, it certainly can implement one or more other interfaces. I discussed the difference between one type extending (inheriting from) another type and a type implementing an interface earlier in this chapter.

Class Layout Information

To provide additional information regarding type layout (field alignment, total type size, or both), you need to use the .pack and .size directives, as shown in this example:

.class public value explicit MyNamespace.MyStruct {
   .pack4
   .size1024
   ...
}

These directives, obviously enough, set the entries PackingSize and ClassSize, respectively, of the ClassLayout record associated with a given class.

The .pack and .size directives appear within the scope of the type declaration, in any order. If .pack is not specified, the field alignment defaults to 1. If .pack or .size is specified, a ClassLayout record is created for this TypeDef.

Integer values specified in a .pack directive must be 0 or a power of 2, in the range 20 to 27 (1 to 128). Breaking this rule results in a compilation error. When the value is 0, the field alignment defaults to the “natural” value defined by the type of the field—the size of the type or the nearest greater power of 2.

Class layout information should not be specified for the autolayout types. Formally, defining the class layout information for an autolayout type represents invalid metadata. In reality, however, it is simply a waste of metadata space; when the loader encounters an autolayout type, it never checks to see whether this type has a corresponding ClassLayout record.

Interfaces

An interface is a special kind of type, defined in Partition I of the Ecma International/ISO standard as “a named group of methods, locations, and other contracts that shall be implemented by any object type that supports the interface contract of the same name.” In other words, an interface is not a “real” type but merely a named descriptor of methods and properties exposed by other types—an IOU note of a type. Conceptually, an interface in the common language runtime is similar to a COM interface—or at least the general idea is the same.

Not being a real type, an interface is not derived from any other type, and other types cannot be derived from an interface. But an interface can “implement” other interfaces. This is not a true implementation, of course. When I say that “interface IA implements interfaces IB and IC,” I mean only that the contracts defined by IB and IC are subcontracts of the contract defined by IA.

As a descriptor of items (methods, properties, events) exposed by other types, an interface cannot offer its own implementation of these items and thus is, by definition, an abstract type. When you define an interface in ILAsm, you can omit the keyword abstract because the compiler adds this flag automatically when it encounters the keyword interface.

For the same reason, an interface cannot have instance fields because a declaration of a field is the field’s implementation. However, an interface must offer the implementation of its static members (the items shared by all instances of a type) if it has any. Bear in mind, of course, that the definition of static as “shared by all instances” is general for all types and does not imply that interfaces can be instantiated. They cannot be. Interfaces are inherently abstract and cannot even have instance constructors.

Static members (fields, methods) of an interface are not part of the contract defined by the interface and have no bearing on the types that implement the interface. A type implementing an interface must implement all instance members of the interface, but it has nothing to do with the static members of the interface. Static members of an interface can be accessed directly like static members of any type, and you don’t need an “instance” of an interface (meaning an instance of a class implementing this interface) for that.

The nature of an interface as a descriptor of items exposed by other types requires that the interface itself and all its instance members must be public, which makes reasonable sense; I am, after all, talking about exposed items.

Interfaces have several limitations. One is obvious: since an interface is not a real type, it does not have layout. It simply doesn’t make sense to talk about the packing size or total size of a contract descriptor.

Another limitation stems from the fact that the instance methods declared by an interface must be virtual because they are implemented elsewhere, namely, by the class implementing the interface. Chapter 10 discusses the virtual methods and their implementation in details.

Yet another limitation is not so obvious: interfaces should not be sealed. This might sound contradictory because, as just noted, no types can be derived from interfaces—which is precisely the definition of sealed. The logic behind this limitation is as follows: since a sealed type cannot extend any other type, its virtual methods cannot be overridden, so they become simple instance methods; and, as you may recall, an interface may provide implementation only of its static methods, so these instance (formerly known as virtual) methods are left unimplemented.

From this logic stems a more general rule, applicable to all types, that dictates an abstract type should not be sealed unless it has only static members. At least that is what the Ecma International/ISO specification says. I personally think that the correct formulation of a general rule would be that an abstract type cannot be sealed unless it has no abstract (unimplemented)virtual methods. And a type may be declared abstract even if it contains no abstract methods. You may just not want this particular type to ever be instantiated. There is quite a difference between “no instance members” and “no abstract virtual methods,” don’t you agree?

On the other hand, what is the use of the instance members of a type if you cannot instantiate this type (it’s abstract) or derive something “instantiatable” from it (it’s sealed)? So maybe the Ecma International/ISO spec is right: the abstract types with only nonabstract instance members could be declared sealed, but they should not be declared sealed.

Instance methods of an interface, however, are all abstract virtual by definition, so there is no “should/could” dilemma.

Value Types

Value types are the closest thing in the common language runtime model to C++ structures. These types are values with either a trivial structure (for example, a 4-byte integer) or a complex structure. When you declare a variable of a class type, you don’t automatically create a class instance. You create only a reference to the class, initially pointing at nothing. But when you declare a variable of value type, the instance of this value type is allocated immediately, by the variable declaration itself, because a value type is primarily a data structure. As such, a value type must have instance fields or size defined. A zero-size value type (with no instance fields and no total size specified) represents invalid metadata; however, as in many other cases, the loader is more forgiving than the official metadata validity rules: when it encounters a zero-size value type, the loader assigns it a 1-byte size by default.

Value types are the types passed by value, as opposed to the reference types passed by reference. It means that the code a = b;, when a and b are value types, is translated into copying the contents of b into a, and when a and b are reference types, it is translated into copying the reference to some class instance from b to a. So in the end you wind up with two identical instances in the case of a and b being of a value type and with two identical references to the same instance in the case of a and b being of a reference type.

Although an instance of a value type is created at the moment a variable having this value type is declared, the default instance constructor method (should it be defined for the value type in question) is not called at this moment. (See Chapter 10 for information about the instance constructor method.) Declaring a variable creates a “blank” instance of the value type, and if this value type has a default instance constructor, it should be called explicitly.

Please don’t ask me why the runtime does not execute the instance constructor of a value type (if available) automatically when it allocates the instance of this type; this question is of the same rhetorical nature as “why does runtime ignore the default values specified for fields and parameters?” (See Chapters 9 and 10 for details.) The correct answer is “because the runtime is built this way.”

Boxed and Unboxed Values

As a data structure, a value type must sometimes be represented as an object to satisfy the requirements of certain generic APIs, which expect object references as input parameters. The common language runtime provides the means to produce a class representation of a value type and to extract a value type (data structure) from its class representation. These operations, called boxing and unboxing, respectively, are defined for every value type.

Recall from the beginning of this chapter that types can be classified as either value types or reference types. Simply put, boxing transforms a value type into a reference type (an object reference), and unboxing does just the opposite. You can box any value type and get an object reference, but this does not mean, however, that you can unbox any object and get a value type: in the .NET type system, every value type has its reference-type “hat,” but not vice versa. Why that is so, when it is obviously possible to extract the data part from any reference type that has it, is another of those rhetoric questions.

When you declare a value type variable, you create a data structure. When you box this variable, an object (a class instance) is created whose data part is an exact bit copy of the data structure. Then you can deal with this instance the same way you would deal with an ordinary object—for example, you could use it in a call to a method, which takes an object reference as a parameter. It is important to understand that the “original” instance of a value type does not go anywhere after it has been boxed. Its copy does. And what happens to this copy is not reflected back to the original instance of the value type. This effect is known as a problem of mutability of the boxed value types. It is up to the author of the code to propagate possible changes inflicted upon the boxed instance of the value type back to the original instance.

When a boxed value type is being unboxed, no instance copying is involved. The unboxing operation simply produces a managed pointer to the data part of the object to which it is applied.

Instance Members of Value Types

Value types, like other types, can have static and instance members, including methods and fields. To access an instance member of a class, you need to provide the instance pointer (known in C++ as this). In the case of a value type, you simply use a managed reference as an instance pointer.

Let’s suppose, for example, that you have a variable of type 4-byte integer. (What can be more trivial than that, except maybe type fewer-byte integer?) This value type is defined as [mscorlib]System.Int32 in the .NET Framework class library. Instead of boxing this variable and getting a reference to an instance of System.Int32 as the class, you can simply take the reference to this variable and call the instance methods of this value type, say, ToString(), which returns a string representation of the integer in question.

...
.locals init(int32J)// Declare variable J as value type
...
ldc.i412345
stloc J  // J = 12345
...
ldloca J // Get managed reference to J as instance pointer
// Call method of this instance
call instance string[mscorlib]System.Int32::ToString()
...

Can value types have virtual methods? Yes, they can. However, to call the virtual methods of a value type, you have to box this value type first. I must clarify, though, that you need to box the value type only if you are calling its virtual method as a virtual method, through the virtual table dispatch, using the callvirt instruction (methods and method call instructions are discussed in Chapters 10, 12, and 13). If you are calling a virtual method of a value type as simply an instance method, using the call instruction, you don’t need to box the value type. That’s why you didn’t need to box the variable J in the previous code snippet before calling the ToString() method despite that ToString() is a virtual method.

Derivation of Value Types

All value types are derived from the [mscorlib]System.ValueType class. More than that, anything derived from [mscorlib]System.ValueType is a value type by definition, with one important exception: the [mscorlib]System.Enum class, which is a parent of all enumerations (discussed in the next section).

Unlike C++, in which derivation of a structure from another structure is commonplace, the common language runtime object model does not allow any derivations from value types. All value types must be sealed. (And you probably thought I was too lazy to draw further derivation branches from value types in Figure 7-1!). As to why all value types must be sealed, I am afraid it’s another one of those rhetorical questions.

Enumerations

Enumerations (a.k.a. enumeration types, a.k.a. enums) make up a special subset of value types. All enumerations are derived from the [mscorlib]System.Enum class, which is the only reference type derived from [mscorlib]System.ValueType. Enums are possibly the most primitive of all types that have some structure, and the rules regarding them are the most restrictive.

Unlike other value types in their boxed form, enumerators don’t show any of the characteristics of a “true class.” Enums can have only fields as members—no methods, properties, or events. Enums cannot implement interfaces; since enums cannot have methods, the question of implementing interfaces is moot.

Here is an example of a simple enumeration:

.class public enum Color
{
   .field public specialname int32 __value
   .field public static literal valuetype Color Red = int32(1)
   .field public static literal valuetype Color Green = int32(2)
   .field public static literal valuetype Color Blue = int32(3)
}

Even with the fields the enums have no leeway: an enum must have exactly one instance field and at least one static literal field. The instance field of an enum represents the value of the current instance of the enum and must be of integer, Boolean, or character type. The type of the instance field is the underlying type of the enum. The enum itself as a value type is completely interchangeable with its underlying type in all operations except boxing. If an operation, other than boxing, expects a Boolean variable as its argument, a variable of a Boolean-based enumeration type can be used instead, and vice versa. A boxing operation, however, always results in a boxed enum and not in a boxed underlying type.

The static literal fields represent the values of the enum itself and have the type of the enum. As values of the enum, these fields must be not only static (shared by all instances of the type) but also literal—they represent constants defined in the metadata. The literal fields are not true fields because they do not occupy memory allocated by the loader when the enum is loaded and laid out. (Chapter 9 discusses this and other aspects of fields.)

Generally speaking, you can think of an enumeration as a restriction of its underlying type to a predefined, finite set of values (however, the CLR does not enforce this restriction). As such, an enumeration obviously cannot have any specific layout requirements and must have the auto layout flag set.

Delegates

Delegates are a special kind of reference type, designed with the specific purpose of representing function pointers. All delegates are derived from the [mscorlib]System.MulticastDelegate type, which in turn is derived from the [mscorlib]System.Delegate type. The delegates themselves are sealed (just like the value types), so no types can be derived from them.

Limitations imposed on the structure of a delegate are as strict as those imposed on the enumerator structure. Delegates have no fields, events, or properties. They can have only instance methods, either two or four of them, and the names and signatures of these methods are predefined.

Two mandatory methods of a delegate are the instance constructor (.ctor) and Invoke. The instance constructor returns void (as all instance constructors do) and takes two parameters: the object reference to the type defining the method being delegated and the function pointer to the managed method being delegated. (See Chapter 10 for details about instance constructors.)

This leads to a question: if you can get a function pointer per se, why do you need delegates at all? Why not use the function pointers directly? You could, but then you would need to introduce fields or variables of function pointer types to hold these pointers—and function pointer types are considered a security risk (because the pointer value can be modified after it was acquired from a particular function) and deemed unverifiable. If a module is unverifiable, it can be executed only from a local drive in full trust mode, when all security checks are disabled. Another drawback is that managed function pointers cannot be marshaled to unmanaged function pointers when calling unmanaged methods, whereas delegates can be. (See Chapter 18 for information on managed and unmanaged code interoperation.)

Delegates are secure, verifiable, and type-safe representations of function pointers first of all because the function pointers in delegate representation cannot be tampered with and as such are preferable over function pointer types. Besides, delegates can offer additional useful features, as I’ll describe in a moment.

The second mandatory method (Invoke) must have the same signature as the delegated method. Two mandatory methods (.ctor and Invoke) are sufficient to allow the delegate to be used for synchronous calls, which are the usual method calls when the calling thread is blocked until the called method returns. The first method (.ctor) creates the delegate instance and binds it to the delegated method. The Invoke method is used to make a synchronous call of the delegated method.

Delegates also can be used for asynchronous calls, when the called method is executed on a separate thread created by the common language runtime for this purpose and does not block the calling thread. So that it can be called asynchronously, a delegate must define two additional methods, BeginInvoke and EndInvoke.

BeginInvoke is a thread starter. It takes all the parameters of the delegated method plus two more: a delegate of type [mscorlib]System.AsyncCallback representing a callback method that is invoked when the call completes, and an object you choose to indicate the final status of the call thread. BeginInvoke returns an instance of the interface [mscorlib]System.IAsyncResult, carrying the object you passed as the last parameter. Remember that since interfaces, delegates, and objects are reference types, when I say “takes a delegate” or “returns an interface,” I actually mean a reference.

If you want to be notified immediately when the call is completed, you must specify the AsyncCallback delegate. The respective callback method is called upon the completion of the asynchronous call. This event-driven technique is the most widely used way to react to the completion of the asynchronous calls.

You might choose another way to monitor the status of the asynchronous call thread: polling from the main thread. The returned interface has the method bool get_IsCompleted(), which returns true when the asynchronous call is completed. You can call this method from time to time from the main thread to find out whether the call is finished.

You can also call another method of the returned interface, get_AsyncWaitHandle, which returns a wait handle, an instance of the [mscorlib]System.Threading.WaitHandle class. After you get the wait handle, you can monitor it any way you please (similar to the use of the Win32 APIs WaitForSingleObject and WaitForMultipleObjects). If you are curious, disassemble Mscorlib.dll and take a look at this class.

If you have chosen to employ a polling technique, you can forgo the callback function and specify null instead of the System.AsyncCallback delegate instance.

The EndInvoke method takes the [mscorlib]System.IAsyncResult interface, returned by BeginInvoke, as its single argument and returns void. This method waits for the asynchronous call to complete, blocking the calling thread, so calling it immediately after BeginInvoke is equivalent to a synchronous call using Invoke. EndInvoke must be called eventually in order to clear the corresponding runtime threading table entry, but it should be done when you know that the asynchronous call has been completed.

All four methods of a delegate are virtual, and their implementation is provided by the CLR itself—the user does not need to write the body of these methods. When defining a delegate, you can simply declare the methods without providing implementation for them, as shown here:

.class public sealed MyDelegate
   extends[mscorlib]System.MulticastDelegate
{
   .method public hidebysig instance
      void .ctor(object MethodsClass,
                 native unsigned int MethodPtr)
         runtime managed{ }
 
   .method public hidebysig virtual instance
      int32Invoke(void*Arg1, void*Arg2)
         runtime managed{ }
 
   .method public hidebysig newslot virtual instance
      class[mscorlib]System.IAsyncResult
         BeginInvoke(void*Arg1, void*Arg2,
                     class[mscorlib]System.AsyncCallback callBkPtr,
                     object) runtime managed{ }
 
   .method public hidebysig newslot virtual instance
      void EndInvoke(class[mscorlib]System.IAsyncResult res )
         runtime managed{ }
}

Nested Types

Nested types are types (classes, interfaces, value types) that are defined within other types. However, being defined within another type does not make the nested type anything like the member classes or Java inner classes. The instance pointers (this) of a nested type and its enclosing type are in no way related. A nested class does not automatically get access to the this pointer of its enclosing class when the instance of the enclosing class is created.

In addition, instantiating the enclosing class does not involve instantiating the class(es) nested in it. The nested classes must be instantiated separately. Instantiating a nested class does not require the enclosing class to be instantiated.

Type nesting is not about membership and joint instantiation; rather, it’s all about visibility. As explained earlier in “Class Attributes,” nested types at any level of nesting have their own specific visibility flags. When one type is nested in another type, the visibility of the nested type is “filtered” by the visibility of the enclosing type. If, for example, a class whose visibility is set to nested public is nested in a private class, this nested class will not be visible outside the assembly despite its own specified visibility.

This visibility filtering works throughout all levels of nesting. The final visibility of a nested class is defined by its own declared visibility and then is limited in sequence by the visibilities of all classes enclosing it, directly or indirectly.

Nested classes are defined in ILAsm the same way they are defined in other languages—that is, the nested classes are declared within the lexical scope of their encloser declaration, like so:

.class public MyNameSpace.Encl {
   ...
   .class nested public Nestd1 {
      ...
      .class nested family Nestd2 {
         ...
      }
   }
}

According to this declaration, the Nestd2 class is nested in the Nestd1 class, which in turn is nested in MyNameSpace.Encl, which is not a nested class.

Full names of the nested classes are not in any way affected by the names of their enclosers: neither the namespace nor the name of the encloser automatically becomes (or is required to be) part of the nested class’s full name. The full name of a nested class must be unique within the encloser scope, meaning that a class cannot have two identically named classes nested in it.

Since the nested classes are identified by their full name and their encloser (which is in turn identified by its scope and full name), the nested classes are referenced in ILAsm as a concatenation of the encloser reference, nesting symbol / (forward slash), and full name of the nested class, like

<nested_class_ref> ::= <encloser_ref> / <full_type_name>

where

<encloser_ref> ::= <nested_class_ref> | <class_ref>

and <class_ref> has already been defined earlier as

<class_ref> ::= [<resolution_scope>]<full_type_name>

According to these definitions, classes Nestd1 and Nestd2 will be referenced respectively as MyNameSpace.Encl/Nestd1 and MyNameSpace.Encl/Nestd1/Nestd2. Names of nested classes must be unique within their nester, as opposed to the full names of top-level classes, which must be unique within the module or (for public classes) within the assembly.

Unlike C#, which uses a dot delimiter for all hierarchical relationships without discrimination—so that One.Two.Three might mean “class Three of namespace One.Two” or “class Three nested in class Two of namespace One” or even “field Three of class Two nested in class One”—ILAsm uses different delimiters for different hierarchies. A dot is used for the full class name hierarchy; a forward slash (/) indicates the nesting hierarchy; and a double colon (::), as in “classic” C++, denotes the class-member relationship. I used the qualifier “classic” because the managed version of Visual C++ uses a double colon instead of a dot delimiter in dotted names, so it has the same ambiguity problem as C#, only instead of the ambiguous One.Two.Three, it uses the equally ambiguous One::Two::Three. That’s a huge difference indeed.

Thus far, the discussion has focused mainly on what nested classes are not. One more important negative to note is that nested classes have no effect on the layout of their enclosers. If you want to declare a substructure of a structure, you must declare a nested value type (substructure) within the enclosing value type (structure) and then define a field of the substructure type.

.class public value Struct {
   ...
   .class nested public value Substruct {
      ...
   }
   .field public valuetype Struct/Substruct Substr
}

Now I need to say something positive about nested classes. Members of a nested class have access to all members of the enclosing class without exception, including access to private members. In this regard, the nesting relationship is even stronger than inheritance and stronger than the member class relationship in C++, where member classes don’t have access to private members of their owner. Of course, to get access to the encloser’s instance members, the nested type members should first obtain the instance pointer to the encloser. This “full disclosure” policy works one-way only; the encloser has no access to private members of the nested class.

Nested types can be used as base classes for other types that don’t need to be nested.

.class public X {
   ...
   .class nested public Y {
      ...
   }
}
.class public Z extends X/Y {
   ...
}

Of course, class Z, derived from a nested class (Y), does not have any access rights to private members of the encloser (X). The “full disclosure” privilege is not inheritable.

A nested class can be derived from its encloser. In this case, it retains access to the encloser’s private members, and it also acquires an ability to override the encloser’s virtual methods. The enclosing class cannot be derived from any of its nested classes.

image Note  A metadata validity rule states that a nested class must be defined in the same module as its encloser. In ILAsm, however, the only way to define a nested class is to declare it within the encloser’s lexical scope, which means you could not violate this validity rule in ILAsm even if you tried.

Class Augmentation

In ILAsm, as in Visual Basic and C#, all members, attributes, and nested classes of a class are declared within the lexical scope of that class. However, ILAsm allows you to reopen a once-closed class scope and define additional items.

.class public X extends Y implements IX,IY {
   ...
}
...
// Later in the source, possibly in another source file...
.class X {
   ... // More items defined
}

This reopening of the class scope is known as class augmentation. A class can be augmented any number of times throughout the source code, and the augmenting segments can reside in different source files. The following simple safety rules govern class augmentation:

  • The class must be fully defined within the module. In other words, you cannot augment a class that is defined somewhere else. (Wouldn’t that be nice? Good-bye, security; fare thee well!)
  • Class flags, the extends clause, and the implements clause must be fully defined at the lexically first opening of class scope because these attributes are ignored in augmenting segments.
  • None of the augmenting segments can contain duplicate item declarations. If you declare field int32 X in one segment and then declare it in another segment, the ILAsm compiler will not appreciate that you probably have the same field in mind and will read it as an attempt to define two identical fields in the same class, which is not allowed.
  • The augmenting segments are not explicitly numbered, and the class is augmented according to the sequence of augmenting segments in the source code. This means the sequence of class item declarations will change if you swap augmenting segments, which in turn might affect the class layout.

A good strategy for writing an ILAsm program in versions 1.0 and 1.1 was to use forward class declaration, explained in the Chapter 1. This strategy allows you to declare all classes of the current module, including nested ones, without any members and attributes, and to define the members and attributes in augmenting segments. This way, the IL assembler gets the full picture of the module’s type declaration structure before any type is referenced. By the time locally declared types are referenced, they all are already defined and have corresponding TypeDef metadata records.

There is no need for forward class declaration in version 2.0 or later of ILAsm, though. In v2.0+, the IL assembler implicitly declares a class whenever this class is mentioned, as a declaration or as a reference. Of course, the class implicitly declared on a reference is just a dummy—a placeholder. It turns from a dummy to “real” class declaration when the declaration of the class (.class ... { ... }) is encountered in the source code. If all compilands are parsed, and there still are “dummies” remaining, the compilation fails.

This method of class “bookkeeping” messes up royally the order of class declaration on round-tripping (disassembling and reassembling of a module), because the classes in the round-tripped module are emitted not in the order they were emitted in the original module, but rather in the order they were mentioned in the disassembly. This is a minor issue because the order of class definitions (TypeDef records) does not really matter, except in the case of nested classes (enclosing class must be declared before the nested class), and this case is handled properly by the IL assembler.

If, however, you want to preserve the order of class declarations or you have some considerations to emit the class declarations in some particular order, you can use directive .typelist, like so:

.typelist { FirstClass SecondClass ThirdClass ...}

The .typelist directive is best placed right on top of the first source file, even before the manifest declarations but after the .mscorlib directive, if present. The reason for such placing is obvious: the IL assembler needs to know right away if you are compiling Mscorlib.dll or something else, and the manifest declarations might have custom attributes, or other class references, that could mix up the intended order of class declaration.

Manifest declarations, described in Chapter 6, plus forward class declarations (v1.0, v1.1) or the .typelist directive (v2.0+), look a lot like a program header, so I would not blame you if you put them in a separate source file. Just don’t forget that this file must be first on the list of source files when you assemble your module.

Summary of the Metadata Validity Rules

Recall that the type-related metadata tables (except those related to generic types, which will be discussed in Chapter 11) include TypeDef, TypeRef, InterfaceImpl, NestedClass, and ClassLayout. The records of these tables contain the following entries:

  • The TypeDef table contains the Flags, Name, Namespace, Extends, FieldList, and MethodList entries.
  • The TypeRef table contains the ResolutionScope, Name, and Namespace entries.
  • The InterfaceImpl table contains the Class and Interface entries.
  • The NestedClass table contains the NestedClass and EnclosingClass entries.
  • The ClassLayout table contains the PackingSize, ClassSize, and Parent entries.

TypeDef Table Validity Rules

  • The Flags entry can have only those bits set that are defined in the enumeration CorTypeAttr in CorHdr.h except the tdForwarder flag, reserved for exported types (validity mask: 0x00173DBF).
  • [run time] The Flags entry cannot have the sequential and explicit bits set simultaneously.
  • [run time] The Flags entry cannot have the unicode and autochar bits set simultaneously.
  • If the rtspecialname flag is set in the Flags entry, the Name field must be set to _Deleted*, and vice versa.
  • [run time] If the bit 0x00040000 is set in the Flags entry, either a DeclSecurity record or a custom attribute named SuppressUnmanagedCodeSecurityAttribute must be associated with the TypeDef, and vice versa.
  • [run time] If the interface flag is set in the Flags entry, abstract must be also set.
  • [run time] If the interface flag is set in the Flags entry, sealed must not be set.
  • [run time] If the interface flag is set in the Flags entry, the TypeDef must have no instance fields.
  • [run time] If the interface flag is set in the Flags entry, all the TypeDef’s instance methods must be abstract.
  • [run time] The visibility flag of a non-nested TypeDef must be set to private or public.
  • [run time] If the visibility flag of a TypeDef is set to nested public, nested private, nested family, nested assembly, nested famorassem, or nested famandassem, the TypeDef must be referenced in the NestedClass entry of one of the records in the NestedClass metadata table, and vice versa.
  • The Name field must reference a nonempty string in the #Strings stream.
  • The combined length of the strings referenced by the Name and Namespace entries must not exceed 1,023 bytes.
  • The TypeDef table must contain no duplicate records with the same full name (the namespace plus the name) unless the TypeDef is nested or deleted.
  • [run time] The Extends entry must be nil for TypeDefs with the interface flag set and for the TypeDef System.Object of the Mscorlib assembly.
  • [run time] The Extends entry of all other TypeDefs must hold a valid reference to the TypeDef, TypeRef, or TypeSpec table, and this reference must point at a nonsealed class (not an interface or a value type).
  • [run time] The Extends entry must not point to the type itself or to any of the type descendants (inheritance loop).
  • [run time] The FieldList entry can be nil or hold a valid reference to the Field table.
  • [run time] The MethodList entry can be nil or hold a valid reference to the Method table.

Enumeration-Specific Validity Rules

If the TypeDef is an enum—that is, if the Extends entry holds the reference to the class [mscorlib]System.Enum—the following additional rules apply:

  • [run time] The interface, abstract, sequential, and explicit flags must not be set in the Flags entry.
  • The sealed flag must be set in the Flags entry.
  • The TypeDef must have no methods, events, or properties.
  • The TypeDef must implement no interfaces—that is, it must not be referenced in the Class entry of any record in the InterfaceImpl table.
  • [run time] The TypeDef must have at least one instance field of integer type or of type bool or char.
  • [run time] All static fields of the TypeDef must be literal.
  • The type of the static fields of the TypeDef must be the current TypeDef itself.

TypeRef Table Validity Rules

  • [run time] The ResolutionScope entry must hold either 0 or a valid reference to the AssemblyRef, ModuleRef, Module, or TypeRef table. In the last case, TypeRef refers to a type nested in another type (a nested TypeRef).
  • If the ResolutionScope entry is nil, the ExportedType table of the prime module of the assembly must contain a record whose TypeName and TypeNamespace entries match the Name and Namespace entries of the TypeRef record, respectively.
  • [run time] The Name entry must reference a nonempty string in the #Strings stream.
  • [run time] The combined length of the strings referenced by the Name and Namespace entries must not exceed 1,023 bytes.
  • The table must contain no duplicate records with the same full name (the namespace plus the name) and ResolutionScope value.

InterfaceImpl Table Validity Rules

A Class entry set to nil means a deleted InterfaceImpl record. If the Class entry is non-nil, however, the following rules apply:

  • [run time] The Class entry must hold a valid reference to the TypeDef table.
  • [run time] The Interface entry must hold a valid reference to the TypeDef or TypeRef table.
  • If the Interface field references the TypeDef table, the corresponding TypeDef record must have the interface flag set in the Flags entry.
  • The table must contain no duplicate records with the same Class and Interface entries.

NestedClass Table Validity Rules

  • The NestedClass entry must hold a valid reference to the TypeDef table.
  • [run time] The EnclosingClass entry must hold a valid reference to the TypeDef table, one that differs from the reference held by the NestedClass entry.
  • The table must contain no duplicate records with the same NestedClass entries.
  • The table must contain no records with the same EnclosingClass entries and NestedClass entries referencing TypeDef records with matching names. In other words, a nested class must have a unique name within its encloser.
  • The table must contain no sets of records forming a circular nesting pattern, such as A nested in B, B nested in C, C nested in A.

ClassLayout Table Validity Rules

A Parent entry set to nil means a deleted ClassLayout record. However, if the Parent entry is non-nil, the following rules apply:

  • The Parent entry must hold a valid reference to the TypeDef table, and the referenced TypeDef record must have the Flags bit explicit or sequential set and must have the interface bit not set.
  • [run time] The PackingSize entry must be set to 0 or to a power of 2 in the range 1 to 128.
  • The table must contain no duplicate records with the same Parent entries.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.89.85