Give Your Clients a Choice: DLL or EXE?

Introduction

In VB (and COM in general), components can be compiled into two resultant executable types: in-process DLLs or out-of-process EXEs. As many articles have shown, the in-process calls are always faster (click here to see some data on this). However, the out-of-process package will not bring down the client if a memory exception occurs in the code, or so the documentation from Microsoft says. I've never been able to find any articles that demonstrate this, so I decided to whip up an example to determine if this really works or not. In the process (no pun intended) I saw how powerful type libraries can be in VB, so I decided to turn it into a full article to demonstrate not only the differences between in-process and out-of-process components, but how interface definitions in type libraries can really make your COM coding life easier. (Note: If you're interested in the source code, please go to the end of this article where you'll find a link to a ZIP file that contains everything you'll need to examine this project in detail. I'm also assuming that the reader has a fundamental understanding of VB and COM.)

How to Create a Memory Exception

Before I begin, I need to come up with a way to make a memory exception. Usually that happens because of faulty coding in a third-party component that we're using (memory exceptions are never our fault (pun intended)), so how can I make an exception on purpose? VBPJ had an article on exceptions in the May 1999 issue that I could use to raise the exception, but I'll use an easier approach. VB allows you to do some optimizations on your code, which is available from the following window:

Note that the Remove Array Bounds Checking checkbox is checked. When it's unchecked, VB will raise an error if an element is accessed that isn't valid. If this box is checked, VB won't make this sanity check. This optimization makes array usage much faster, but now there's the danger of throwing an exception if I go beyond the array's allocated memory. Well, this is exactly what I want. It's pretty easy to find the upper bound of an array via the UBound function, so I'll get that value and try to access the array 500 elements beyond that. This doesn't guarantee that I'll throw an exception, because it's possible that spot in memory is OK for the application to access. But it's a safe bet that I'll crash something eventually.

Making the Component Design Easier

I'm not one for reinventing the wheel if possible. If I can grab some code from someone who's already done what I want to do, that's great. If I can use someone's component to execute what I need, that's even better. In this example, I have to create two components, a DLL and an EXE. Personally, I don't want to type longer than necessary, so I tried to think of a way to make my life eaiser, and two ideas immediately came to mind:

  • Since the class's implementation is the same in both components, I can reuse the class module file in each project. That's a good thing.
  • Since each class will act the same way, I could create an interface definition via IDL to compile a type library. This interface would be implemented by the class, and therefore the client would not need to reference the DLL or the EXE component (this point will become clearer later). That's even better.

As you'll see, this reuse will make the development time very quick.

The IDLLEXE Interface Definition

The first thing I did was to create the type library via the DLLEXE.idl file that defined the following interface:

interface IDLLEXE : IDispatch
{
    HRESULT SafeCall([in, out] SAFEARRAY(long)* ArrayToSearch, [out, retval] long* RetVal);
    HRESULT UnsafeCall([in, out] SAFEARRAY(long)* ArrayToSearch, [out, retval] long* RetVal);
};

For those of you who are unfamiliar with IDL, this syntax probably doesn't make a whole lot of sense at first glance. I won't go into the guts of IDL here, but let's cover the essentials for now. Note that there are two methods, SafeCall and UnsafeCall. Both take the same number of arguments, an array called ArrayToSearch of type Long and a Long argument named RetVal. This second argument is actually the return type we're all familiar with in VB (hence the retval keyword); VB doesn't actually return the HRESULT code. The HRESULT value is a COM error code that returns the success or failure of the call (among other things). If the call was in error, VB translates the offending call into a VB-generated error (like that annoying 429 error that most VB programmers really hate getting!). If the call was OK, VB will return the value in RetVal via the return value of the method.

As the names of the methods suggest, the SafeCall will try to act nicely, but the UnsafeCall is yelling at you to save your code before you trod upon its' unholy territory. However, an interface definition does nothing in itself, because there is no implementation associated with it - that's what I did later on in our DLL and EXE components.

