-
Saturday, May 3, 2003 6:56 PMInterface layout
The CLR has two different techniques for implementing interfaces. These two techniques are exposed with distinct syntax in C#:
interface I { void m(); } class C : I { public virtual void m() {} // implicit contract matching } class D : I { void I.m() {} // explicit contract matching }
At first glance, it may seem like the choice between these two forms is a stylistic one. However, there are actually deep semantic differences between the two forms.
(C# has at least one other place where a choice of semantics is encoded in what seems to be a stylistic choice. A class constructor can be expressed in C# either as a static constructor method, or as assignments in a set of static field declarations. Depending on this stylistic choice, the class will or will not be marked with tdBeforeFieldInit. This mark – shown as beforefieldinit in ILDASM – affects the semantics of when the .cctor method will be executed by the CLR. This also results in performance differences, particularly in situations like NGEN or domain-neutral code.)
In class C, we get a public class method ‘m’ that does double duty as the implementation of the interface method. This is all pretty vanilla:
.method public hidebysig newslot virtual instance void m() cil managed { // Code size 1 (0x1) .maxstack 0 IL_0000: ret } // end of method C::m
But in class D, we see something quite different:
.method private hidebysig newslot virtual final instance void I.m() cil managed { .override I::m // Code size 1 (0x1) .maxstack 0 IL_0000: ret } // end of method D::I.m
There are several surprising things about this case:
- The method is introduced (newslot) with the bizarre incantation of virtual, private and final.
- The name of the method isn’t even ‘m’. It is ‘I.m’.
- There is a mysterious ‘override’ clause associated with the method body.
The method is marked as virtual because the CLR can only implement interface contracts using virtual members. There’s a bit of a debate on our team whether this is an architectural requirement or an implementation detail. At this point, we’re comfortable that we could remove this restriction without much work. However, we have to consider the ECMA standard, the impact on other CLI implementations like Compact Frameworks, the effect on the various languages targeting the compiler, and some interesting effects on existing applications. We might be saddled with this rule indefinitely.
At the language level, C# allows non-virtuals to implement interface contracts. How do they get around the CLR restriction? Well, if the class that introduces the non-virtual is in the same assembly as the class that uses that method to implement the interface contract, C# quietly defines the base class’ method as virtual. If the base class that introduced the non-virtual is in a different assembly, then C# generates a virtual thunk in the subtype which delegates to the non-virtual base method.
Getting back to our example, I.m is declared as private because it is not available for calling via the class. It can only be called via the interface.
I.m is declared as final because C# really doesn’t want to mark the method as virtual. This was forced on them by the architectural decision / implementation restriction that interface contracts can only be implemented by virtual methods.
As for the name, C# could have picked anything that’s a legal identifier. This member isn’t available for external binding, since it is private to the class and only accessible through the interface.
Since the name ‘I.m’ is insignificant, obviously this isn’t what tells the CLR loader to use this method to satisfy the interface contract. In fact, it’s that mysterious ‘override’ clause. This is what’s known as a MethodImpl. It should not be confused with System.Runtime.CompilerServices.MethodImplAttribute, which controls a method’s eligibility for inlining, its synchronization behavior and other details.
A MethodImpl is a statement in the metadata that matches a method body to a method contract. Here it is used to match the body I.m with the interface contract I::m. Generally, you will see MethodImpls used in this way to match methods to interfaces. But MethodImpls can be used to match any method body to any contract (e.g. a class virtual slot) provided that:
- The contract is virtual
- The body is virtual
- The body and the MethodImpl are defined on the same class
- The contract is defined either on this class or somewhere up the hierarchy (including implemented interfaces).
Once again, it’s open to debate whether MethodImpls require virtual contracts and bodies for sound architectural reasons or for temporary implementation reasons.
The ECMA spec contains the rules for how interface contracts are satisfied by class methods. This explains how the base class’ layout can be at least partially re-used, and it explains the precedence of the two techniques we’ve seen above (class methods match by name and signature vs. MethodImpls which match methods of any name that have the correct signature).
It also mentions one other surprising detail of interface layout. In the example below, we would expect Derived and Redundant to have the same layout. Sure, there’s a redundant mention of interface I on class Redundant, but that seems irrelevant.
interface I { void m(); } class A : I { public virtual void m() {} } class Derived : A { public new virtual void m() {} } class Redundant : A, I { public new virtual void m() {} }
In fact, it is highly significant. Class A has already satisfied the interface contract for I. Class Derived simply inherits that layout. The new method Derived.m is unrelated to I.m. But in class Redundant, we mention interface I in the implements list. This causes the CLR loader to satisfy the interface contract all over again. In this new layout, Redundant.m can be used to satisfy I.m.
If you’re thinking that some of this stuff is pretty subtle, you are right. Normally, developers wouldn’t concern themselves with the different ways that the CLR can satisfy interface contracts. Instead, you would happily code to your language rules and you would trust your IL generator to spit out the appropriate metadata. In fact, one of the reasons we have all these subtle rules in the CLR is so we can accommodate all the different language rules that we encounter.