If the shell is written in C++, why not just export its base classes?


ton suggested that since the shell is written in C++, IShell­Folder should have been an abstract class, and then it could have used techniques like exceptions and Inversion of Control.

Okay, first of all, I'm not sure how Inversion of Control is something that requires C++, so I'm going to leave that aside.

Second of all, who says the shell is written in C++? As it happens, when IShell­Folder was introduced in Windows 95, the entire shell was written in plain C. That's right, plain C. Vtables were built up by hand, method inheritance was implemented by direct replacement in the vtable, method overrides were implemented by function chaining, multiple inheritance was implemented by manually moving the pointer around.

const IShellFolderVtbl c_vtblMyComputerSF =
{
 MyComputer_QueryInterfaceSF,
 MyComputer_AddRefSF,
 MyComputer_ReleaseSF,
 MyComputer_ParseDisplayName,
 ... you get the idea ...
};

const IPersistFolderVtbl c_vtblMyComputerPF =
{
 MyComputer_QueryInterfacePF,
 MyComputer_AddRefPF,
 MyComputer_ReleasePF,
 MyComputer_Initialize,
};

struct MyComputer {
 IShellFolder sf;
 IShellFolder pf;
 ULONG cRef;
 ... other member variables go here ...
}

MyComputer *MyComputer_New()
{
 MyComputer *self = malloc(sizeof(MyComputer));
 if (self) {
  self->sf.lpVtbl = &c_vtblMyComputerSF;
  self->pf.lpVtbl = &c_vtblMyComputerPF;
  self->cRef = 1;
  ... other "constructor" operations go here ...
 }
 return self;
}

// sample cast
MyComputer *pThis;
IPersistFolder *ppf =  &pThis->pf;

// sample method call
hr = IShellFolder_CompareIDs(psf, lParam, pidl1, pidl2);
// which expands to
hr = psf->lpVtbl->CompareIDs(psf, lParam, pidl1, pidl2);

// sample forwarder for multiply-derived method
HRESULT STDCALL MyComputer_QueryInterfacePF(
    IPersistFolder *selfPF, REFIID riid, void **ppv)
{
 MyComputer *self = CONTAINING_RECORD(selfPF, MyComputer, pf);
 return MyComputer_QueryInterfaceSF(&self->sf, riid, ppv);
}

So one good reason why the shell didn't export its C++ base classes was that it didn't have any C++ base classes.

Why choose C over C++? Well, at the time the Windows 95 project started, C++ was still a relatively new language for systems programming. While there were certainly people on the shell team capable of writing code in C++, the old-timers grew up with C as their native language, and the newcomers weren't taught C++ in their computer science classes. (Computer science departments still taught primarily C or Pascal, with maybe some Lisp if you took an AI class.) Also, the C++ compilers of the day did not provide fine control over automatic code generation,¹ and since even saving 4KB of memory had a perceptible impact on overall system performance, manually grouping rarely-used functions into the same region of memory of memory (so they could all remain paged out) was still a common practice.

But even if the shell was originally written in C++, exporting the base classes wouldn't have been a good idea. COM is a language-neutral platform. People have written COM objects in C, C++, Visual Basic, C#, Delphi, you-name-it. If IShell­Folder were an exported C++ base class, then you have effectively said, "Sorry, only C++ code can implement IShell­Folder. Screw off, all you other languages!"

But wait, it's worse than just that. Exporting a C++ base class ties you to a specific compiler vendor, because name decoration is not standardized. So it's not just "To implement IShell­Folder you must use C++" but "To implement IShell­Folder you must use the Microsoft Visual Studio C++ compiler."

But wait, it's worse than just that. The name decoration algorithm can even change between compiler versions. Furthermore, the mechanism by which exceptions are thrown and caught is not merely compiler-specific but compiler-version specific. If an exception is thrown by code compiled by one version of the C++ compiler and reaches code compiled by a different version of the C++ compiler, the results are undefined. (For example, the older version of the C++ compiler may not have supported RTTI.) So it's not just "To implement IShell­Folder you must use C++" but "To implement IShell­Folder you must use Microsoft Visual C++ 2.0." (So maybe Bjarne was right after all.)

But wait, it's worse than just that. Exporting a C++ base class means that the base class can never change, because various properties of the base class become hard-coded into the derived classes. The list of interfaces implemented by the base class becomes fixed. The size of the base class is fixed. Any inline methods are fixed. The precise layout of member variables is fixed. Exporting a C++ base class for IShell­Folder would have meant that the base class could never change. You want support for IShell­Folder2? Sorry, we can't add that without breaking everybody who compiled with the old header file.

Exercise: If exporting base classes is so horrible, why does the CLR do it all over the place?

Footnote

¹ Actually, I don't think even the C++ compilers of today give you fine control over automatic code generation, which is why Microsoft takes a conservative position on use of C++ in kernel mode, where the consequences of a poorly-timed page fault are much worse than simply poor performance. It will bluescreen your machine.

