type system supports both virtual and non-virtual instance methods. 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. Either kind of call instruction can be
applied to either kind of instance method, resulting in four distinct
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:
0x06000007 // call
0x06000007 // callvirt
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.
way to look at this is through ILDASM:
callvirt instance string
has chased the token down for you and recovered some information from it. This consists of the name of the method,
the signature & calling convention of the method, and a class where that
method may be found.
this class hint that’s the most interesting. 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? This isn’t a concern. The purpose of the class hint is to
indicate the contract of the virtual call, rather than the actual override. If you think in terms of VTables, it
selects the slot rather than the method body.
this class hint is still a hint in the CALL (non-virtual) case. 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
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. 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
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. Based on this, the IL generator can make
sane decisions about how to emit class hints in call instructions.
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. Now the contract is known.
Determination of the contract could happen at JIT time or class loading
time. It can be hoisted far above
the actual call.
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.
we can explain all four legal combinations of CALL / CALLVIRT instructions on
virtual / non-virtual methods.
CALLVIRT on a virtual instance
This is the normal virtual dispatch. Given the contract and the receiver, at
call-time we select the appropriate override and dispatch the call.
CALL on a non-virtual instance
This is the normal non-virtual dispatch. When we discovered the contract, we
discovered the appropriate method implementation. Dispatch the call to it.
CALL on a virtual instance
This is a scoped (non-virtual) call. An example is a ‘base’ call in C# where
one virtual method is calling the inherited implementation. 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++.
CALLVIRT on a non-virtual instance
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? There are two reasons.
Some languages allow non-virtual
methods to become virtual in subsequent versions of a type. If callers are already performing
virtual dispatch, they might arguably tolerate this change better.
The JIT performs an important side
effect when making virtual calls on non-virtual instance methods. 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. 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.
is ever simple.