The ActiveX DLL Way of Prime Numbers


Once you have an object-oriented solution, you’re just a few clicks away from an ActiveX DLL server. Here’s all you have to do to turn the CSieve class into a Sieve component. None of the steps require writing code:
  1. Select New from the File menu, and choose ActiveX DLL in the project type list. Name the project SieveBasDllN. This will be a native code DLL component written in Visual Basic.

  2. Remove the dummy class the IDE automatically puts in a new pro­-
    ject, and insert the file SIEVE.CLS (the local CSieve class from the last
    section).

  3. Change the name property from CSieve to CSieveBasDllN. Use Save As commands to name the class file SieveBasDllN.CLS and the project file SieveBasDllN.VBP.

  4. Change the instancing property from private to MultiUse (5). This is the generic way to make a class public. If this class were to be part
    of an object hierarchy and it were created by the top-level class of the
    hierarchy, you might want to set it to PublicNotCreatable (2) so that clients couldn’t create it directly. We discussed the reasons for setting GlobalMultiUse back in Chapter 5. Figure 10-2 on the following page shows the settings so far.

  5. Open the Project Properties dialog box, and fill out blank fields starting with Project Description. The description is optional, but you’ll have a rude server indeed if you fail to provide it. Any client that wanted to register and use your services would have to guess your purpose from the project name. When you write a server, you don’t know who will use it. It might be called from Excel, from Word, or from a program you’ve never heard of. Visual Basic itself is a typical client. It offers a References dialog box for loading and registering servers, and it has an Object Browser for displaying public classes. Both use the description to identify the server.

  6. Write a help file for your server, and provide the filename and context ID of the server help topic in the Help File Name and Project Help ContextID fields. Why doesn’t my server have a help file? Well, uh...
    it has a virtual help file.

  7. Leave the Startup Object as None. In version 4 you had to specify a startup module—even if it did nothing. Now if your startup doesn’t need to do anything, Visual Basic won’t specify it for you automa­ti­cally. Keep in mind that any startup done in a Sub Main specified
    by this dialog box occurs once for the whole component and all the classes in it. It’s not the same as the object-specific startup that occurs in Class_Initialize events.

  8. Ignore the Upgrade ActiveX Controls checkbox. The server has no controls to upgrade.

  9. The SieveBasDllN component has no user interaction, so you mark
    it as having Unattended Execution. This makes it suitable for running with multithreaded clients. I discuss the implications of Unattended
    Execution in “Threads and Synchronization” in Chapter 11.

  10. On the Component tab, I accept the default setting of Project Compatibility. If I were creating a new implementation of an existing component (such as one of the Sieve components from the first edition of this book), I might want to set Binary Compatibility. I’ll talk more about this issue later.

  11. Compile the DLL to native code. The client program also uses a p-code version of the DLL. It’s created in exactly the same way except that
    the name and the compile options are different. When the server is compiled in the IDE, it is automatically registered. I’ll look at component registration in more detail in “Registering components” later in this chapter.


Figure 10-2. Settings for ActiveX DLL server.


Problem: Compare calculating prime numbers functionally, locally, externally, with objects, and with early or late binding.

Problem P-Code Native Code
Local Basic function .1270 sec .0380 sec
Early-bound Local Basic class, one at a time .1750 sec .0400 sec
Late-bound Local Basic class, one at a time .7550 sec .6120 sec
Early-bound Local Basic class, all at once .1460 sec .0330 sec
Late-bound Local Basic class, all at once .1540 sec .0330 sec
Basic global function in DLL .1210 sec .0350 sec
Early-bound Basic class in DLL, one at a time .1710 sec .0390 sec
Late-bound Basic class in DLL, one at a time .8380 sec .6040 sec
Early-bound Basic class in DLL, all at once .1450 sec .0330 sec
Late-bound Basic class in DLL, all at once .1510 sec .0330 sec
Early-bound Basic class in EXE, one at a time 9.8660 sec 9.2240 sec
Late-bound Basic class in EXE, one at a time 20.5320 sec 18.9650 sec
Early-bound Basic class in EXE, all at once .2040 sec .0810 sec
Late-bound Basic class in EXE, all at once .2210 sec .1040 sec
Basic class in control, one at a time .5930 sec .3660 sec
Basic class in control, all at once .1470 sec .0340 sec
Early-bound C++ class in ATL DLL, one at a time NA .0150 sec
Late-bound C++ class in ATL DLL, one at a time NA .2900 sec
Early-bound C++ class in ATL DLL, all at once NA .0150 sec
Late-bound C++ class in ATL DLL, all at once NA .0160 sec
Early-bound C++ class in MFC DLL, one at a time NA .0830 sec
Late-bound C++ class in MFC DLL, one at a time NA .1660 sec
Early-bound C++ class in MFC DLL, all at once NA .0230 sec
Late-bound C++ class in MFC DLL, all at once NA .0240 sec