Comments (22)
  1. Mathieu Garstecki says:

    The CLR can export base classes as it wants because C# classes are implemented differently. I think the IL code doesn't contain method addresses but symbol references (or something close enough), and the JIT computes the actual adresses. The base class implementation can change, because the JIT will always generate the right method calls at execute time, based on the actual metadata of the base class.

  2. Adam Rosenfield says:

    The latest version of Objective-C solves the fragile base class problem by adding an extra level of indirection: all instance variable accesses in a derived class are performed by taking the base object pointer, adding the base class size, adding the derived member offset, and performing the load or store.  This lets the base class size be determined at runtime, but it introduces a potential small performance penalty (one extra add instruction).  So, base class layouts can change without breaking binary compatibility.

    Method calls in Objective-C have always been resilient to fragile base classes, since method names are always looked up dynamically at runtime (with some heavy optimization by the dynamic linker) instead of using vtable offsets that are burned in at compile time.

  3. alegr1 says:

    For what it's worth, the whole KMDF (Kernel Mode Driver Framework) is written internally in C++, even though it only exposes C API.

    Also, in Win8, MS added official (though limited) C++ support to DDK.

  4. Joshua says:

    Short version: Because C++ does not have a stable ABI.

  5. Gabe says:

    The answer isn't some object-oriented buzzword mumbo-jumbo, it's to design the interface to support this feature in the first place! It seems like the current options are "silently fail" (by passing NULL for hwndOwner in the call to EnumObjects) and "show dialog". They could have created a means to return an error from enumeration as a third option. For example, one of the flags might mean "return a special error item in the event of an error while enumerating" or "verify media and return an error from EnumObjects if there is none". Or there could be a function you call at the end of enumeration to return a list of errors.

    Obviously hindsight is 20/20, but this problem shouldn't have been hard to foresee back in 1994 when every Windows computer had a floppy or CD-ROM drive.

  6. pete.d says:

    "The latest version of Objective-C solves the fragile base class problem…"

    Now if Objective-C would solve the fragile constructor problem.  I'm sick and tired of having to worry that I'm actually initializing the base class correctly, just because the language doesn't enforce initializing through the immediate base class rather than some other ancestor class.

  7. My first thought after just reading the headline:

    "Because C++ has no standard ABI and Windows is not tied to any specific C++ compiler.  Everything has to be C compatible.  End of discussion."

    (And who says the consumer of the shell API is written in C++?  What about Delphi?  VB?  Java?  .NET languages?)

  8. Former BE Programer says:

    BE did this, all the APIs were written and exposed as C++ classes… it turned out to be a disaster to maintain and program for. lat out one of the worst API I ever had to work with.

  9. ton says:

    "Okay, first of all, I'm not sure how Inversion of Control is something that requires C++, so I'm going to leave that aside."  

    No its not necessarily required but C++ has better idiomatic support for OOP design techniques over C.

    Anyway my whole post boils down to the fact that I hate error codes and would rather deal with exceptions instead. Lack of stability in the ABI of C++ not withstanding it would have been nice if C++ didn't have that design flaw and nice descriptive exception objects could be used instead of opaque error codes. At least speaking from a modern operating system API design point of view.

  10. Joe says:

    Well, there's another problem, ton. I hate exceptions and would rather deal with error codes.

  11. Aaron.E says:

    I hate exceptions and error codes and would rather nothing ever went wrong.

  12. steveg says:

    @Aaron.E: I hate nothing.

  13. Alex says:

    > Exercise: If exporting base classes is so horrible, why does the CLR do it all over the place?

    All hail MSIL?

  14. Peter says:

    From the article:

    "method inheritance was implemented by direct replacement in the vtable, method overrides were implemented by function chaining…"

    Probably, "method overrides were implemented by direct replacement in the vtable" ?

  15. Nick says:

    @steveg:

    Most people don't like Nothing very much.

    en.wikipedia.org/…/The_Neverending_Story

    :)

  16. Simon Buchan says:

    Related: I'm really sad WinRT is being restricted to Metro apps (along with Windows Store and ARM support, what the heck?) – A native object oriented ABI on par with MSIL would be intensely useful, especially if it were publicly specced and (not just C++!) compiler writers were encouraged to adapt it. Heck, I'm thinking of adapting it if I ever get round to finishing my compiler. (presumably that's legal?)

  17. Ooh says:

    @Simon Buchan: WinRT isn't restricted to Metro apps, it just doesn't make sense to force non-Metro apps to a new ABI due to compatibility issues.

  18. ILoveCPP says:

    I wonder if these new Windows 8 WinRT-based IInspecatble metadata-rich API's are more inefficient than pre-WinRT classical lean-and-mean COM API's.

    …If so, is this a price payed to make it easier for the .NET managed guys to call into Windows?

  19. Simon Buchan says:

    @Ooh: Well, MSDN only has Reference for the APIs *for* WinRT so far, which doesn't list any execution environment restrictions: msdn.microsoft.com/…/br224646.aspx, so unless they just haven't documented it yet, it looks like you *might* be able to RoActivateInstance() a – for example – Windows.Media.Transcoding.MediaTranscoder outside of a Metro package, which is nice – however, it definitely seems like that is something Microsoft would consider unintended usage.

    I'm not asking that there's a full WinRT alternative for every method in the Platform SDK, but at least the idea that you could write a "WinRT desktop app" in the same way you write a ".NET app" would be nice. With a .NET comparible ABI and API, the "lego block" development approach .NET is approaching now could be replicated for native code, and that's something I'd love to see. Presumably, Microsoft would too?

    Don't get me wrong, though: the progress showen on Win8 so far is already impressive to me.

  20. WndSks says:

    @ILoveCPP: Yes there is a little bit of overhead, both with how MSVC calls the methods and the implementation of the "COM" objects, see http://www.interact-sw.co.uk/…/09

  21. 640k says:

    short answer: windows isn't c++ compatible.

  22. Klimax says:

    @640k: So wrong, it's not even funny (If that was joke, you are missing whole smiley as it is similar as your past postings, so can't tell if serious or not)

Comments are closed.