Custom Attributes
Every system worth its salt needs extensibility. The languages that describe an extensible system and their compilers need extensibility as well; otherwise, they are describing not the system but rather its glorious past.
A system and the associated languages can be extended in three ways. The first way is to tinker with the system itself, changing its inner structure and changing the languages accordingly. This approach is good as long as the system has a negligible number of users, because each new version of the system (and hence the languages) is basically different from the previous version. This approach is characteristic of the early stages of the life cycle of a complex system.
The second way is to leave the system and the languages as they are and build a parallel system (and parallel languages and their compilers), providing additional functionality. A classic example of this approach was the introduction of the remote procedure call (RPC) standard and the interface description language (IDL) in parallel with existing C runtime and C/C++ compilers.
The third way is to build into the system (and the languages) some formal means of extensibility and then employ these means when needed. This approach allows the system developers to sneak in new features and subsystems without changing the basic characteristics of the system. The only challenge is to devise means of extensibility that are both efficient and universal—efficient because we need productivity, and universal because we don’t know what we’ll need tomorrow or a year from now. These requirements are contradictory, and usually universality wins out. If efficiency wins at universality’s expense, sooner or later the designers run out of options and must switch to the second way.
The Microsoft .NET platform, including the common language runtime, the .NET Framework, and the compilers, has an extensibility mechanism built in. This mechanism employs the metadata entities known as custom attributes.
Concept of a Custom Attribute
A custom attribute is a metadata item specifically designed as a universal tool for metadata extension. Custom attributes do not, of course, change the metadata schema, which is hard-coded and a sacrosanct part of the common language runtime. Custom attributes also don’t play any role similar to the generics, which create new types based on some “templates.” Rather, custom attributes provide a way to specify additional information about metadata items—information not represented by a metadata item itself.
The information carried by custom attributes is intended mostly for various tools such as compilers, linkers, and debuggers. The runtime recognizes only a small subset of custom attributes.
Custom attributes are also lifesavers for compilers. If the designers of compilers and languages discover, to their surprise, that more features are required to describe a problem area than were initially built into a language or its compiler, they can easily extend the descriptive power of the language by introducing new custom attributes. Of course, the language and its compiler must recognize the concept of a custom attribute to begin with, but that’s hardly a problem—all managed languages and their compilers do this.
I’ve heard some slanderous statements to the effect that the number of custom attributes used by a tool is in direct proportion to the degree of wisdom acquired by the tool designers after the fact. But of course this can’t be true.
Jokes aside, custom attributes are extremely useful tools. Think of the following simple example. If we want managed code to interoperate with classic COM applications, we need to play by the classic COM rules. One of these rules is that every exported interface must have a globally unique identifier (GUID) assigned to it. The runtime generates GUIDs on the fly, but we might need not just any GUID but rather a specific GUID assigned to a class. What do we do? Add another field to the TypeDef record to store an offset in the #GUID stream? This would surely not help reduce the size of the metadata tables, especially when we consider that only a small fraction of all TypeDefs might ever be used in COM interoperation. To solve the problem, we can introduce a GUID-carrying custom attribute—actually, we have one already, System.Runtime.InteropServices.GuidAttribute—and assign this attribute to any TypeDef participating in the COM interoperation.
The problem with custom attributes is that they are very expensive in terms of resources. They bloat the metadata. Because they represent metadata add-ons, the IL code has no means of accessing them directly. As a result, custom attributes must be resolved through Reflection methods, an approach that approximates having a lively chat by means of mailing letters written in Morse code—fun if you have an eternity at your disposal.
There’s bad news regarding custom attributes, and there’s also good news. The bad news is that custom attributes keep breeding at an astonishing rate as new tools and new features are introduced. And sometimes custom attributes are invented not because of need but because “I can” or because someone wonders, “Why should I do it the hard way?” It’s so easy to use, no wonder. . . . Ahem! The good news, however, is that most custom attributes are specific to certain tools and only a small fraction are actually used at runtime.
CustomAttribute Metadata Table
The CustomAttribute table contains data that can be used to instantiate custom attributes at runtime. A record in this CustomAttribute table has three entries:
The CustomAttribute table must be sorted by the Parent field.
Given their nature as informational add-ons to other metadata items, custom attributes can be attached to any metadata item that has a specific token type assigned to it. The one exception is that custom attributes cannot be attached to another custom attribute. Chapter 5 described the 23 token types. The token type mdtCustomAttribute (0x0C000000) belongs to the custom attributes themselves. This leaves 22 tables providing potential owners of custom attributes: Module, TypeRef, TypeDef, Field, Method, Param, InterfaceImpl, MemberRef, DeclSecurity, StandAloneSig, Event, Property, ModuleRef, TypeSpec, Assembly, AssemblyRef, File, ExportedType, ManifestResource, GenericParam, GenericParamConstraint, and MethodSpec. No metadata table references the CustomAttribute table. Note that a custom attribute can be assigned to a specific type (TypeRef, TypeDef), but not to an instance of the type.
The Type entry of a custom attribute is a coded token of type CustomAttributeType and hence theoretically can be one of the following: TypeRef, TypeDef, Method, MemberRef, or String. (See Chapter 5.) In fact, in the existing releases of the common language runtime, the choice is limited to Method or MemberRef because of the requirement that the type of a custom attribute must be an instance constructor and nothing else. The class whose instance constructor represents the custom attribute type should be derived from the abstract class [mscorlib]System.Attribute.
The Value entry of a custom attribute is a blob whose contents depend on the nature of the custom attribute. If we were allowed to use a user-defined string as the custom attribute type, Value would contain the Unicode text. But because the custom attribute type is limited to instance constructors, the Value blob contains the encoded arguments of the constructor and (possibly) encoded name/value pairs, setting the fields and properties of the custom attribute’s class. If the constructor has no parameters because the mere presence of the custom attribute is considered sufficiently informational, the Value entry can hold 0.
Custom Attribute Value Encoding
The Value blob of a custom attribute might contain two categories of data: encoded argument values of the instance constructor and additional encoded name/value pairs specifying the initialization values of the fields and properties of the custom attribute class.
The Value blob encoding is based on serialization type codes enumerated in CorSerializationType in the CorHdr.h file. The serialization codes for the primitive types, strings, and vectors are the same as the respective ELEMENT_TYPE_* codes—that is, ELEMENT_TYPE_BOOLEAN, and so on, as described in Chapter 8. Additional serialization codes include TYPE (0x50), TAGGED_OBJECT (0x51), FIELD (0x53), PROPERTY (0x54), and ENUM (0x55). All the constant names include the prefix SERIALIZATION_TYPE_, which I omit because of my inherent laziness.
The encoded blob begins with the prolog, which is always the 2-byte value 0x0001. This is actually the version of the custom attribute blob encoding scheme, which hasn’t changed since its introduction, so the prolog is the same for all existing versions of the runtime. The prolog is followed by the values of the constructor arguments. The size and byte layout of these values are inferred from the constructor’s signature. For example, the value 0x1234 supplied as an argument of type int32 is encoded simply as a little-endian 4-byte integer, that is, as the following sequence of bytes:
0x34 0x12 0x00 0x00
If the argument is a vector, its encoding begins with a 4-byte element count, followed by the element values. For example, a vector of the three unsigned int16 values 0x1122, 0x3344, and 0x5566 is encoded as follows:
0x03 0x00 0x00 0x00 0x22 0x11 0x44 0x33 0x66 0x55
If the argument is a string, its encoding begins with the compressed string length, followed by the string itself in UTF-8 encoding, without the terminating 0 byte. The length compression formula was discussed in Table 5-1. For example, the string Common Language Runtime is encoded as the following byte sequence, with the leading byte (0x17) representing the string length (23 bytes):
0x17 0x43 0x6F 0x6D 0x6D 0x6F 0x6E 0x20 0x4C 0x61 0x6E 0x67 0x75 0x61
0x67 0x65 0x20 0x52 0x75 0x6E 0x74 0x69 0x6D 0x65
If the argument is an object reference to a boxed primitive value type—bool, char, one of the integer types, or one of the floating-point types—the encoding consists of SERIALIZATION_TYPE_TAGGED_OBJECT (0x51), followed by 1-byte primitive type encoding, followed by the value of the primitive value type. The encoding does not support object references to boxed nonprimitive value types.
Finally, if the argument is of type System.Type, its encoding is similar to that of a string, with the type’s fully qualified name playing the role of the string constant. The rules of the fully qualified type name formatting applied in the custom attribute blob encoding are those of Reflection, which differ from ILAsm conventions. The full class name is formed in Reflection and ILAsm almost identically, except for the separator symbols that denote the class nesting. ILAsm notation uses a forward slash, as in
MyNamespace.MyEnclosingClass/MyNestedClass
whereas the Reflection standard uses a plus sign, as in
MyNamespace.MyEnclosingClass+MyNestedClass
We find greater difference, however, in the way resolution scope is designated. In ILAsm, the resolution scope is expressed as the external assembly’s alias (see Chapter 6) in square brackets preceding the full class name. In Reflection notation, the resolution scope is specified after the full class name, separated by a comma. In addition, the concept of the external assembly alias is specific to ILAsm, and Reflection does not recognize it. Thus, if the version, public key token, or culture must be specified, it is done explicitly as part of the resolution scope specification. The following is an ILAsm example:
.assembly extern OtherAssembly as OtherAsm2
{
.ver1:2:3:4
.publickeytoken= (01 02 03 04 05 06 07 08)
.locale"fr-CA"
}
...
[OtherAsm2]MyNamespace.MyEnclosingClass/MyNestedClass
In contrast, here is a Reflection example:
MyNamespace.MyEnclosingClass+MyNestedClass, OtherAssembly,
Version=1.2.3.4, PublicKeyToken=0102030405060708, Culture=fr-CA
According to Reflection conventions, the resolution scope specification can be omitted if the referenced class is defined in the current assembly or in Mscorlib.dll. In ILAsm, as you know, the resolution scope is omitted only if the class is defined in the current module.
The byte sequence representing the prolog and the constructor arguments is followed by the 2-byte count of the name/value pairs. A name/value pair specifies which particular field or property must be initialized to a certain value.
The name/value pair encoding begins with the serialization code of the target: FIELD or PROPERTY. The next byte is the serialization code of the target type, which is limited to the primitive types, enums, STRING, and TYPE. After the target type comes the name of the target, encoded the same way a string argument would be: the compressed length, followed by the string itself in UTF-8 encoding, without the 0 terminator. Immediately after the target name is the target initialization value, encoded similarly to the arguments. For example, the name/value pair initializing a field (0x53) of type bool (0x02) named Inherited (length 0x09) to true (0x01) is encoded as this byte sequence:
0x53 0x02 0x09 0x49 0x6E 0x68 0x65 0x72 0x69 0x74 0x65 0x64 0x01
There is a specific way to encode the enumerations in name/value pairs. If the type of a field or property is an enum, the target type encoding starts with SERIALIZATION_TYPE_ENUM (0x55) rather than with ELEMENT_TYPE_VALUETYPE, as you would expect. The SERIALIZATION_TYPE_ENUM byte is followed by the compressed length of the full enum’s name in Reflection notation and the name itself without the zero terminator.
Verbal Description of Custom Attribute Value
Version 2.0 of the ILAsm compiler introduced the verbal description of the custom attribute value blob. This makes reading and writing the custom attribute values quite a lot easier.
The definition of a serialized primitive type is similar to the definition of the fields’ and properties’ initialization values (see Chapter 9). For example, the value 0x1234 of an int32 parameter from the previous section is expressed as int32(0x1234). The values of Boolean parameters are expressed as bool(true) or bool(false).
The definition of a string begins with the keyword string, followed by the single-quoted string value in parentheses. For example, the string Common Language Runtime from the previous section would be represented as string('Common Language Runtime'). To express a null-reference string, the construct string(nullref) is used.
The definition of the serialized types (instances of [mscorlib]System.Type) begins with the keyword type, followed either by the type name in ILAsm notation or by keyword class and the single-quoted class name in Reflection notation:
type([OtherAsm2]MyNamespace.MyEnclosingClass/MyNestedClass)
or
type(class'MyNamespace.MyEnclosingClass+MyNestedClass, OtherAssembly,
Version=1.2.3.4,PublicKeyToken=0102030405060708, Culture=fr-CA')
To express a null-reference type, the construct type(nullref) is used.
The definition of a boxed value of a primitive value type begins with the keyword object, followed by the definition of the primitive type in parentheses, such as object(int32(0x1234)).
The definition of an array contains the element count in square brackets and the space-delimited sequence of values in parentheses, such as int32[3](123 456 789) or string[4]('One' 'Two' 'Three' 'Four').
The definition of a name/value pair, denoting the initialization of a field or a property, begins with the keyword field or property, respectively, followed by the type of the field (property) and its name, followed by the equality symbol and the definition of the serialized initialization value as described previously. For example,
field int32[] xxx = int32[3](1 2 3)
property string yyy = string('Hello World!')
property enum MyEnum yyy = int32(2)
All definitions of the serialized initialization values comprising the custom attribute value blob are written in space-delimited sequence and enclosed in curly braces
Custom Attribute Declaration
The ILAsm syntax for declaring a custom attribute is
.custom<attribute_type> [ = ( <hexbytes> ) ]
or, considering the limitation imposed on <attribute_type> in the existing releases of the common language runtime that the only legal <attribute_type> is an instance constructor, is
.custom instance void<class_ref>:: .ctor(<arg_list>)
[ = ( <hexbytes> ) ]
where class_ref> is a fully qualified class reference, <arg_list> is an argument list of the instance constructor, and hexbytes> is the sequence of two-digit hexadecimal numbers representing the bytes in the custom attribute’s blob.
The ILAsm v2.0+ syntax for declaring a custom attribute with a verbal value description is
.custom<attribute_type> [ = { <serialized values> } ]
or
.custom instance void<class_ref>:: .ctor(<arg_list>)
[ = { <serialized values> } ]
To appreciate the difference between verbal and byte-array representations of a custom attribute value, consider a definition of a hypothetical custom attribute like
.custom instance void[System]System.SomeAttribute:: .ctor(bool, object)
= { bool(true)
object(int32(1234))
field type XXX = type([mscorlib]System.MulticastDelegate)
field int32[] YYY = int32[3](1 2 3)
field string[] ZZZ = string[3]('abc' 'def' 'ghe')
property enum MyEnum PPP = int32(2) }
versus
.custom instance void[System]System.SomeAttribute:: .ctor(bool, object)
= ( 01 00 01 08 D2 04 00 00 04 00 53 50 03 58 58 58
65 53 79 73 74 65 6D 2E 4D 75 6C 74 69 63 61 73
74 44 65 6C 65 67 61 74 65 2C 20 6D 73 63 6F 72
6C 69 62 2C 20 56 65 72 73 69 6F 6E 3D 32 2E 30
2E 30 2E 30 2C 20 43 75 6C 74 75 72 65 3D 6E 65
75 74 72 61 6C 2C 20 50 75 62 6C 69 63 6B 65 79
74 6F 6B 65 6E 3D 62 37 37 61 35 63 35 36 31 39
33 34 65 30 38 39 53 1D 08 03 59 59 59 03 00 00
00 01 00 00 00 02 00 00 00 03 00 00 00 53 1D 0E
03 5A 5A 5A 03 00 00 00 03 61 62 63 03 64 65 66
03 67 68 65 54 55 06 4D 79 45 6E 75 6D 03 50 50
50 02 00 00 00 )
The owner of the custom attribute, or the metadata item to which the attribute is attached, is defined by the positioning of the custom attribute declaration. At first glance, the rule regarding the declaration of metadata items is simple: if the item declaration has a scope (for example, an assembly, a class, or a method), the custom attributes of the item are declared within this scope. Otherwise—that is, if the item declaration has no scope (such items as a file, a module, or a field)—the custom attributes of the item are declared immediately after the item declaration. For example, take a look at these excerpts from the disassembly of Mscorlib.dll:
.assembly mscorlib
{
// Assembly's custom attributes
.custom instance void System.CLSCompliantAttribute:: .ctor(bool)
= ( 01 00 01 00 00 )
.custom instance void
System.Resources.NeutralResourcesLanguageAttribute:: .ctor(string)
= ( 01 00 05 65 6E 2D 55 53 00 00 ) // …en-US..
...
}
...
.module CommonLanguageRuntimeLibrary
// Module's custom attribute
.custom instance void
System.Security.UnverifiableCodeAttribute:: .ctor()
= ( 01 00 00 00 )
...
.class interface public abstract auto ansi IEnumerable
{
// Class's custom attribute
.custom instance void
System.Runtime.InteropServices.GuidAttribute:: .ctor(string)
= ( 01 00 24 34 39 36 42 30 41 42 45 2D 43 44 45 45
2D 31 31 64 33 2D 38 38 45 38 2D 30 30 39 30 32
37 35 34 43 34 33 41 00 00 )
.method public hidebysig newslot virtual abstract
instance class System.Collections.IEnumerator
GetEnumerator()
{
// Method's custom attribute
.custom instance void
System.Runtime.InteropServices.DispIdAttribute:: .ctor(int32)
= ( 01 00 FC FF FF FF 00 00 )
...
} // End of method IEnumerable::GetEnumerator
...
} // End of class IEnumerable
...
This is in stark contrast to the way custom attributes are declared, for instance, in C# where a custom attribute belonging to an item immediately precedes the item declaration.For example, the following is an excerpt showing the C# declaration of the interface IEnumerable mentioned in the preceding code:
[Guid("496B0ABE-CDEE-11d3-88E8-00902754C43A")]
public interface IEnumerable
{
[DispId(-4)]
IEnumerator GetEnumerator();
}
The ILAsm rule specifying that custom attribute ownership is defined by the position of the attribute declaration can play tricks on you if you don’t pay attention. Don’t forget that when a declaration of a nonscoped item is encountered within the scope of another item, the custom attribute’s ownership immediately switches to this newly declared item. Because of that, the custom attributes belonging to a scoped item cannot be declared just anywhere within the item’s scope. The following code snippet illustrates the point:
.class public MyClass
{
.custom instance void MyClassAttribute:: .ctor()=(01 00 00 00)
.field int32MyField
.custom instance void MyFieldAttribute:: .ctor()=(01 00 00 00)
.method public int32MyMethod([ opt] int32J)
{
.custom instance void MyMethodAttribute:: .ctor()=(01 00 00 00)
.param[1] = int32(123456)
.custom instance void MyParamAttribute:: .ctor()=(01 00 00 00)
...
}
}
To avoid possible confusion about the ownership of a custom attribute, it’s better to declare an item’s custom attributes as soon as the item’s scope is opened, before any other items are declared within the scope.
The preceding discussion covers the rules for assigning custom attributes to items that are declared explicitly. Obviously, these rules cannot be applied to metadata items, which are declared implicitly, simply by their appearance in ILAsm directives and instructions. After all, such metadata items as TypeRefs, TypeSpecs, and MemberRefs might want their fair share of custom attributes, too.
To resolve this problem, ILAsm offers another (full) form of the custom attribute declaration, with the explicit specification of the custom attribute owner such as
.custom( <owner_spec> ) instance void<class_ref>:: .ctor(<arg_list>)
[ = ( <hexbytes> ) ]
where
<owner_spec> ::= <class_ref> | <type_spec>
| method<method_ref> | field<field_ref>
For example,
.custom([mscorlib]System.String)
instance void MyTypeRefAttribute:: .ctor()=(01 00 00 00)
.custom([mscorlib]System.String[])
instance void MyTypeSpecAttribute:: .ctor()=(01 00 0 0 00)
.custom(method instance void[OtherAssem]Foo::Bar(int32, int32))
instance void MyMemberRefAttribute1:: .ctor()=(01 00 00 00)
.custom(field int32[ .module OtherMod]Foo::Baz)
instance void MyMemberRefAttribute2:: .ctor()=(01 00 00 00)
Custom attribute declarations in their full form can appear anywhere within the ILAsm source code because the owner of a custom attribute is specified explicitly and doesn’t have to be inferred from the positioning of the custom attribute declaration. The IL disassembler puts the custom attribute declarations in full form at the end of the source code dump, before the data dump.
Note that version 2.0 or later of the IL assembler also supports the verbal description of custom attribute values in the full form of custom attribute declaration.
Two additional categories of metadata items can in principle their own custom attributes: InterfaceImpls and StandAloneSigs. The existing releases of ILAsm offer no language means to define custom attributes belonging to these items, an omission to be corrected in future revisions of ILAsm and its compiler. Of course, so far no compiler or other tool has generated custom attributes for these items, but you never know. The tools develop quickly, and the custom attributes proliferate even more quickly, so sooner or later somebody will manage to assign a custom attribute to an interface implementation or a stand-alone signature.
Classification of Custom Attributes
Let’s concentrate on the custom attributes recognized by the common language runtime or the tools dealing with managed PE files and see which custom attributes are intended for various subsystems of the runtime and tools.
Before proceeding, however, I must mention one custom attribute that stands apart from any classification and is truly unique. It is the attribute System.AttributeUsageAttribute, which can (and should) be owned only by the custom attribute types. Make no mistake—custom attributes can’t own custom attributes, but as you have already found out, the Type entry of a custom attribute is always a reference to an instance constructor of some class. This class should own the custom attribute System.AttributeUsageAttribute, which identifies what kinds of metadata items can own the custom attributes typed after this class, whether these custom attributes are inheritable by the derived classes or overriding methods, and whether multiple custom attributes of this type can be owned by a concrete metadata item. Because all operations concerning custom attributes are performed through Reflection, AttributeUsageAttribute can be considered the only custom attribute intended exclusively for Reflection itself. The instance constructor of the AttributeUsageAttribute type has one int32 parameter, representing the binary flags for various metadata items as potential owners of the custom attribute typed after the attributed class. The flags are defined in the enumeration System.AttributeTargets.
The following should save you the time of looking up this enumeration in the disassembly of Mscorlib.dll:
.class public auto ansi serializable sealed AttributeTargets
extends System.Enum
{
// The following custom attribute is intended for the compilers
// And indicates that the values of the enum are binary flags
// And hence can be bitwise OR'ed
.custom instance void System.FlagsAttribute::.ctor()
= ( 01 00 00 00 )
.field public specialname rtspecialname int32value__
.field public static literal valuetype System.AttributeTargets
Assembly = int32(0x00000001)
.field public static literal valuetype System.AttributeTargets
Module = int32(0x00000002)
.field public static literal valuetype System.AttributeTargets
Class = int32(0x00000004)
.field public static literal valuetype System.AttributeTargets
Struct = int32(0x00000008) // Value type
.field public static literal valuetype System.AttributeTargets
Enum = int32(0x00000010)
.field public static literal valuetype System.AttributeTargets
Constructor = int32(0x00000020)
.field public static literal valuetype System.AttributeTargets
Method = int32(0x00000040)
.field public static literal valuetype System.AttributeTargets
Property = int32(0x00000080)
.field public static literal valuetype System.AttributeTargets
Field = int32(0x00000100)
.field public static literal valuetype System.AttributeTargets
Event = int32(0x00000200)
.field public static literal valuetype System.AttributeTargets
Interface = int32(0x00000400)
.field public static literal valuetype System.AttributeTargets
Parameter = int32(0x00000800)
.field public static literal valuetype System.AttributeTargets
Delegate = int32(0x00001000)
.field public static literal valuetype System.AttributeTargets
ReturnValue = int32(0x00002000)
.field public static literal valuetype System.AttributeTargets
GenericParameter = int32(0x00004000)
.field public static literal valuetype System.AttributeTargets
All = int32(0x00007FFF)
} // End of class AttributeTargets
As you can see, Reflection’s list of potential custom attribute owners is somewhat narrower than the metadata’s list of 22 tables. Perhaps we needn’t worry about the custom attributes of the interface implementations and stand-alone signatures just yet.
The remaining two characteristics of AttributeUsageAttribute—the Boolean properties Inherited and AllowMultiple—must be defined through the name/value pairs. The defaults are All for the potential custom attribute owners, true for Inherited and false for AllowMultiple.
You’ll find this information useful when (note that I’m not saying “if”) you decide to invent your own custom attributes. And now, back to your classification scheme.
Execution Engine and JIT Compiler
The execution engine and the JIT compiler of the common language runtime recognize three custom attributes:
Other debugging-related custom attributes (DebuggerStepThroughAttribute, DebuggerHiddenAttribute, DebuggerBrowsableAttribute, DebuggerTypeProxyAttribute, DebuggerDisplayAttribute, and DebuggerVisualizerAttribute) are debugger-specific and don’t affect the operation of the JIT compiler or the execution engine.
Interoperation Subsystem
I discuss the interoperation between managed and unmanaged code in Chapter 18, and here I’m just listing the custom attributes related to this area. All the custom attribute types in this group belong to the namespace System.Runtime.InteropServices. The following list refers to them by their class names only:
Security
Security-relatedcustom attributes are special attributes that are converted to DeclSecurity metadata records (discussed in the next chapter). Usually, the security custom attributes (except one, which is discussed in the next paragraph) don’t make it past the compilation stage—the compiler uses them to create the DeclSecurity metadata and doesn’t include the original custom attributes in the module’s metadata. In one scenario, however, the security custom attributes do “survive” the compilation and are emitted into the PE file. This happens when the security attributes owned by the assembly are specified in the assembly modules, further linked to the assembly by the assembly linker tool. In this case, the assembly-owned security attributes are converted to DeclSecurity metadata records by the assembly linker, but they remain in the assembly modules, although they play no role.
One of the security custom attributes belongs to the namespace System.Security: SuppressUnmanagedCodeSecurityAttribute. This attribute, which can be owned by a method or a TypeDef, indicates that the security check of the unmanaged code invoked by the attribute owner through the P/Invoke mechanism must be suppressed. Only trusted code can call methods that have this attribute. The instance constructor has no parameters. This custom attribute differs from other security attributes in that it is not converted to DeclSecurity metadata and hence stays intact once emitted.
The rest of the security custom attributes belong to the namespace System.Security.Permissions. The ownership of all security custom attributes is limited to the assembly, a TypeDef (class or value type), or a method. The instance constructors of these attributes have one int16 parameter, the action type code. Chapter 17 discusses the security action types and their respective codes as well as various types of permissions.
The following list briefly describes the security custom attributes; you can find further details in Chapter 17:
All intricacies of the .NET security are described in an excellent book titled NET Framework Security (Pearson, 2002), by Brian A. LaMacchia, Sebastian Lange, Matthew Lyons, Rudi Martin, and Kevin T. Price. These guys created the .NET security.
Remoting Subsystem
The following custom attributes are recognized by the remoting subsystem of the common language runtime and can be owned by a TypeDef:
Table 16-1. Synchronization Requirement Flags of SynchronizationAttribute
Value |
Meaning |
---|---|
1 |
The class should not be instantiated in a context that has synchronization. |
2 |
It is irrelevant to the class whether the context has synchronization. |
4 |
The class should be instantiated in a context that has synchronization. |
8 |
The class should be instantiated in a context with a new instance of the Synchronization property. |
Table 16-2. Instance Constructors of SynchronizationAttribute
Constructor |
Description |
---|---|
Constructor with no parameters |
Defaults the synchronization requirement to 1 and the reentrancy flag to false |
Constructor with one int32 parameter |
Sets the synchronization requirement and defaults the reentrancy flag |
Constructor with one Boolean parameter |
Sets the reentrancy flag and defaults the synchronization requirement |
Constructor with int32 and Boolean parameters |
Sets both values |
The information provided here is rather brief, but a protracted discussion of the topics related to remoting implementation goes far beyond the scope of this book. This is one of those occasions when one has to remember that modesty is a virtue. I’d rather refer you to the excellent book Advanced .NET Remoting, Second Edition (Apress, 2005), by Ingo Rammer.
Visual Studio Debugger
Thefollowing two custom attributes are recognized by the Microsoft Visual Studio debugger. They are not recognized by the .NET Framework debugger (Cordbg.exe). Both of these custom attributes belong to the namespace System.Diagnostics.
Assembly Linker
The five custom attributes listed in this section are intended for the assembly linker tool (Al.exe). This tool takes a set of nonprime modules, analyses them, and constructs an additional prime module, creating a multimodule assembly. The prime module of this assembly doesn’t carry any functionality and serves as an “official spokesperson” for the assembly. The custom attributes I am about to discuss specify the characteristics of the multimodule assembly that the assembly linker is about to create from several modules.
The most fascinating aspect of these attributes is their ownership. Think about it: when the attributes are declared, no assembly exists yet; if it did, we wouldn’t need these attributes in the first place. Hence, the attributes are declared in one or more of the modules that will make up the future assembly. What in a module might serve as an owner of these attributes? The solution is straightforward: the .NET Framework class library defines the System.Runtime.CompilerServices.AssemblyAttributesGoHere class (the prize for this class’s name invention goes to Ronald Laeremans of Visual Studio), and the assembly-specific attributes are assigned to the TypeRef of this class. Ownership of the assembly-specific attributes is the only reason this class exists.
All of the assembly-specific attributes, described in the following list, belong to the namespace System.Reflection:
Common Language Specification (CLS) Compliance
The following two custom attributes are intended for the compilers and similar tools. Both custom attributes belong to the System namespace.
Table 16-3. Instance Constructors of ObsoleteAttribute
Constructor |
Description |
---|---|
Constructor with no parameters |
Produces no message and no error |
Constructor with a string parameter |
Produces a message but no error |
Constructor with string and Boolean parameters |
Produces a message and an error flag |
Pseudocustom Attributes
As mentioned earlier, custom attributes are a lifesaver for compilers. Once a language is given the syntax to express a custom attribute, it’s free to use this syntax to describe various metadata oddities its principal syntax can’t express. The parallel evolution of the common language runtime and the managed compilers, with the runtime getting ahead now and then, created the concept of the so-called pseudocustom attributes. These attributes are perceived and treated by the compilers as other custom attributes are, but they are never emitted as such. Instead of emitting these attributes, the metadata emission API sets specific values of the metadata.
The following are the 13 pseudocustom attributes:
ILAsm syntax is adequate to describe all the parameters and characteristics listed here and does not use the pseudocustom attributes.
Caution As a matter of fact, I should warn you against using the pseudocustom attributes instead of ILAsm keywords and constructs. Using pseudocustom attributes rather than keywords is not a bright idea in part because the keywords are shorter than the custom attribute declarations. In addition, you should not forget that the ILAsm compiler, which has no use for custom attributes, treats them with philosophical resignation—in other words, it emits them just as they are, without analysis. Hence, if you specify important flags through pseudocustom attributes, the compiler will not see these flags and as a result could come to the wrong conclusions.
Summary of Metadata Validity Rules
A record of the CustomAttribute table has three entries: Parent, Type, and Value. The metadata validity rules for the custom attributes are rather simple:
18.227.209.207