To get a type library out of this definition, I used the MIDL compiler that comes with Visual Studio. It's a command-line tool that spits out a bunch of files depending on the numerous switches given. Suffice to say, I won't cover all of them, but by adding the /tlb switch, I created a type library file based on the IDL file given.

Creating the DLL and EXE Components

With the type library created, I moved on to making the DLL and EXE components (note that I'll only go over the DLL project. Since the EXE component is exactly the same, there's no need to cover all of the details twice.) I named the ActiveX DLL project DLLComponent, turned off the bounds checking, and created one class, CDLLEXE. Remember, both the DLL and EXE components are defined the same way save for the resulting executable format, so there's no need to create a different class file for both components.

Once the project was loaded, I referenced the type library file by using the References window:

Since the type library wasn't in the list, I used the Browse button to find and load the type library file (this added the correct registry entries). Then, in my CDLLEXE I implemented the IDLLEXE interface:

Implements DLLEXE.IDLLEXE

Now I had two methods to implement, the SafeCall and the UnsafeCall. Let's look at SafeCall first:

Private Function IDLLEXE_SafeCall(ArrayToSearch() As Long) As Long

On Error GoTo Error_IDLLEXE_SafeCall
Dim lngC As Long

If ElementCount(ArrayToSearch) > 0 Then
    For lngC = LBound(ArrayToSearch) To UBound(ArrayToSearch)
    Next lngC
End If

IDLLEXE_SafeCall = lngC

Exit Function

Error_IDLLEXE_SafeCall:

IDLLEXE_SafeCall = -1 * Err.Number

End Function

