Nigel Thompson
Microsoft Developer Network Technology Group
March 20, 1995
Click to open or copy the files in the HOUSE6 sample application for this technical article.
This article is the sixth in a series that looks at creating and using Component Object Model (COM) objects with Visual C++™ and the Microsoft® Foundation Class Library (MFC). In this article, I'll be looking at the possibilities of using COM objects directly from Visual Basic®. You should note that this is a somewhat theoretical article, inasmuch as I did the evaluation using an internal version of Visual Basic 4.0 (32-bit), which is not yet available. As you will see, although it is possible to use COM objects directly from Visual Basic, it is neither easy nor fun. In a future article, I'll be looking at making the entire problem much simpler by using OLE Controls. The OLE Controls will make use of the COM objects, but provide a much better interface for the Visual Basic programmer. The sample code for this article uses COM objects and bitmaps from the earlier articles. Note that to run these samples, you need the correct registry entries for the COM objects. Please refer to the first article in this series ("MFC/COM Objects 1: Creating a Simple Object") for details on how to do this.
Because I know that many of you out there in programming land are Visual Basic® programmers, I thought it would be interesting to see if our fledgling 32-bit version of Visual Basic would be able to make direct use of the Component Object Model (COM) objects I've created using Visual C++™. My initial goal was to reproduce the HOUSE example entirely in Visual Basic. The short story is that I didn't get there. I possibly could have, but the effort required seemed ridiculous to me compared to the simplicity afforded by OLE Controls. So in this article, I'm going to tell you how far I did manage to get in building a Visual Basic application that uses COM objects, and then I'll follow that up in a later article with some OLE Controls that make the Visual Basic exercise a lot easier.
Let me just emphasize again that this example was built using an internal version of our unreleased Visual Basic 4.0 product and, as such, is simply an example of what you might be able to do with the final product. Because Visual Basic 4.0 is not finished yet, what I've done here may or may not work in the final product. Enough warnings; let's see what using COM objects from Visual Basic involves.
The idea of using COM objects from Visual Basic is all very well, but to be practically possible, the interfaces that the COM objects provide must be designed in such a way that it is possible to pass all their parameters correctly from Visual Basic. If you read the earlier articles in this series, you might guess where I'm headed with this. Let's briefly revisit the IDrawing interface as defined in previous articles:
class IDrawing : public IUnknown
{
public:
// Standard IUnknown interface functions
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid,
LPVOID* ppvObj) = 0;
virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0;
virtual ULONG STDMETHODCALLTYPE Release(void) = 0;
// This interface
virtual HRESULT STDMETHODCALLTYPE Draw(CDC* pDC,
int x,
int y) = 0;
virtual HRESULT STDMETHODCALLTYPE SetPalette(CPalette* pPal) = 0;
virtual HRESULT STDMETHODCALLTYPE GetRect(CRect* pRect) = 0;
};
Have you spotted the problem yet? Right! How is a Visual Basic application going to pass pointers to C++ objects?
So, defining COM interfaces with C++ objects as arguments wasn't too smart. What we need is an interface definition that's usable from C, C++, Visual Basic, and so on. We can achieve this by confining argument types to a set of types we can handle in every language. It turns out that in a 32-bit world, we really only need two types of arguments: a 32-bit thing and a pointer to a 32-bit thing, both of which can be handled in Visual Basic. A Long in Visual Basic is a 32-bit thing, and if any argument is passed using the ByRef modifier, we get a pointer to that argument. All Microsoft® Windows® objects can be passed as parameters using either a 32-bit value or a pointer.
In order to make the IDrawing interface usable from Visual Basic, I redefined it as:
class IDrawing : public IUnknown
{
public:
// Standard IUnknown interface functions
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid,
LPVOID* ppvObj) = 0;
virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0;
virtual ULONG STDMETHODCALLTYPE Release(void) = 0;
// This interface
virtual HRESULT STDMETHODCALLTYPE Draw(HDC hDC,
int x,
int y) = 0;
virtual HRESULT STDMETHODCALLTYPE SetPalette(HPALETTE hPal) = 0;
virtual HRESULT STDMETHODCALLTYPE GetRect(RECT* pRect) = 0;
};
Now we have an interface that uses only Windows native types, object handles (which are 32-bit values), or pointers. I modified all the other interfaces that I had defined to use no C++ classes, and rebuilt the dynamic-link libraries (DLLs) and the HOUSE application to make sure everything still worked. Having made sure the interfaces were (theoretically) callable from Visual Basic, I set about building a Visual Basic application that would create some COM objects.
COM objects are created using the CoCreateInstance function, which is in COMPOBJ.DLL. So the first thing I needed to do was create a function declaration of CoCreateInstance. Let's look at the C definition of this function:
WINOLEAPI CoCreateInstance(REFCLSID rclsid, LPUNKNOWN pUnkOuter,
DWORD dwClsContext, REFIID riid, LPVOID FAR* ppv);
Breaking the arguments down, we have a reference, a pointer, a 32-bit value, a reference, and a pointer. The references are all effectively pointers, so we really have four pointers and a single 32-bit value. This seems like it will be no problem in Visual Basic. The important things to note are (1) which structures the pointers are pointing to, and (2) how possible it is to build those structures in Visual Basic. Well, the only problem with the pointers we have here is the reference to a class ID (rclsid) and the reference to the interface ID (riid). Both of these ID values are 128-bit numbers that we have to find a way to declare and initialize. Here's the structure I used for this:
Public Type GUID
l As Long
w1 As Integer
w2 As Integer
b1 As Byte
b2 As Byte
b3 As Byte
b4 As Byte
b5 As Byte
b6 As Byte
b7 As Byte
b8 As Byte
End Type
To create a COM object, we will need a class ID and an interface ID, so let's define one each of those to use:
' Standard COM interface IDs
Public IID_IUnknown As GUID
' Appliance COM object class IDs
Public CLSID_LightBulb As GUID
Unfortunately, Visual Basic has no way to allow us to initialize these structures when they are declared, so I created some helper routines to simplify this:
' Helper routine to initialize an OLE style GUID
Public Sub DefineOLEGuid(ByRef i As GUID, l As Long, w1 As Integer, w2 As Integer)
i.l = l
i.w1 = w1
i.w2 = w2
i.b1 = &HC0
i.b2 = 0
i.b3 = 0
i.b4 = 0
i.b5 = 0
i.b6 = 0
i.b7 = 0
i.b8 = &H46
End Sub
' Helper routine to set up a GUID from its string representation
Public Sub DEFINE_GUID(ByRef cls As GUID, l As Long, w1 As Integer, w2 As Integer, _
b1 As Byte, b2 As Byte, b3 As Byte, b4 As Byte, _
b5 As Byte, b6 As Byte, b7 As Byte, b8 As Byte)
cls.l = l
cls.w1 = w1
cls.w2 = w2
cls.b1 = b1
cls.b2 = b2
cls.b3 = b3
cls.b4 = b4
cls.b5 = b5
cls.b6 = b6
cls.b7 = b7
cls.b8 = b8
End Sub
Not very exciting stuff, this, but bear with me. Now we can create an initialization function that can be called when the application's main form loads:
Public Sub InitCOMSupport()
' Set up the COM interface IDs.
Call DefineOLEGuid(IID_IUnknown, 0, 0, 0)
' Set up the Appliance COM object class IDs and interface IDs.
Call DEFINE_GUID(CLSID_LightBulb, &H3A015B30, &H41FC, &H11CE, _
&H9E, &HE5, 0, &HAA, 0, &H42, &H31, &HBF)
Call DEFINE_GUID(IID_IDrawing, &H15038B10, &H3D3E, &H11CE, &H9E, _
&HE5, 0, &HAA, 0, &H42, &H31, &HBF)
End Sub
The values shown here for class and interface IDs were taken from the C++ header files. When the application's main form is loaded, all class IDs are set up:
Private Sub Form_Load()
InitCOMSupport
End Sub
Having defined the ID values, we can now create the function definition for CoCreateInstance:
Public Declare Function CoCreateInstance Lib "compobj" _
(ByRef rclsid As GUID, ByVal pUnkOuter As Long, _
ByVal dwContent As Long, ByRef riid As GUID, _
ByRef ppv As Long) As Long
Now we can actually create a COM object. In my sample I added a button to the main form and put the code to create my test object in the button's click function. That way I could run the piece of test code by simply pressing the button. Here's the first part of the function that creates the object:
Private Sub AddLightBulb_Click()
' Try to create a light bulb object.
Dim hr As Long
Dim punkLightBulb As Long
hr = CoCreateInstance(CLSID_LightBulb, 0, CLSCTX_INPROC_SERVER, _
IID_IUnknown, punkLightBulb)
If hr < 0 Then
MsgBox ("Failed to create light bulb. Error " + Hex(hr) + "H")
Exit Sub
End If
CLSCTX_INPROC_SERVER is a constant taken from the OLE header files.
So this doesn't look too bad so far, does it? We have managed to create a COM object from our short piece of Visual Basic code. Now what we want to do is get one of the object's interfaces and make it do something. I wanted to get the light bulb's IDrawing interface and tell it to draw itself onto the form's window area somewhere.
Given a pointer to an object's IUnknown interface to get to any other interface, we need to call IUnknown::QueryInterface. We got the IUnknown pointer back when we called CoCreateInstance, so now all we need is a way to call QueryInterface through the pointer. This is where the trouble starts, because Visual Basic is a language that doesn't know too much about pointers, and it's at this point that we have to abandon Visual Basic and write some C code in a DLL to act as glue between the Visual Basic code and the COM object's interface.
Given that I had to create some "glue" code to go between Visual Basic and the COM objects, I thought it would be a fine idea to try to create a kind of generic "CallObjectMember" function. This would certainly be possible if I were to define a Visual Basic function that took a variable argument list, and somewhere in that argument list it had information about which interface to use and which member to call. Of course, we'd also probably have to figure out what types of data were in the other arguments, too. That was way too much work for this experiment, so instead, I created a number of functions that have a fixed number of arguments. Here are the Visual Basic declarations of the various "glue" functions in my support DLL:
Public Declare Function comQueryInterface Lib "vbsysdbg" _
(ByVal punkObject As Long, ByRef riid As GUID, _
ByRef ppv As Long) As Long
Public Declare Function comAddRef Lib "vbsysdbg" _
(ByVal punkObject As Long) As Long
Public Declare Function comRelease Lib "vbsysdbg" _
(ByVal punkObject As Long) As Long
Public Declare Function comCallStdMember0 Lib "vbsysdbg" _
(ByVal punkObject As Long, _
ByVal dwIndex As Long) As Long
Public Declare Function comCallStdMember1 Lib "vbsysdbg" _
(ByVal punkObject As Long, _
ByVal dwIndex As Long, ByVal dwArg1 As Long) As Long
Public Declare Function comCallStdMember2 Lib "vbsysdbg" _
(ByVal punkObject As Long, _
ByVal dwIndex As Long, ByVal dwArg1 As Long, ByVal dwArg2 As Long) _
As Long
Public Declare Function comCallStdMember3 Lib "vbsysdbg" _
(ByVal punkObject As Long, _
ByVal dwIndex As Long, ByVal dwArg1 As Long, ByVal dwArg2 As Long, _
ByVal dwArg3 As Long) As Long
Using these functions, we can call QueryInterface, AddRef, and Release on any interface. We can also call any other interface member that takes 0, 1, 2, or 3 32-bit arguments and returns a 32-bit value—this covers most, if not all, interface functions that I care about. So although this isn't a very elegant or generic solution, it fits the bill nicely for what I want to do today. Let's see how these functions are used to get the IDrawing interface to the light-bulb object and then used to draw the light bulb. Here's the entire function:
Private Sub AddLightBulb_Click()
' Try to create a light bulb object.
Dim hr As Long
Dim punkLightBulb As Long
hr = CoCreateInstance(CLSID_LightBulb, 0, CLSCTX_INPROC_SERVER, _
IID_IUnknown, punkLightBulb)
If hr < 0 Then
MsgBox ("Failed to create light bulb. Error " + Hex(hr) + "H")
Exit Sub
End If
' Try to get a pointer to its IDrawing interface.
Dim pIDrawing As Long
hr = comQueryInterface(punkLightBulb, IID_IDrawing, pIDrawing)
If hr < 0 Then
MsgBox ("Failed to get IDrawing interface. Error " + Hex(hr) + "H")
Exit Sub
End If
' Set the palette to the palette in the image.
hr = IDrawing_SetPalette(pIDrawing, Image1.Picture.hPal)
' Call IDrawing::Draw and see what we get.
hr = IDrawing_Draw(pIDrawing, Form1.hDC, 120, 120)
' Finished with IDrawing interface
hr = comRelease(pIDrawing)
' Finished with light bulb
hr = comRelease(punkLightBulb)
End Sub
The IDrawing_SetPalette and IDrawing_Draw functions use the comCallStdMember helpers:
Public Function IDrawing_Draw(ByVal punkObject As Long, _
ByVal hDC As Long, ByVal x As Long, ByVal y As Long) As Long
IDrawing_Draw = comCallStdMember3(punkObject, 3, hDC, x, y)
End Function
Public Function IDrawing_SetPalette(ByVal punkObject As Long, ByVal hPal) As Long
IDrawing_SetPalette = comCallStdMember1(punkObject, 4, hPal)
End Function
You can see that for any COM interface, we can create some Visual Basic functions that call the COM object's members via these helper routines.
The support DLL was created using Visual C++ as an MFC-based AppWizard DLL. The source code is in the VBSYSDBG subdirectory. This DLL provides two things: glue for Visual Basic to call COM object interfaces, and a debug window that shows what's going on when its being used.
Two files contain 99 percent of the interesting material: COMFNS.CPP, which has the implementation of the glue code, and VBSYSDBG.DEF, which has the definitions of the exported functions:
; VBSysDbg.def : Declares the module parameters for the DLL.
LIBRARY VBSYSDBG
DESCRIPTION 'VBSYSDBG Windows Dynamic Link Library'
EXPORTS
; Explicit exports can go here
CoCreateInstance = vbCoCreateInstance
comQueryInterface
comAddRef
comRelease
comCallStdMember0
comCallStdMember1
comCallStdMember2
comCallStdMember3
As you can see, each of the glue functions is named here. CoCreateInstance is actually implemented in a function called vbCoCreateInstance so as to avoid a name conflict with the "real" function in my C code.
The only reason for implementing CoCreateInstance at all here is so I can watch the calls in the debug window. Here's how:
STDAPI vbCoCreateInstance(REFCLSID rclsid, LPUNKNOWN pUnkOuter,
DWORD dwClsContext, REFIID riid, LPVOID FAR* ppv)
{
// Say what we're about to call.
dprintf2("CoCreateInstance()");
// Maybe print the arguments.
dprintf3(" CLSID %8.8lX-%4.4X-%4.4X-%2.2X%2.2X%2.2X%2.2X%2.2X%2.2X%2.2X%2.2X",
rclsid.Data1,
rclsid.Data2,
rclsid.Data3,
rclsid.Data4[0], rclsid.Data4[1], rclsid.Data4[2], rclsid.Data4[3],
rclsid.Data4[4], rclsid.Data4[5], rclsid.Data4[6], rclsid.Data4[7]);
dprintf3(" pUnkOuter %8.8lXH", pUnkOuter);
dprintf3(" dwClsContext %8.8lXH", dwClsContext);
dprintf3(" IID %8.8lX-%4.4X-%4.4X-%2.2X%2.2X%2.2X%2.2X%2.2X%2.2X%2.2X%2.2X",
riid.Data1,
riid.Data2,
riid.Data3,
riid.Data4[0], riid.Data4[1], riid.Data4[2], riid.Data4[3],
riid.Data4[4], riid.Data4[5], riid.Data4[6], riid.Data4[7]);
dprintf3(" ppv %8.8lXH", ppv);
// Call the function.
HRESULT hr = CoCreateInstance(rclsid, pUnkOuter, dwClsContext, riid, ppv);
// If it failed, print a message.
if (FAILED(hr)) {
dprintf1("CoCreateInstance failed: %8.8lXH", hr);
}
// Return the result.
return hr;
}
Boy, that sure looks like a lot of code—until you notice that almost all of it deals with printing the parameters out into the debug window (via the dprintf macros). If you cut out the debug code, it is simply a pass-through to the real CoCreateInstance function in the COMPOBJ DLL.
Let's take a look at one of the functions that calls an interface member:
STDAPI comCallStdMember3(IUnknown* pInterface, DWORD dwIndex, DWORD dwArg1,
DWORD dwArg2, DWORD dwArg3)
{
dprintf2("comCallStdMember3");
if (!pInterface) {
dprintf1("Invalid args");
return E_INVALIDARG;
}
dprintf3(" pInterface %8.8lXH", pInterface);
dprintf3(" dwIndex %8.8lXH", dwIndex);
dprintf3(" dwArg1 %8.8lXH", dwArg1);
dprintf3(" dwArg2 %8.8lXH", dwArg2);
dprintf3(" dwArg3 %8.8lXH", dwArg3);
POBJIFACE pObjIface = (POBJIFACE)pInterface;
PVTABLE pVtable = *pObjIface;
METHOD3* pMethod = (METHOD3*)pVtable[dwIndex];
HRESULT hr = pMethod(pInterface, dwArg1, dwArg2, dwArg3);
if (FAILED(hr)) {
dprintf1("comCallStdMember3 failed: %8.8lXH", hr);
}
return hr;
}
Again, there's a lot of code here to send stuff to the debug window, but mostly what it does is take the interface pointer and the offset of the interface member function in the COM object's vtable and call the function through the pointer in the correct table entry location.
You can use Visual Basic and some glue code to create and manipulate COM objects, but it needs some C support code and isn't very robust. If you get the parameters to a call mixed up, all sorts of problems will come your way. You need to create helper functions for each member of each interface you use in order to simplify the Visual Basic code (rather than calling comCallStdMember... directly). On top of this, it's possible that for some interface member functions, you will have to create more specific C glue code.
So, although you can create and use COM objects directly from Visual Basic, it's certainly not trivial to do so. In a later article, I'll be looking at adding OLE Automation support to COM objects, which will greatly simplify their use from Visual Basic.