Subterranean IL: Pseudo custom attributes

Posted by Simon Cooper on Simple Talk See other posts from Simple Talk or by Simon Cooper
Published on Tue, 30 Nov 2010 17:25:00 GMT Indexed on 2010/12/06 16:58 UTC
Read the original article Hit count: 683

Filed under:

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:

[Obsolete("Please use MyClass2", true)]
public class MyClass {
    // ...
}
corresponds to the following IL class definition:
.class public MyClass {
    .custom instance void
        [mscorlib]System.ObsoleteAttribute::.ctor(string, bool)
        = { string('Please use MyClass2' bool(true) }
    // ...
}
and results in the following entry in the CustomAttribute table:
TypeDef(MyClass)
MemberRef(ObsoleteAttribute::.ctor(string, bool))
blob -> {string('Please use MyClass2' bool(true)}
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#:

[Serializable]
public class MySerializableClass {}
compiles to this IL:
.class public serializable MySerializableClass {}

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:

[DllImport("mscorsn.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.U1)]
private static extern bool StrongNameSignatureVerificationEx(
    [MarshalAs(UnmanagedType.LPWStr)] string wszFilePath,
    [MarshalAs(UnmanagedType.U1)] bool fForceVerification,
    [MarshalAs(UnmanagedType.U1)] ref bool pfWasVerified);
compiles to the following IL definition:
.method private static pinvokeimpl("mscorsn.dll" lasterr winapi)
bool marshal(unsigned int8)
StrongNameSignatureVerificationEx(
    string marshal(lpwstr) wszFilePath,
    bool marshal(unsigned int8) fForceVerification,
    bool& marshal(unsigned int8) pfWasVerified) cil managed preservesig {}
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.

© Simple Talk or respective owner