First of all, what's ElementCount? It's a function that I reuse a lot in projects - it returns the number of elements in a 1-dimensional array (check the source code to see how it works - it's pretty easy). Once I know that I actually have some elements in the array given, I iterate through each element in the array, and return the value of lngC. Pretty boring, but this shouldn't raise an exception.

However, take a look at the implementation for UnsafeCall:

Private Function IDLLEXE_UnsafeCall(ArrayToSearch() As Long) As Long

On Error GoTo Error_IDLLEXE_UnsafeCall

Dim lngC As Long

If ElementCount(ArrayToSearch) > 0 Then
    For lngC = LBound(ArrayToSearch) To UBound(ArrayToSearch)
    Next lngC
    lngC = lngC + 500
    '  This will throw a really bad error.
    lngC = ArrayToSearch(lngC)
    ArrayToSearch(lngC) = lngC
End If

IDLLEXE_UnsafeCall = lngC

Exit Function

Error_IDLLEXE_UnsafeCall:

IDLLEXE_UnsafeCall = -1 * Err.Number

End Function

Since I don't have any array bound checking anymore, I won't have any protection when I try to access an element of the array well beyond its' upper limit. This should cause the memory exception I'm looking for.

Once the DLL project was compiled, the EXE was insanely easy to make. I made an ActiveX EXE project, named it EXEComponent, turned off the array bounds checking, referenced the type library and imported the CDLLEXE class module file. That's it! That's all I needed to do to compile the EXE project.

Creating the Test Client

The client was pretty easy to make. Here's a picture of what the main form looks like:

The button names make it self-explanatory as to what they're going to do. Each one calls the following function:

Private Sub CallComponent(CalledType As ComponentType, SafetyLevel As ComponentSafety, NumberOfElements As Long)

with the following defined enumerations:

Public Enum ComponentSafety
    Safe
    Unsafe
End Enum

Public Enum ComponentType
    DLL
    EXE
End Enum

Therefore, the Safe EXE Call button makes the following call:

If IsNumeric(txtElements.Text) Then CallComponent EXE, Safe, CLng(txtElements.Text)

Before I get into how CallComponent is implemented, take a look at the following references that this project has:

Note that I'm only making a reference to the type library file; none of the components are referenced. Why? Because each object in both components implement the same interface. There's no reason to reference these components explicitly. Let's drill this point home by looking at the implementation of CallComponent

Private Sub CallComponent(CalledType As ComponentType, SafetyLevel As ComponentSafety, NumberOfElements As Long)

On Error GoTo Error_SafeDLLCall

Dim itfDLLEXE As DLLEXE.IDLLEXE
Dim lngArray() As Long
Dim lngRet As Long

ReDim lngArray(1 To NumberOfElements) As Long

If CalledType = DLL Then
    Set itfDLLEXE = CreateObject("DLLComponent.CDLLEXE")
ElseIf CalledType = EXE Then
    Set itfDLLEXE = CreateObject("EXEComponent.CDLLEXE")
End If

If Not itfDLLEXE Is Nothing Then
    If SafetyLevel = Safe Then
        lngRet = itfDLLEXE.SafeCall(lngArray)
    Else
        lngRet = itfDLLEXE.UnsafeCall(lngArray)
    End If

    If lngRet < 0 Then
        MsgBox "Error occurred!  Error number is " & Abs(lngRet)
    Else
        MsgBox "Number of elements counted is " & CStr(lngRet - 1)
    End If

    Set itfDLLEXE = Nothing
End If

Exit Sub

Error_SafeDLLCall:

MsgBox "Unhandled error!  " & Err.Number & " - " & Err.Description

If Not itfDLLEXE Is Nothing Then
    Set itfDLLEXE = Nothing
End If

End Sub

As you can see, I used the value of CalledType to determine which kind of component to create via the CreateObject function. However, note that itfDLLEXE is dimensioned as the IDLLEXE interface. Since the CDLLEXE class implements this interface in both components, it doesn't matter where the class resides as far as the interface is concerned - only that the resultant class supports the interface that is requested.

The rest of the code is pretty straigtforward. I create an array and pass it into the correct call depending upon the value of SafetyValue. With the client code complete, I compiled it to see what happens. (Note that you should use a compiled version of this code to test the components out. Remember that we're trying to create a memory exception in the components, so if you're testing within the IDE you run the risk of losing any changes that you've made to my code.)

The Results

So what happened when the client was run? Well, when I made a safe call to either the DLL or EXE component with a 50,000 element array, this is what I got:

Nothing unusual - this is to be expected. However, I should note that the first call to the EXE is pretty slow, as Windows and COM have a bit more work to do to load the separate executable and set up the communication between the two EXEs. So let's start looking at the bad calls. When I made an unsafe call to the EXE component, the following screen popped up:

Note that EXEComponents is causing the memory exception and not the client executable. In fact, once I pressed the OK button, the following message box popped up:

Since the client was still alive, I could've kept pressing buttons if I wanted to. Now watch what happened when I made an unsafe DLL call:

Note that this time the memory exception was in the client executable. Therefore, after I pressed the OK button, the client program was terminated.

Conclusions

In this article, I demonstrated a couple of things. It was shown that a bad EXE component isn't as threatening to a client as a DLL component is. I also showed how type libraries and interfaces can make your coding life much easier in VB. Furthermore, I showed that it's fairly easy to release two versions of your components, an in-process and out-of-process component. This gives clients the choice to use an out-of-process component, which is separated from your client but the method calls are slower, or an in-process component, which has very fast method calls but has the danger of bringing down the client when it crashes. Either way, by using the IDLLEXE interface, switching the component type from one to another is very easy. In fact, with a little more work, a developer could create a function called CreateObjectType, which would wrap the CreateObject function and take another parameter similiar to the ComponentType enumeration. If the components were developed similar to the ones in this article, it would be simple to return the correct object reference back to the caller. (One could also look at the vbp files for a DLL and EXE project and determine how one could be created from another. This would make the generation of in-process and out-of-process components a trivial task.)

References & Code

  • All of the source code can be downloaded here. Remember to register the components and type libraries! I have a VBScript file that can register type libraries - click here for further details.
  • Al Major, "COM IDL and Interface Design," Wrox Press
  • Thomas Lewis, "VB COM," Wrox Press
  • Ted Pattison, "Programming Distributed Applications With COM and Microsoft Visual Basic 6.0," Microsoft Press

Blog History