Multiple inheritance for interfaces in Delphi for .NET

By: John Kaster

Abstract: Update 2 of the Delphi for .NET preview includes compiler enhancements for multiple inheritance for interfaces

By Danny Thorpe and John Kaster

Multiple inheritance (MI) for interfaces has been implemented and appears in update 2 of the Delphi for .NET preview, which is available to registered users of purchased versions of Delphi 7. To help explain what this means, let's look at some code that illustrates this new language feature.

  IFoo = interface
     procedure Hello;
     procedure Walk;

  IBar = interface
     procedure Hello;
     procedure Talk;

  ICombo = interface(IFoo, IBar)
     procedure ChewBubbleGum;

  TMyClass = class(TObject, ICombo)
     procedure Hello;   // bound to IFoo.Hello and IBar.Hello by default
     procedure Walk;
     procedure Talk;
     procedure ChewBubbleGum;

   I: ICombo;
   F: IFoo;
   X: TMyClass;
   X := TMyClass.Create;
   F := X;
   I := X;
   I.Hello;   //  syntax error: ambiguous call (IFoo.Hello and IBar.Hello)
   IBar(I).Hello;   // ok
   (I as IBar).Hello;   // ok
TMyClass is type compatible with IFoo and IBar, even though it only declares that it implements ICombo. In Win32, you must explicitly declare all interfaces that your class supports, including ancestor interfaces. This requirement is an historical artifact of some OLE2 bugs with IClassFactory and IClassFactory2, and some other interfaces.

Delphi for .NET is not affected by this OLE2 implementation quirk. When you declare that a class implements an interface in Delphi for .NET, the compiler automatically adds all the ancestors of that interface to the class as well. In the example above, TMyClass implements interfaces IFoo, IBar, and ICombo, even though it only declares support for ICombo.

If the names and parameter signatures of two methods in two interfaces are identical (see Hello in the example above), you cannot call those methods using an interface that inherits multiple identical names\signatures from ancestors - you must typecast to the specific interface type to disambiguate name collisions.

Support for MI Interfaces also means you now have full access to inherited members of interfaces imported from CLR.

What about Delphi 8?

Multiple inheritance for Interfaces will also be implemented in the dcc32 compiler for Delphi 8.

Delphi 8 will still have to support the "denial of ancestors" policy to work around OLE2 bugs such as IClassFactory/IClassFactory2 behavior. How that will be accomplished alongside new support for multiple inheritance of interfaces has not been decided. In all likelihood, there will be some inheritable attribute or directive attached to IUnknown such that all OLE interfaces (inheriting from IUnknown) have the ancestor denial behavior while all internal interfaces (inheriting from IInterface but not IUnknown) will have the ancestor inclusion behavior.

A bit of history

Early versions of OLE defined an IClassFactory interface that an OLE\COM server module was required to implement and register so that the system could request instances of the COM object from the server module. It's the factory design pattern.

IClassFactory did not provide any means of access control. If someone bought an application that included a COM server, there was no way to prevent the person from using that COM server in new applications they write - vendors wanted a way to distinguish between people who purchased the COM server for development versus people who obtained the COM server as part of an application and should not have development rights.

In OLE2, Microsoft introduced a new interface IClassFactory2 inheriting from IClassFactory. This new interface added methods to support license key testing and querying. If a valid key was not provided, the COM server could refuse to instantiate its classes.

The problem was in COM itself. To load a module, COM would load the DLL, GetProcAddress on a well-known entry point that was supposed to be exported from the DLL, call the DLL function to obtain an IUnknown interface, and then QueryInterface for IClassFactory. The problem was, when Microsoft added support for IClassFactory2, they added the QueryInterface for IClassFactory2 after the existing code that queried for IClassFactory. IClassFactory2 would only be requested if the query for IClassFactory failed.

Thus, COM would never request IClassFactory2 on any COM server that implemented both IClassFactory2 and IClassFactory. The only way to support control licensing provided by IClassFactory2 was to have some way to implement a descendent interface without responding to requests for the ancestor of that interface.

The reason this wasn't a problem for COM servers produced using the Microsoft SDK tools was that construction of COM interfaces was entirely a manual source-code process (in C source code). Interface definitions were written as structures of function pointers in C and did not have any kind of inheritance. There wasn't any automatic QueryInterface reply service provided by the tools. You had to write your C code to respond to queries for IClassFactory2 yourself. The problem wasn't really discovered until Delphi and Visual Basic provided tools to generate consistent COM interfaces and automatic QueryInterface handlers.

This bug existed in COM for a long time. Microsoft said that they couldn't fix the COM loader with an OS service pack because both Word and Excel (at the time) relied on the buggy behavior. Regardless of whether it's fixed in the latest releases of COM or not, Borland has to provide some way to preserve this behavior in Win32 Delphi for the forseeable future. Suddenly adding all ancestors into an implementing class that weren't there before is very likely to break existing code that unintentionally falls into the same pattern as the COM loader.

This problem may have been encountered again in some of the OLE document interfaces. There are very few certainties when dealing with OLE document view interfaces.

Server Response from: ETNASC04