Virtual and non-virtual


The CLR
type system supports both virtual and non-virtual instance methods. style="mso-spacerun: yes">  And IL can contain both CALLVIRT and
CALL instructions.  So it makes
sense that IL generators would call virtual methods using CALLVIRT and call
non-virtual instance methods with CALL. 
In fact, this is not necessarily the case. style="mso-spacerun: yes">  Either kind of call instruction can be
applied to either kind of instance method, resulting in four distinct
semantics.


"urn:schemas-microsoft-com:office:office" /> size=2> 


Before
we look at each of those four cases, we need to look at what a general call
(CALL or CALLVIRT) looks like in the IL stream:


size=2> 


style="FONT-FAMILY: 'Lucida Console'"> style="mso-tab-count: 1">      0x68
0x06000007  style="mso-spacerun: yes"> // call
method


style="FONT-FAMILY: 'Lucida Console'"> style="mso-tab-count: 1">      0x6f
0x06000007   // callvirt
method


size=2> 


Well,
that’s not too instructive.  There’s
just an opcode for the call, followed by a MethodRef or MethodDef token for the
method.  Generally you will see a
MethodDef if the method is defined in the same assembly as the callsite, though
IL generators aren’t required to make this optimization.


size=2> 


A better
way to look at this is through ILDASM:


size=2> 


style="FONT-FAMILY: 'Lucida Console'"> style="mso-spacerun: yes">  call style="mso-spacerun: yes">       instance
string callstyle.B::m()


style="FONT-FAMILY: 'Lucida Console'"> style="mso-spacerun: yes">  style="mso-spacerun: yes"> callvirt style="mso-spacerun: yes">   instance string
callstyle.B::m()


size=2> 


ILDASM
has chased the token down for you and recovered some information from it. style="mso-spacerun: yes">  This consists of the name of the method,
the signature & calling convention of the method, and a class where that
method may be found.


size=2> 


It’s
this class hint that’s the most interesting. style="mso-spacerun: yes">  Alarm bells may be going off in your
head.  How can I make a virtual call
(where the override should be determined by the actual type of the receiver) if
the IL stream statically declares the method to call? style="mso-spacerun: yes">  This isn’t a concern. style="mso-spacerun: yes">  The purpose of the class hint is to
indicate the contract of the virtual call, rather than the actual override. style="mso-spacerun: yes">  If you think in terms of VTables, it
selects the slot rather than the method body.


size=2> 


In fact,
this class hint is still a hint in the CALL (non-virtual) case. style="mso-spacerun: yes">  The class that’s mentioned might not
even implement this method directly. 
So long as this class or a base class has the method, the bind attempt
will succeed.


size=2> 


