More fun with call vs callvirt

A while back I posted a little bit about the one implication of call vs. callvirt and the other day I ran into another one that I thought I would share. This one is part of class of issues where C# or VB developers assume that if their language can’t do it, it is not doable. It turns out that is not always true and you can get yourself in trouble for thinking that way.

Let’s say I have a little collection that I want to ensure only stores even numbers in it. As Whidbey is not quite out yet, I can’t use List<int>, so I subclass for ArrayList and customize the add method to ensure that only even numbers get in.

      public class EvenCollection : ArrayList

      {

            public EvenCollection()

            {

            }

            public override int Add(object value)

            {

                  if (value is Int32 && IsEven ((int) value))

                        return base.Add (value);

                  else throw new Exception ("Value must be a prime int");

            }

            internal bool IsEven (int value)

            {

                  return value % 2 == 0;

            }

      }

I repeat for all the other methods on ArrayList which allow new elements to be added to the list.

Then I do a couple of tests in C# to be sure everything is working as expected, and indeed it does.

      EvenCollection ec = new EvenCollection();

      ec.Add (2); // works

      ec.Add (3); //throws as expected

Just to be sure I really do understand OOP 101, I make sure that calls through the base class (ArrayList) still execute my Add() method, and indeed they do:

      ArrayList l = new EvenCollection();

      l.Add (2); // works

      l.Add (3); //throws as expected

 

But here is the trick, try this is C++. Calling the method the normal way works as expected, but C++ supports the ability to call any implementation of a virtual method, not just the most derived.

      EvenCollection* ec = new EvenCollection();

      ec->Add(

__box(2)

); //works as expected

      ec->ArrayList::Add(

__box(3)

); //Utz! Does not throw an exception

The syntax “ArrayList::Add” tells the compiler to call the implementation of Add() on the ArrayList method rather than the most derived one. This is not supported in C#\VB.NET but IS supported in the CLR.

This is the basic difference between the call and the callvirt instructions that was the subject of a recent contest

. Let’s look at the IL:

IL_001b: callvirt instance int32 [OverrideDemoLibrary]OverrideSecIssue.EvenCollection::Add(object)

IL_002a: call instance int32 [mscorlib]System.Collections.ArrayList::Add(object)

The call at 001b is the same call the C# and VB make… A callvirt. The call on 002a is one that only C++ supports (that I know of), it is a call to a virtual member. This is legal and verifiable IL.

Why is this important?

You should be aware of this issue anytime you are using virtual members to guarantee semantics for correctness or security issues. Imagine if this was a list of only authenticated users or strings that have been validated. The lesson here to not inherit from ArrayList… It is meant to be delegated to rather than inherited from.

Note: this is a repost