Greetings from DLL Hell

Assembling components to construct software holds great promise for the future of software engineering. But first, you must overcome the challenge of versioning and components.


October 01, 1999
URL:http://www.drdobbs.com/greetings-from-dll-hell/184415748

October 1999: Beyond Objects: Greetings from DLL Hell

As the speed of change in both business and technology accelerates, it's increasingly clear that

software development needs to move from a craft to a modern industrial process capable of using

componentization to reduce cost and time-to-market. In response, Software Development is launching

a new column, "Beyond Objects," to address the architecture, process, and technology issues of

component-based development (CBD), and discuss related technologies that developers are using

today, including: COM and ActiveX, client-side and Enterprise Java Beans, CORBA, and software

patterns. With contributions from a rotating group of prominent software methodologists, the

mission of the "Beyond Objects" column is to provide careful, thoughtful, and timely insights

to help developers and team leaders realize the benefits of component-based tools and techniques.

In this month's column, Clemens Szyperski, author of the Jolt Award-winning book Component

Software—Beyond Object-Oriented Programming (Addison-Wesley, 1998), discusses approaches

to versioning component-based software and avoiding DLL Hell.

—Roger Smith

A new software package is installed and something loaded previously breaks. However, both packages

work just fine in isolation. That is not good, but it is state-of-the-art. On all platforms, there

are essentially two possibilities: deploy statically linked and largely redundant packages that don't

share anything but the operating system—or you'll have DLL Hell on your doorstep. The former

strategy doesn't work well, either. Operating systems change, and it's unclear what is part of the

shared platform—device drivers, windowing systems, database systems, or network protocols.

Let me step back for a moment. Constructing software by assembling components holds great promise for

next-generation software engineering. I could even go as far as to claim that it isn't engineering

before you master this step—it's mere crafting, much like state-of-the-art manufacturing prior

to the Industrial Revolution. So, clearly, we should begin building component-based software.

There are downsides, however. Moving from the crafting of individual pieces to lines of products based

on shared components undermines our present understanding and practice of how to build software.

Step by step, you must question your current approaches, either replacing or "component-proofing"

them as you go. Here, I will look at the interaction of versioning and components, a conundrum so

deep that even our intense spotlight might fail to illuminate it. Surprisingly, there are practical

solutions to much of this problem.

Relative Decay

Why do you version software? Because you got it wrong last time around? That is one common reason,

but there is a bigger and better one: Things might have changed since you originally captured

requirements, analyzed the situation, and designed a solution. You can call this relative decay.

Software clearly cannot decay as such—just as a mathematical formula cannot. However,

software—like a formula—rests on contextual assumptions of what problems you need

to solve and what best machinery to do so. As the world around a frozen artifact changes,

the artifact's adequateness changes—it could be seen as decaying relative to the changing

world.

Reuse Across Versions

Once we accept that we must evolve our software artifacts to keep them useful over time, we must

decide how to achieve this. A straightforward answer is: throw the old stuff away and start from

scratch. Strategically, this can be the right thing to do. Tactically, as your only alternative,

this is far too expensive. To avoid the massive cost of complete restarts for every version or

release, you can aim for reuse across versions. In a world of monolithic applications, you traditionally

assumed that you would retain the source code base, meddle with it, and generate the next version.

The strategic decision to start from scratch is thus reduced to throwing out parts of the source

code base.

With the introduction of reuse of software units across deployed applications, the picture changes.

Such units are now commonplace: shared object files on UNIX, dynamic link libraries on Windows, and

so on. (I hesitate to call these components for reasons that will become apparent shortly.)

Applying the old approach of meddling with, and rebuilding, such shared units tends to break them.

The reason is simple: if multiple deployed applications exist on a system that share such a unit,

then you cannot expect all of them to be upgraded when only one needs to be. This is particularly

true when the applications are sourced from separate providers.

Why does software break when you upgrade a shared unit that it builds on? There are a number of

reasons and all of them are uniformly present across platforms and approaches. Here is a list of

important cases:

Unit A depends on implementation detail of unit B, but that is not known to B's developer. Even A's

developer is often unaware of this hidden dependency. A lengthy debugging session when developing A

might have led to hard-coding observed behavior of B into A. With the arrival of a new version of B,

A might break.

Interface specifications are not precise enough. What one developer might perceive as a guarantee

under a given specification (often just a plain English description), another might perceive as not

covered. Such discrepancies are guaranteed to break things over time.

Unit A and unit B are coupled in a way that isn't apparent from the outside. For example, both depend

on some platform API that is stateful, or both share some global variables. The resulting fragility

prevents units A and B from safely being used in another context and often even in isolation.

Unit A and unit B are both upgraded to a new version. However, some of the connections between A

and B go through a third unit that has not been upgraded. This "middleman" has a tendency to present

A to B as if A were still of the old version, leading to degraded functionality at best and

inconsistencies at worst.

If your shared units declared which other units they depended on and strictly avoided undocumented

coupling, then you could call them components. Actually, they should also be well documented and

satisfy a few other criteria, such as specification of required—besides provided—interfaces

and separate deployability (adherence to some "binary" standard).

A Middleman Scenario

Of all the problems introduced earlier, the last requires special attention. It is difficult