Conclusion: Switching from a functional approach to an object-oriented approach doesn’t have a significant cost, but you do need to worry about late binding. Passing data across process boundaries has a significant cost, but you can reduce it by using early binding and transferring data in large chunks. Of course, total performance might not be an important goal; some clients could prefer an even flow of data instead of big chunks that take a long time to process. Visual Basic native code isn’t too far behind C++ native code, and when you compare to MFC, Visual Basic is actually faster. That’s because MFC doesn’t have dual interfaces as do Visual Basic and ATL. But notice that MFC is faster for late-bound operations (because it implements a fast, custom version of the IDispatch interface). In most cases, which kind of late-bound server is faster matters about as much as which breed of cart horse is faster.


The client code to access a public class in an ActiveX DLL server is pretty much the same as the code to access a private class in the current application. Only the class and the object variable names change. So instead of looking at this boilerplate, let’s take a quick look at the dark side—late binding. The only reason the Sieve Client program does late binding is to prove how slow it is. The reason I’m showing it here is to demonstrate that it can sometimes be more flexible than early binding.

        ‘ Set variable at run time
Dim sieveLate As Object
Select Case cboServer.ListIndex
Case estBasicLocalClass
Set sieveLate = New CSieve
#Const fUseTypeLib = 1
#If fUseTypeLib Then
Case estBasicDllPCode
Set sieveLate = New CSieveBasDllP
Case estBasicDllNative
Set sieveLate = New CSieveBasDllN
§
#Else
Case estBasicEXE
Set sieveLate = CreateObject(“SieveBasDllP.CSieveBasDllP”)
Case estBasicDllNative
Set sieveLate = CreateObject(“SieveBasDllN.CSieveBasDllN”)
§
#End If
End Select
sieveLate.MaxPrime = txtMaxPrime.Text
If chkAll = vbUnchecked Then
‘ Get one at a time
ms = timeGetTime()
For i = 1 To cIter
sieveLate.ReInitialize
Do
iPrime = sieveLate.NextPrime
‘ More of the same...

An interesting point is that the same variable can be used for all the different kinds of servers. In the early binding branch of this code, every server uses a different class and must have a different object variable. Therefore, the code to calculate the prime numbers must be duplicated for each class, with no difference other than the variable name. In the late binding branch, the same variable can work for any class because the class isn’t selected until run time. As a result, the code needs to be written only once. We could get the same behavior for early binding by writing an ISieve interface and making all the sieve classes implement it. But that’s not the point of this exercise.

Notice that there are two ways of creating a new late-bound object—one using the New statement and the other using CreateObject. In this case, there’s a type library, so New is slightly more efficient. But in most cases where you have a type library, you can use early binding. Use CreateObject for those cases (rare in my experience) where you don’t have a type library or don’t know the name of the class until run time.

NOTE You shouldn’t think of your DLL or OCX components as complete until you select the DLL base address. You do this on the Compile tab of the Project Properties dialog box. Visual Basic gives you a default base address, but if you accept it, your component will have a very good chance of being bumped by one of the other dummies who accepted the default address instead of selecting their own. If two components in the system have the same base address, the operating system will have to relocate one or the other. You don’t want it to be yours. The operating system expects base addresses to be on 64-KB boundaries, so there are 32,512 64-KB chunks in the available address range. You have a very good chance of avoiding everybody else’s range if you select your range randomly. The Address-o-matic program (as seen on TV) doesn’t aspire to being a wizard, but it will assign you a base address that isn’t in my range or in the Visual Basic range. You could enhance it to keep a database of known addresses to avoid, including the ranges for your company, your clients, and major component vendors. By the way, my range is one megabyte starting at &H2E8B0000. Keep out!