Subterranean IL: Pseudo custom attributes

Comments 0

Share to social media

Custom attributes were designed to make the .NET framework extensible; if a .NET language needs to store additional metadata on an item that isn’t expressible in IL, then an attribute could be applied to the IL item to represent this metadata. For instance, the C# compiler uses DecimalConstantAttribute and DateTimeConstantAttribute to represent compile-time decimal or datetime constants, which aren’t allowed in pure IL, and FixedBufferAttribute to represent fixed struct fields.

How attributes are compiled

Within a .NET assembly are a series of tables containing all the metadata for items within the assembly; for instance, the TypeDef table stores metadata on all the types in the assembly, and MethodDef does the same for all the methods and constructors. Custom attribute information is stored in the CustomAttribute table, which has references to the IL item the attribute is applied to, the constructor used (which implies the type of attribute applied), and a binary blob representing the arguments and name/value pairs used in the attribute application.

For example, the following C# class:

corresponds to the following IL class definition:

and results in the following entry in the CustomAttribute table:

However, there are some attributes that don’t compile in this way.

Pseudo custom attributes

Just like there are some concepts in a language that can’t be represented in IL, there are some concepts in IL that can’t be represented in a language. This is where pseudo custom attributes come into play.

The most obvious of these is SerializableAttribute. Although it looks like an attribute, it doesn’t compile to a CustomAttribute table entry; it instead sets the serializable bit directly within the TypeDef entry for the type. This flag is fully expressible within IL; this C#:

compiles to this IL:

For those interested, a full list of pseudo custom attributes is available here. For the rest of this post, I’ll be concentrating on the ones that deal with P/Invoke.

P/Invoke attributes

P/Invoke is built right into the CLR at quite a deep level; there are 2 metadata tables within an assembly dedicated solely to p/invoke interop, and many more that affect it. Furthermore, all the attributes used to specify p/invoke methods in C# or VB have their own keywords and syntax within IL. For example, the following C# method declaration:

compiles to the following IL definition:

As you can see, all the p/invoke and marshal properties are specified directly in IL, rather than using attributes. And, rather than creating entries in CustomAttribute, a whole bunch of metadata is emitted to represent this information. This single method declaration results in the following metadata being output to the assembly:

  • A MethodDef entry containing basic information on the method
  • Four ParamDef entries for the 3 method parameters and return type
  • An entry in ModuleRef to mscorsn.dll
  • An entry in ImplMap linking ModuleRef and MethodDef, along with the name of the function to import and the pinvoke options (lasterr winapi)
  • Four FieldMarshal entries containing the marshal information for each parameter.

Phew!

Applying attributes

Most of the time, when you apply an attribute to an element, an entry in the CustomAttribute table will be created to represent that application. However, some attributes represent concepts in IL that aren’t expressible in the language you’re coding in, and can instead result in a single bit change (SerializableAttribute and NonSerializedAttribute), or many extra metadata table entries (the p/invoke attributes) being emitted to the output assembly.

About the author

Simon Cooper's contributions