Why
would an IL generator mention a method on a class, when the class doesn’t
implement that method directly?  If
the callsite and the target are in the same assembly, there’s little reason to
do so.  But if multiple assemblies
are involved, versioning can intrude. 
The IL generator might mention a method on a class, but the method could
move up to a superclass in a subsequent version. style="mso-spacerun: yes">  And in the case of chaining calls to
virtual methods up the hierarchy (e.g. ‘base’ calls in C#), the IL generator
should probably mention the immediate base class in order to increase version
resiliency.


size=2> 


In the
face of metadata directives like newslot (e.g. the way C# distinguishes between
‘virtual’, ‘new’ and ‘overrides’ keywords), some of the versioning issues become
quite tricky.  Each language needs
to define what kinds of edits are breaking and which ones are tolerated. style="mso-spacerun: yes">  Based on this, the IL generator can make
sane decisions about how to emit class hints in call instructions.


size=2> 


So, to
recap, the CALL or CALLVIRT instruction gives us a token which gives us the
name, signature, calling convention, and class hint for the method contract to
target.  Then a search is made
upwards from the class hint, until we find an actual method definition. style="mso-spacerun: yes">  Now the contract is known.


size=2> 


size=2>Determination of the contract could happen at JIT time or class loading
time.  It can be hoisted far above
the actual call.


size=2> 


If the
call has non-virtual semantics, discovering the contract also reveals the actual
method definition to execute.  If
the call has virtual semantics, we cannot know the actual method definition to
execute until the call happens.  At
that time, we are given the object to invoke on, so we can use that object’s
actual type to select the appropriate method body.


size=2> 


Finally
we can explain all four legal combinations of CALL / CALLVIRT instructions on
virtual / non-virtual methods.


size=2> 


style="MARGIN: 0in 0in 0pt 0.25in; TEXT-INDENT: -0.25in; mso-list: l0 level1 lfo1; tab-stops: list .25in"> style="FONT-FAMILY: Symbol; mso-fareast-font-family: Symbol; mso-bidi-font-family: Symbol"> style="mso-list: Ignore">· style="FONT: 7pt 'Times New Roman'">        
CALLVIRT on a virtual instance
method


size=2>This is the normal virtual dispatch. style="mso-spacerun: yes">  Given the contract and the receiver, at
call-time we select the appropriate override and dispatch the call.


size=2> 


style="MARGIN: 0in 0in 0pt 0.25in; TEXT-INDENT: -0.25in; mso-list: l0 level1 lfo1; tab-stops: list .25in"> style="FONT-FAMILY: Symbol; mso-fareast-font-family: Symbol; mso-bidi-font-family: Symbol"> style="mso-list: Ignore">· style="FONT: 7pt 'Times New Roman'">        
CALL on a non-virtual instance
method


size=2>This is the normal non-virtual dispatch. style="mso-spacerun: yes">  When we discovered the contract, we
discovered the appropriate method implementation. style="mso-spacerun: yes">  Dispatch the call to it.


size=2> 


style="MARGIN: 0in 0in 0pt 0.25in; TEXT-INDENT: -0.25in; mso-list: l0 level1 lfo1; tab-stops: list .25in"> style="FONT-FAMILY: Symbol; mso-fareast-font-family: Symbol; mso-bidi-font-family: Symbol"> style="mso-list: Ignore">· style="FONT: 7pt 'Times New Roman'">        
CALL on a virtual instance
method


size=2>This is a scoped (non-virtual) call. style="mso-spacerun: yes">  An example is a ‘base’ call in C# where
one virtual method is calling the inherited implementation. style="mso-spacerun: yes">  If it used virtual semantics for this
call, an infinite recursion would result. 
This kind of call is available more generally via the scope resolution
operator ‘::’ in C++.


size=2> 


style="MARGIN: 0in 0in 0pt 0.25in; TEXT-INDENT: -0.25in; mso-list: l0 level1 lfo1; tab-stops: list .25in"> style="FONT-FAMILY: Symbol; mso-fareast-font-family: Symbol; mso-bidi-font-family: Symbol"> style="mso-list: Ignore">· style="FONT: 7pt 'Times New Roman'">        
CALLVIRT on a non-virtual instance
method


size=2>This is the most surprising one. 
Why would someone make a virtual call when the selection of the method
body doesn’t depend on dynamically discovering the type of the receiver? style="mso-spacerun: yes">  There are two reasons.


size=2> 


style="MARGIN: 0in 0in 0pt 0.5in; TEXT-INDENT: -0.25in; mso-list: l1 level1 lfo2; tab-stops: list .5in"> style="mso-fareast-font-family: Tahoma; mso-bidi-font-family: Tahoma"> style="mso-list: Ignore">1) style="FONT: 7pt 'Times New Roman'">     
Some languages allow non-virtual
methods to become virtual in subsequent versions of a type. style="mso-spacerun: yes">  If callers are already performing
virtual dispatch, they might arguably tolerate this change better.


style="MARGIN: 0in 0in 0pt 0.5in; TEXT-INDENT: -0.25in; mso-list: l1 level1 lfo2; tab-stops: list .5in"> style="mso-fareast-font-family: Tahoma; mso-bidi-font-family: Tahoma"> style="mso-list: Ignore">2) style="FONT: 7pt 'Times New Roman'">     
The JIT performs an important side
effect when making virtual calls on non-virtual instance methods. style="mso-spacerun: yes">  It ensures that the receiver is not
null.  In the case of the current
X86 JIT where EAX is scratch and ‘this’ is in ECX, you’ll see code like “mov
eax, [ecx]” right before the call. 
This moves the exception out of some random point in the method body and
delivers it at the callsite.  If you
look at C#’s use of this, they will suppress subsequent calls on ‘this’ so that
only the outer skin of non-virtual instance methods receive this treatment. style="mso-spacerun: yes">  It’s a good heuristic, though obviously
it can be thwarted if the outer caller is using an IL generator that doesn’t
follow this convention.


size=2> 


Nothing
is ever simple.


size=2> 

Comments (3)

  1. >And in the case of chaining calls to virtual methods up the hierarchy (e.g. ‘base’ calls in C#), the IL generator should probably mention the immediate base class in order to increase version resiliency.<<

    Is there any way to do this using Reflection.Emit? I don’t think I can construct arbitrary MethodRefs using Reflection.Emit.

  2. Chris Brumme says:

    I’ve talked to the owners of Reflection Emit. This looks like a hole in our support. For truly dynamic cases, versioning isn’t a concern. But if you are persisting the results (e.g. building a compiler on top of reflection & reflection emit), then you need this.

    I’ve put in a feature request to address this. Obviously it will be triaged against many other requests, so I don’t know when you will get relief.

    Thanks for pointing it out.

Skip to main content