enough to consider what happens at the boundary between two components if one is upgraded while

the other is not. However, introducing a middleman scenario truly separate the half-working

(really, the half-broken) from true solutions.

Consider the following case, as shown in Figure 1. Our component ScreenMagic supports some interface

IRenderHTML. An instance (object) created by ScreenMagic implements this interface (among others).

Our component WebPuller uses this instance to render HTML source that it just pulled off the World Wide

Web. However, WebPuller's instances don't acquire references to ScreenMagic's instances directly.

Instead, the ScreenMagic instances are held on to by some component Base, which WebPuller contacts

to get these instances. In fact, WebPuller doesn't know about ScreenMagic, just IRenderHTML,

and contacts Base to get the rendering object to be used in some context. At the same time,

Base doesn't use IRenderHTML—it just holds onto objects that implement this interface.

Figure 1: The Middleman Scenario

Now assume that a new version of HTML comes along and only WebPuller gets upgraded initially. If you

just modify the IRenderHTML's meaning, then Base won't be affected. However, in an installation that

holds the new WebPuller and the old ScreenMagic, you might get unexpected error messages from

ScreenMagic. (This is not a perfect example since HTML was so ill-defined from the beginning that

no existing software can guess at what is meant and do something regardless. It is thus unlikely

that you will get an error message from ScreenMagic, but it still might crash.)

This incompatibility problem is caused by changing the contract of an interface without changing all

the affected implementations (which is generally infeasible). To avoid such a problem, you could

instead define a new interface, IRenderHTML2, that specifies that the used HTML streams will

be of a newer version. If you managed to get WebPuller instances to implement both IRenderHTML

and IRenderHTML2, you can eliminate the incompatibility. Since Base and ScreenMagic aren't aware

of IRenderHTML2, they will continue to work via IRenderHTML.

Next, you can upgrade ScreenMagic to also support IRenderHTML2, but still leave Base as it is.

Since Base doesn't use IRenderHTML, there shouldn't be a problem. However, since Base cannot

statically guarantee that returned objects would implement IRenderHTML2, WebPuller will now

need a way to query objects for whether they implement IRenderHTML2 or merely IRenderHTML.

A standard approach is to use classes that implement multiple interfaces. For example, you

would write:

class ScreenMagicRenderer implements IRenderHTML, IRenderHTML2 ...

Then, WebPuller would use dynamic type inspection to find out whether the rendering object

obtained from Base implements IRenderHTML2:

if (aRenderer instanceof IRenderHTML2) { ...new version... }

else { ...old version...}

Now, all combinations of old and new can coexist and cooperate. This coexistence is very important—

without it, there is no plausible story of components as units of deployment. DLL Hell is caused by

the inability of newer DLLs to coexist with older ones. Of course, it isn-t necessary to continue

supporting old versions (old interfaces) for all time. After declaring deprecation, you can eventually

drop interfaces. The point is that you must maintain a window of permissible versions and ensure

those versions coexist.

The Devil in the Detail

It wouldn't be DLL Hell if the devil weren't in the detail; the approach I've outlined doesn't really

work in C++, Java, and many other object-oriented languages. In these languages, a class merges all

methods from all implemented interfaces (base classes). To avoid clashes, you must ensure that all

methods in IRenderHTML that need to change semantics are renamed in IRenderHTML2. Then a class like

ScreenMagicRenderer can still implement both. Renaming methods in addition to renaming the interface

itself is inconvenient, but possible.

A second nasty detail is interface inheritance. When interfaces are derived from other interfaces

defined elsewhere, the concept of renaming all method names when renaming the interface becomes

infeasible. If faced with this situation, the cleanest way out is to not rename methods and to

use adapter objects instead of classes that implement multiple interfaces. In other words:

class ScreenMagicRenderer implements IRenderHTML ...

class ScreenMagicRenderer2 implements IRenderHTML2 ...

This way, you have avoided all problems of method merging. However, the component Base is in

trouble again'if it only knows about IRenderHTML, it will only hold on to instances of

ScreenMagicRenderer, not ScreenMagicRenderer2. To resolve this problem, you could introduce a

standard way of navigating from one to the other. Let's call this INavigate:

interface INavigate {

Object GetInterface (String iName); //*GetInterface returns null if no such interface */

}

Now, WebPuller would get an object that implements IRenderHTML from Base, but would ask that object

for its IRenderHTML2 interface. If the provider were the new ScreenMagic, then that interface would

be supported and WebPuller would use it.

Supporting combinations of versioned components in this manner is plausible. After smoothing some

rough edges and institutionalizing this approach as a convention, it would enrich most existing

approaches. You may have picked up by now that this is exactly how COM does it (where INavigate

is called IUnknown, GetInterface is called QueryInterface, and GUIDs are used instead of strings

for unique interface naming).

Curbing the Problem

If COM solves this problem, why does DLL Hell still happen on Windows platforms? Because a large

number of DLLs are not pure COM servers but instead rely on traditional procedural entry points.

High degrees of implicit coupling are another problem. Clearly, DLL Hell is a problem that is,

in one form or another, present on practically all of today's platforms—although, as I've sketched

in this column, there are approaches that help curb—if not solve—this problem today.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.