Using COM Instead of Normal DLL Entry

In Visual B++, you can define a more structured interaction with other languages by using COM for your communications. That is, you can use objects defined via a type library that have been implemented in other programming languages like Visual C++. (Indeed, it's difficult these days to not use COM in any Visual C++ development, just as it is in Visual B++.)

However, using COM you can also use objects defined in Visual B++ from, say, Visual C++ or Visual J++. Of course, COM is Microsoft's standard protocol for integrating independently developed software components, so here's a better look at how the connectivity is achieved.

Connectivity Example

Let's say that we use Visual B++ to create an ActiveX DLL that contains a single class called CTest. Let's also say that CTest has a property (defined both as a procedure and as a public data item), a method (sub), and an event. Create the sub with no parameters, name the sub DoIt, and within the sub invoke Visual B++'s MsgBox routine with App.EXEName as an argument. How can this be consumed by a Visual C++ developer?

First compile the project to create the ActiveX DLL. Let's call the project ObjTest, so now we have OBJTEST.DLL. That does it for the Visual B++ piece. Notice that I haven't elaborated on the process here, as I'm assuming that this is not your first foray into building ActiveX components.

Next start Visual C++ and create a new MFC AppWizard workspace (select New from the File menu, then select MFC AppWizard (exe) from the Projects tab). Name the workspace Test. Using the wizard that starts when you click OK, add whatever options you want. If this is your first experience with MFC, C++, or Visual C++ (or all the above), I'd suggest you select Single Document in Step 1 and check the Automation checkbox in Step 3; leave the default choices for all the other options. When you click the wizard's Finish button, the wizard will build an application for you. The resulting application is Windows API, C++, and MFC code. Don't expect to see anything similar to the Visual B++ interface.

Next use the ClassWizard to add the necessary C++ classes to mirror your Visual B++-generated OBJTEST CTest class.

  1. Select ClassWizard from the View menu.

  2. In the MFC ClassWizard dialog box click the Add Class button and select From A Type Library.

  3. Select OBJTEST.DLL from the File Open dialog box. The next dialog box should show _CTest and __CTest highlighted, since it should be your only class. Click OK. The ClassWizard will now add more C++ source code to your project.

Now that all the automatic code has been built, select Find In Files from the Edit menu, enter AfxOleInit, and then hit Enter. Double-click the line that appears in the Find In Files 1 tab in your Output window to go to the code that the IDE found. The code will look something like this:

// Initialize OLE libraries if (!AfxOleInit()) {     AfxMessageBox(IDP_OLE_INIT_FAILED);     return FALSE; } 

This code was inserted because you checked the Automation checkbox in Step 3 of the AppWizard. Before an application can use any of the OLE system services, it must initialize the OLE system DLLs and verify that the DLLs are the correct version. The AfxOleInit routine does all we want it to. We need to run this routine, of course, because we're about to talk to—automate—our OBJTEST DLL via OLE and COM.

Navigate through the code to someplace suitable where you can add the necessary code to automate the DLL. I suggest you use Find In Files to locate void CTestApp::OnAppAbout(). This is a class function (I can't really explain what this is here, so please just carry on) that's used to create the application's About box. Replace the two lines of code between the C-style braces ({}) with the following:

_CTest * pCTest = new _CTest; if(FALSE == pCTest -> CreateDispatch("ObjTest.CTest"))  {     AfxMessageBox("Can't create dispatch table."); } else {     pCTest -> DoIt();     pCTest -> DetachDispatch();     pCTest -> ReleaseDispatch(); } 

You also need to add a statement at the top of the file to include the new header file:

#include "ObjTest.h" 

Next build and run the application. If you followed all my instructions exactly you shouldn't have any trouble building the application. If you do, go over the instructions once more.

So what just happened, from the top?

Using ClassWizard to add the type library caused Visual C++ to add some C++ classes to your project to mirror those in the DLL. Visual C++ will have named these classes the same as your Visual B++ classes prefixed with an underscore. _CTest is our main class.

The code we added to the About box routine creates a new _CTest C++ class instance and stores its address in memory in the variable pCTest (a pointer to a _CTest object, if you will). The C++ class inherits some routines (think of Implements), one of which we now call: pCTest -> CreateDispatch(). This routine connects C++ functions defined in our C++ class _CTest with interfaces in a CTest object—the Visual B++ object, that is. We then call DoIt, our routine that does something. You should see the message OBJTEST appear in a message box when this is called (when you select About from the Help menu). Since we're basically through with our Visual B++ class now, we disconnect ourselves from it, which is what DetachDispatch and ReleaseDispatch do.

NOTE
The class __CTest (two underscores) contains the event we defined.
Can these routines in the C++ classes be called from routines in other languages, perhaps from C? The answer is "Yes," because you can export the routines (make them available as "ordinary exports") by using a class modifier such as Class _declspec(dllexport) _CTest{};. However, this exposes the C++ class methods and properties via their decorated names; each will also be expecting to be passed an instance of the class type via a pointer parameter called this. All in all, not very easy.

NOTE
The code for the connectivity examle can be found on the companion CD in the CHAP11 folder. The project for the CTest class is in the VBJTEST subfolder; the project for the ObjTest DLL is in the OBJTEST subfolder; and the Visual C++ code is in the TEST subfolder.

Calling C++ DLLs from Visual B++

DLLs are separate binary modules that allow developers to share code easily at run time. If you have Visual B++_based client code that needs to use a C++ class (like a dialog box) that lives within a DLL, you basically have three options.

The first option is to write a single C++ function that invokes the dialog box and returns the results to the Visual B++ client. The advantage to this approach is that both the client code and the server code are fairly straightforward. The disadvantage to this approach is that the client code doesn't have a whole lot of control over the dialog box.

The second option is to provide a set of functions in the C++ DLL that manipulate the C++ object. With this approach each function must provide the client code with a handle, and the client code must write a single entry point for each member function it wants to use. The advantage to this approach is that the client has fairly good control over the dialog box. The downside is that the DLL code has to provide wrappers for each class member function (most tedious!), and the client has to keep track of the handle.

The third option is to have the C++ class within the DLL implement a COM interface. The advantage of this method is that the client code becomes greatly simplified. The client gets to use the C++ class in an object-oriented manner. In addition, most of the location and creation details are hidden from the client. This approach means buying into COM. However, that's generally a good thing because just about everything coming out of Redmond these days is based on COM. Using COM from Visual B++ is a good place to start.

Passing a Visual B++ Object to Visual C++

You can pass objects from a function in Visual B++ to most languages that support pointers. This is because Visual B++ actually passes a pointer to a COM interface when you pass an object.

All COM interfaces support the QueryInterface function, so you can use any passed interface to get to other interfaces you might require. To return an object to Visual B++, you simply return an interface pointer—in fact, they're one in the same.

Here's an example taken from the MSDN CDs. The following ClearObject function will try to execute the Clear method for the object being passed, if it has one. The function will then simply return a pointer to the interface passed in. In other words, it passes back the object.

#include <windows.h> #define CCONV _stdcall LPUNKNOWN CCONV ClearObject(LPUNKNOWN * lpUnk) {     auto LPDISPATCH pdisp;          if(        NOERROR ==        (*lpUnk) -> QueryInterface(IID_IDispatch, (LPVOID *)&pdisp)       )     {         auto DISPID     dispid;         auto DISPPARAMS dispparamsNoArgs = {NULL, NULL, 0, 0};         auto BSTR       name = "clear";         if(            S_OK == pdisp -> GetIDsOfNames(IID_NULL, &name, 1, NULL,                                           &dispid)        )         {             pdisp -> Invoke(                             dispid                            ,IID_NULL                            ,NULL                            ,DISPATCH_METHOD                            ,&dispparamsNoArgs                            ,NULL                            ,NULL                            ,NULL                            );         }         pdisp -> Release();     }     return *lpUnk; } 

The following Visual B++ code calls the ClearObject function:

Private Declare Function ClearObject Lib "somedll.dll" _     (ByVal X As Object) As Object Private Sub Form_Load()     List1.AddItem "item #1"     List1.AddItem "item #2"     List1.AddItem "item #3"     List1.AddItem "item #4"     List1.AddItem "item #5" End Sub Private Sub ObjectTest()     Dim X As Object          ' Assume there is a ListBox with some displayed items     ' on the form.     Set X = ClearObject(List1)     X.AddItem "This should be added to the ListBox" End Sub 

Using an ActiveX Control for MLP

What's the difference between an ActiveX in-process server and an ActiveX control? Not much, unless we start talking about n-tier or Microsoft Transaction Server (MTS).

A control resides on a form, but a server doesn't. As such, the server needs to be set up, meaning that a client needs to add a reference to and then create an instance of some server class using code like this:

Dim o As Server.ClassName Set o = New Server.ClassName 

Of course, when o goes out of scope, your class instance is going to disappear, but we all know this, right?

With a control, there's no need to create an object variable to hold the instance (it's like a global class in this respect). You create an instance of a control up front by setting its name at design time. For example, Command1, the name you assign to the control at design time, is at run time an object variable that is set to point to an instance of a CommandButton class object. The control's lifetime is the same as the lifetime of the form on which it resides.

"Ah," you say, "but I can have multiple instances of my server." By either using a control array, using the new Add method on the Controls collection, or by loading multiple control-holding forms you can also have multiple instances of a control. "Ah," you say again, "but I can have many clients use one instance of my server object!" "Ah," I say, "so you can with controls; one form serves many consumers." Hands up all of you who have used just one CommonDialog control throughout your application to handle all your common dialogs!

Ok, enough with the comparison for now. Because controls reside on a form we tend wrongly to think of them as having to be based on, or represent, something that's manifestly visual (well, at least I do), although we know deep down that this is not necessarily the case. A CommonDialog control isn't visible at run time. Even so, controls do present some type of user interface, right? After all, the CommonDialog control shows dialog boxes. Not necessarily, though—think about the Timer control, which has no user interface at run time. The Timer is a good example of using an object that is not wrapped in an ActiveX server (though you might conversely argue that it should be). It's just a component that presents to the programmer a particular way of being consumed. The Timer control is also pretty cool in that it sits in your toolbox (which by now might have many panes in it). A toolbox tab can hold all your server controls; just drag-and-drop them as required.

How about using MLP to create this kind of component? Write the controls in Visual B++, Visual J++, or Visual C++ and then use them from Visual B++. You could write a MatrixInversion or a HashTable server control, or whatever you want. So long as the source language can build controls, you have a natural way of consuming them, in-process!

Here are a few more bits and pieces (minutiae, if you will) about controls and servers:

  • Controls normally self-register when they are loaded (in the DLL sense), so they are potentially a little easier to deploy since they don't need preregistering before they're used.

  • Control references like Command1 cannot be killed by assigning Nothing to them. Such a control reference is always valid, even if the form is unloaded, because if the control is accessed, both the form and the control are recreated (no more Object Variable Not Set messages). Control object references are basically constants (both good and bad, perhaps). If it were legal, we'd have to declare them like this:

    Const WithEvents Command1 As New CommandButton

  • All controls are always in scope within a project; in other words, you can't make them private and non-accessible from outside the form in which they reside. Conversely, a server's object variable can be declared Private. A control object reference is implicitly declared Public.

  • Controls (UserControls, that is) can be brought entirely inside an application or compiled to OCXs. (Servers have the potential to be brought inside and application, also).

  • Servers are easier to consume out-of-process than controls.

  • Controls are easily consumed by other controls. That is, they easily can be used as the basis of another control (a kind of implementation inheritance vs. the strict interface inheritance available to servers).

  • A control's initial state can be set at design time via the Properties pane and, as such, controls do not have to have an implicit null state when they're created. This type of initialization is not possible with servers.

  • Controls can save their state to a property bag. With some simple abstraction, this could be modified to provide a type of object persistence for controls.

  • Controls are supported by user interfaces. _ The Property Page and Toolbox can be used to interact with them (even before they're created).

  • Controls can easily draw or present a user interface—it's their natural "thang," of course. (Imagine seeing how your OLE DB data provider control is linked to other controls at design time!)

  • Control instances are automatically inserted into the Controls Collection, which itself is automatically made available.

  • Controls have a more standard set of interfaces than servers. For example, controls always have a Name property (and a HelpContextID, and a Tag, and so forth).

  • Controls accessed outside their containing form need to be explicity scoped to—they can't be resolved via the References list, in other words. Controls can be made more public by setting an already public control-type object reference to point to them, making them accessible to anyone with access to the object reference.

  • Control arrays are, well, they're for controls!

  • Controls can run in the IDE when they're added to a project at design time, so they are a little easier to set up and debug early on.

  • Making a control work asynchronously requires a nasty kludge or two, because controls can never easily be running out of process. To do any form of asynchronous work they need their own thread (or a timer).

  • In-process servers present an extra nice ability, through global classes, to act as repositories for compiled, yet shared, code libraries and can be used to create objects (containing modules) that are as useful as VBA itself (such as in the object reference—VBA.Interaction.MessageBox and so on). This functionality is also available using controls. At TMS we wrapper the entire C run-time library in this way and make the library available through use of a control typically called CLang. So CLang.qsort gets you the Quick Sort routine and so on. Likewise, we also wrapper access to the Registry using a Registry control. So for example you might access the Registry with commands such as RemoteRegistry.ReadKey and LocalRegistry1.WriteValue.

I give no recommendation here as to which you should use where—there are just too many variables and too many terms to the equation. You decide.

Mixing in Assembly Language

One of the easiest ways to build in Assembler is to build in C, because C allows you to write in Assembler using an _asm directive. This directive tells the compiler that it's about to see Assembler, so it doesn't do anything with your code. The reason for this is that the C compiler's natural output is Assembler; this is then assembled, during the last pass of the compiler, into machine code. Most C compilers work like this, so it's very easy for any C compiler to support the inclusion of Assembler. The really great thing about doing your Assembler work in C is that you can provide all the boilerplate code using straight C (which saves you from having to fathom and then write all the necessary prologue and epilogue code for the call and parameter resolution stuff). You can then tell the compiler to generate Assembler from C. This process allows you to rough out the bare bones in C and then fine tune the code in the Assembler generated by the compiler. It's also a great way to learn about Assembler programming.

Other Languages

I opened this chapter talking about COBOL, so I guess I'd better briefly describe how you get to it from within Visual B++. The first step is to find a COBOL compiler that can create DLL code—Micro Focus' COBOL Workbench version 4 will do nicely (version 3.4 is the latest 16-bit version). The rest of the steps are pretty obvious. (See Q103226 in the Microsoft Knowledge Base for more information.) You're going to call into the DLL to get at your heritage code. Why rewrite when you can re-position?

Maybe you have a bunch of scientific routines to write and your language of choice for these is FORTRAN (DIGITAL Visual Fortran 5.0 is a good choice here—MLP is especially easy with Visual Fortran 5.0 as it's based on Microsoft's Developer Studio).

Building your own little language

I'm getting off the topic a bit so I'll be brief here. Specialized, so_called "little languages" (actually some aren't so little) can be easily created using tools such as lex and yacc. (These tools can be used to build type 2 (context-free) languages as classified by the Chomsky language hierarchy). These tools came from the UNIX world originally but are now widely available for other operating systems, including Windows and DOS. The tool lex builds lexical analyzers and yacc (which stands for Yet Another Compiler Compiler) builds parsers. For example, lex can build a program that can break down code like x = a * b * c() / 3 into identifiable chunks, and yacc can build a program that can check that the chunks make syntactic sense, in the order identified by lex (which is as they're written above). As well as syntax checking your code, yacc normally generates output code to perform whatever it is that your grammar has just described, in this case math.

Note that yacc can generate any kind of code—it can output C, C++, Assembler, COBOL, or Visual B++. So by using lex and yacc you can create grammars and language compilers to perform specialized tasks. If you want to learn more about these tools see the Mortice Kern Systems Inc. Web site at www.mks.com.

Versioning Components

When building applications from components it's vitally important to know what the version number is of the component you're using. After all, you wouldn't want to link with an old buggy version of a control would you?

Using VERSIONINFO

All Visual B++ applications (I'm including ActiveX servers here) have access to a VERSIONINFO resource—Visual B++ inserts one of these into every project for you automatically. If you build an empty project to an EXE and pull out its version information using Visual Studio (select Resources from the Open dialog box in Visual C++), it'll look something like this:

1 VERSIONINFO FILEVERSION    1,0,0,0 PRODUCTVERSION 1,0,0,0 FILEFLAGSMASK 0x0L FILEOS        0x4L      /* Means VOS_WINDOWS32 */ FILETYPE      0x1L      /* Means VFT_APP       */ FILESUBTYPE   0x0L      /* Means VFT2_UNKNOWN   */ BEGIN     BLOCK "StringFileInfo"     BEGIN         BLOCK "040904b0" /* Means English (United States) */         BEGIN             VALUE "Comments", "\0"             VALUE "CompanyName", "TMS\0"             VALUE "FileDescription", "\0"             VALUE "FileVersion", "1.00\0"             VALUE "InternalName", "Project1\0"             VALUE "LegalCopyright", "\0"             VALUE "LegalTrademarks", "\0"             VALUE "OriginalFilename", "Project1.exe\0"             VALUE "PrivateBuild", "\0"             VALUE "ProductName", "Project1\0"             VALUE "ProductVersion", "1.00\0"             VALUE "SpecialBuild", "\0"         END     END     BLOCK "VarFileInfo"     BEGIN         /* More language stuff. 4b0 is 1200 dec */         VALUE "Translation", 0x409, 1200      END END 

The bold lines denote the application's version number. Of course, you don't have to have anything to do with this raw resource in Visual B++ because, like most things, this data structure has an IDE interface to it. (See Figure 11-3.)

Figure 11-3 Version information in the Visual B++ Project Properties dialog box.

The most important item in a VERSIONINFO is the version number. In terms of code, version numbers are held in App.Major, App.Minor, and App.Revision. I urge everyone to expose these properties through a Project class. If you want to know what version of a DLL or EXE you have, all you have to do is instantiate the component's Project class and ask it!

Dim o As Component.Project Set o =  New Component.Project MsgBox CInt(o.Major) & "." & CInt(o.Minor) & "." & CInt(o.Revision) 

Using a GUID

COM assists with versioning. In COM, the version number of the DLL becomes somewhat insignificant. Strictly speaking, the version number is used to indicate which version of the built component you're using. The version number changes whenever the server or component is rebuilt, but it says nothing about what services are available. These services are specified via a GUID. In COM, a GUID is used to version the component's interfaces—facilities it provides, if you prefer. The GUID states what interfaces are available, not which build of the interfaces you're currently using. If the component is significantly different from what you're expecting (meaning that its interface, and thus the services it provides, has changed), its GUID will be different from the GUID of the component you're expecting. There is strictly a two-level version at work here, the actual version number and the GUID. You probably want to check both.

Conclusion

Visual B++, Visual C++, and Visual J++ reflect radically different styles of development. While Visual B++ is a higher level environment especially suitable for implementing user interfaces, Visual C++ is known for providing greater programming control and performance. Java is the man in the middle and is especially relevant for cross-platform (write once run anywhere) and Web development.



Ltd Mandelbrot Set International Advanced Microsoft Visual Basics 6. 0
Advanced Microsoft Visual Basic (Mps)
ISBN: 1572318937
EAN: 2147483647
Year: 1997
Pages: 168

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net