|
Variant Type ID | VFP Type | Comments |
VT_EMPTY | empty | This means .F. in VFP |
VT_I4 | Long | All integer values in VFP |
VT_BSTR | String | Strings are in wide chars and need conversion for use as C strings. |
VT_BOOL | Logical | .T. or .F. |
VT_DATE | DateTime | DateTime |
VT_DISPATCH | Object Ref | Other COM object references that may be passed in or out as parameters. |
VT_VARIANT | VT_BYREF | Variant | Only applicable in C |
VT_ARRAY | VT_<type> | COM Array | Arrays. This works well only in VFP 6.0. Use COMARRAY() to force arrays to 0 or 1 based. |
Idispatch interface calls require that all arguments are passed as Variants - if the actual parameters or return values for the methods or properties are of other types the Idispatch::Invoke() COM function will do the type conversion for you. For Visual FoxPro this is a non-issue, because VFP COM objects can only receive and return variant values anyway. Other more type aware languages like Visual Basic can use explicit types like Long, Boolean, Currency, Date etc. For C programmers Idispatch has a major limitation: It doesn't support pointers, structures, classes or native arrays (although Variant Arrays are supported). However, Idispatch does support passing other Idispatch pointers, which does give the ability to reference other more complex structures through another Idispatch enabled COM object.
Calling all COM servers
So how do access our COM object from C/C++ code? There are a number of different ways to get at the server. I'll discuss the following three:Getting started with Visual C++
- Low Level Access
Accessing the server using the Win32 native COM API calls. This is a very tedious process and one prone to many errors. This code requires a ton of support code to make all the proper type and string conversions as well as providing for error checking for each COM call.
- Using COM Smart Pointers and the CCOMDispatchDriver Class
Visual C++ 6.0 improves on the smart pointer support that Visual C++ 5.0 first introduced by providing specialized COM Smart Pointers that handle specific COM interfaces. For Visual FoxPro servers the CcomDispatchDriver class is extremely useful as it provides a mechanism for late binding to a server at runtime. SmartPointers let you use late binding.
- Importing the Type Library and using a C++ Class wrapper
Visual C++ has the ability to import type libraries and create a C++ class wrappers that makes the appropriate COM calls on your behalf. The class is linked to the type library providing an early bound server. With #import you can access both methods and properties just as you can with any other C++ class pointer. Importing results in early binding.
Raw IDispatch accessI'll set up a new project called C_COMClient and then add three source files that demonstrate each of these three mechanisms for accessing the COM object above:
- Start up Visual C++ and Create a new project using the Project Wizard. Select Win32 Application and select Create an Simple Win32 application project. This creates a project and adds the standard pre-compilation header and a WinMain routine, but creates an otherwise empty project.
- Open up the StdAfx.h header file and comment out the following line:
#define WIN32_MEAN_AND_LEAN. This line removes the required COM include files. Since we need access to COM, take out this line.
- Modify the C_COMClient.cpp to look like this:
#include "stdafx.h"
/// Forward Declarations
BOOL ImportTlb(void);
BOOL RawDispatch(void);
BOOL DispatchDriver(void);int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { /// Required on each thread before COM can be used CoInitialize(0);// ImportTlb();DispatchDriver();// RawDispatch();/// Required to clean up COM resources CoUninitialize(); return 0; }- Then add three new CPP files called C_COMRaw.cpp, C_COMDispatch.cpp and C_COMImport.cpp which implement the ImportTlb, RawDispatch() and DistpatchDriver functions. For simplicity I didnt create header files for these but simply added the forward declarations at the top of the main source file.
- Before you can compile this emtpy project you need to implement an empty function prototype for each of the functions. Here's RawDispatch:
BOOL RawDispatch(void) {
return TRUE;
}- I'll look at the three implementations in the sections below. You can find the entire project on this month's Companion Resource CD.
If you are interested in learning about COM it's quite revealing to walk through the manual steps of low level COM access through IDispatch. It takes about a hundred lines of code to instantiate the server and actually make a method call if you count all the code necessary to handle errors and conversion from Ansi strings to wide character OLE strings. COM doesn't use ANSI character strings and instead requires all character parameters to be passed as Unicode (double byte) strings, which is rather tedious using native Win32 APIs. If you're using Idispatch, which is the most common way to access Visual FoxPro servers, you also need convert any parameters and return values to and from Variants.We don't have enough room to print the full code here, but here are the highlights of making method calls through the Idispatch interface using the raw COM APIs. Keep in mind that code below doesn't show the character conversions and error handling. You can check out the C_COMRaw.cpp file for the actual code and some helper functions in COMHelpers.cpp I built to make this easier.
Before you can use COM in an application you need to initialize the COM subsystem with:
CoInitialize(0);
This function must be called for every thread in application that needs to use COM. All mechanisms described here require that this line of code preceeds any use of COM in your application. You can CoUninitialize(); unloads the COM resources related to the matching CoInitialize().
To create an instance of a server you first need to get its ClassId. Since we most commonly use ProgIds (like visualfoxpro.application), you need to convert it:
hr = CLSIDFromProgID(lpWideCharProgId, pclsid);
Once you have the ClassId you can now create an instance of the object:
hr = CoCreateInstance(clsid, NULL, CLSCTX_SERVER, IID_IUnknown, (LPVOID *) &punkObj);This call to CoCreateInstance retrieves an interface pointer to the COM object's Iunknown interface. Iunknown is the base interface, which all other COM interfaces must support. Once you have the pointer to Iunknown you now have to ask for the actual interface that you would like to work with. In this case we want to work with Idispatch, which is a dynamic interface that lets us figure out which methods to call at runtime.
hr = punkObj->QueryInterface(IID_IDispatch, (void**)&pdispObj);
After the call to QueryInterface we now have a pointer to the Idispatch interface which allows use to access our COM object indirectly through the Idispatch method interface.
punkObj->Release(); // done with Iunknown InterfaceNotice the Release() call on the Iunknown interface pointer. COM works through reference counting and every time you ask for an interface pointer via CoCreateInstance or QueryInterface COM automatically calls AddRef() to update the reference count. Release() is the corresponding function that decreases the reference count and should be called whenever you're done with an Interface pointer. When the last interface pointer is released the reference count goes to 0 and the object actually unloads, but prior to that last Release the object will continue to be active.
All this to do the equivalent of Visual FoxPro's CreateObject()! With the Idispatch pointer in hand we can now get at our server and call a method. Unfortunately this also can be a fair amount of work. The first step is retrieve an ID to the method or property you're trying to call.
hr = pdispObj->GetIDsOfNames(IID_NULL, &pwzMethod, 1, LOCALE_USER_DEFAULT,
&dispidMethod);T
his call goes out and looks at the type library to retrieve the ID for the method or property. Note that you can cache this result value if you call the same method repeatedly so you don't have to keep calling GetIDsOfNames.Once you have the ID you then need to call Idispatch->Invoke() to call the server. The tricky part here is that all parameters to the server need to be packaged up into variants first and passed in a structure that contains all of the variants.
DISPID dispidMethod; DISPPARAMS dispparms; VARIANTARG varg[1]; VARIANT vResult; EXCEPINFO excep; VariantInit(&varg[0]); // Variant Array of Arguments VariantInit(&vResult); // Simple Variant // Setup parameters dispparms.rgvarg = varg; dispparms.rgdispidNamedArgs = NULL; dispparms.cArgs = 1; dispparms.cNamedArgs = 0; // Push in reverse order varg[0].vt = VT_I4 ; // Long see variant table for codes varg[0].lVal = dwParameter; hr = pdispObj->Invoke(dispidMethod, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &dispparms, &vResult, &excep, NULL); dwResult = vResult.lVal; // Peel out Long from variant struc
can get quite messy as each type has to be converted to its own variant and then dropped into the parameter structure. If you're using strings they have to be converted to the BSTR type first, then attached to a Variant. Invoke() is the work method that goes out and makes the call on your server which is the equivalent of a simple one line VFP method call:
lnValue = oServer.GetProcessId(1)
If the call succeeded vResult will contain the return value from your FoxPro COM object call and you can retrieve that value using the vResult structures. Note that string results come back as BSTR values that need to be converted back into C style strings. COMHelpers.cpp contains a pair of work functions for this: BSTRToString and StringToBSTR.
If you properly released your interface pointers for each time QueryInterface was called your object should now disappear. Keep in mind that in multithreaded situations your object may still have outstanding references from other threads and may force the object not to unload immediately. Also, remember that in C++ you have to handle memory management and reference cleanup yourself. If you don't call Release() because your code crashed somewhere that object will continue to stick around regardless of the fact that pointer went out of scope./// Converting a VARIANT BSTR result to C string char *lpzPath; char lcBuffer[512];/// Creates a new string buffer on the heap hr = BSTRToString(vResult.pbstrVal,0,&lpzPath);strcpy(lcBuffer,lpzPath); /// Must release the memory allocated for the string value HeapFree(GetProcessHeap(),0,lpzPath);/// Must release interface reference once we're done with it pdispObj->Release();In the following sections I'll show you easier ways to do the same job with much less code and headaches. So, why would you bother to mess with this low level stuff?
- Because this code is low level it's lean and mean. There's no additional overhead for intermediate mapping calls but you have to direct access to the COM APIs. If speed is your ultimate concern the native code will save some clock cycles.
- The Visual C++ COM classes are powerful, but they leave out some functionality. If you need to create servers via DCOM on remote machines or you need to marshall Interfaces across processes or apartments the manual way is the only way to do it. Keep in mind that you can mix and match the COM classes and manual code as the COM classes expose the COM interface pointer.
- It's good to understand how the low level APIs work before working with the COM smart pointers.
Using the CComDispatchDriver ATL Class
The raw access approach is a lot of work. It's easy to forget to handle some conversion correctly or forget some error handling code. To make life easier Visual C++ and the Active Template Library expose the CComDispatchDriver class. You still can't call methods directly because that's the nature of Idispatch, but single method calls can handle the actual Invoke access in a single method call. Furthermore, you can take advantage of ATL's support for Variant and BSTR classes that ease the conversions required by wrapping these COM data types into easy to use classes that use simpler assignment syntax. _variant_t and _bstr_t let you simply assign values and have them automatically converted to the proper variant and BSTR types. Here's what our code looks like now:
As with a lot of ATL, CComDispatchDriver is not complete in order to instantiate the object you need to use another class first to create an interface pointer. The returned pointer can then be passed to a CComDispatchDriver object to access the Dispatch interface.#include <windows.h> #include <comdef.h> #include <atlbase.h> CComModule _Module; // extern if declared elsewhere #include <atlcom.h>BOOL DispatchDriver(void) {CComDispatchDriver oServer; CComPtr<IDispatch> lpTDispatch;_variant_t vResult; char lpResult[512]; DWORD dwResult = 0; HRESULT hr = 0;char lpzError[256]; char lpzError1[128]; hr = lpTDispatch.CoCreateInstance(_bstr_t("C_Com.ComTest")); if ( FAILED(hr) ) goto Exit; // All errors need checking!/// Assign the interface ptr to the Dispatch Driver oServer = lpTDispatch;/// Call with no parameters hr = oServer.Invoke0(_bstr_t("GetCurrentPath"),&vResult); if ( FAILED(hr) ) goto Exit;strcpy(lpResult,_bstr_t(vResult)); // assign to C String/// Call with 1 parameter hr = oServer.Invoke1(_bstr_t("SetCurrentPath"), &_variant_t("c:\\temp\\"),&vResult);// Note: Numerics must be forced to Longs! hr = oServer.Invoke2(_bstr_t("AddNumbers"),&_variant_t(5L), &_variant_t(10L),&vResult);dwResult = vResult.lVal;hr = oServer.GetPropertyByName(_bstr_t("cCurrentPath"), &vResult);hr = oServer.PutPropertyByName(_bstr_t("cCurrentPath"), &_variant_t("c:\\"));/// Retrieve a property value hr = oServer.GetPropertyByName(_bstr_t("cCurrentPath"), &vResult);Exit: if (FAILED(hr)) { FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,0,hr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), lpzError1,sizeof(lpzError1),0);sprintf(lpzError,"An error occurred:%s",hr,lpzError1); MessageBox(0,lpzError,"Raw Dispatch Interface Error",48); return FALSE; } return TRUE; }Hence, you have to first create an instance with a plain COM smart pointer:
hr = lpTDispatch.CoCreateInstance(_bstr_t("C_Com.ComTest"));
and then pass the return pointer to the CcomDispatchDriver to activate the object:
oServer = lpTDispatch;
Note that both of lpTDispatch and oServer are classes that wrap the interface pointers. They're using overloaded operator = to allow them to be treated as if they were standard interface pointers so the assignment above works.
Once you have the interface pointer assigned to oServer it can now be used to call the Invoke and Get/Put property helper methods. Invoke comes in several flavors: Invoke0, Invoke1 and Invoke2 for a max of 2 parameters to be passed if you need more parameters you need to subclass and create additional methods on the CCOMDispatchDriver class. These methods package up the input parameters and unbundle the output parameters as needed, but everything must be done using variants.
I'm using the ATL _variant_t class, which allows you to assign most types directly to a variant structure. _variant_t can be used in place of a VARIANT or VARIANTARG pointer type argument. Most commonly you'll use the variant class to automatically format the input parameters. You don't need to use the class though: plain VARIANTs will do just fine, but it's a little more work.
All method names need to be passed in as COM BSTRs, so the _bstr_t class is used to quickly convert regular C strings to BSTRs. You can do this manually, or as we did before in the Raw interface calls by using the COMHelpers.cpp functions to convert the strings for you if you don't want to use the classes. The methods to look at are BSTRToString() and StringToBSTR().
One note about both the _variant_t and _bstr_t classes: Neither one of these supports binary strings that contain NULLs. BSTRs can contain NULL values, but neither class acknowledges the absolute size of the string. It's impossible to retrieve a string beyond a null. So if you need to return or send in binary data you may well be stuck creating your BSTR and Variants manually or using the helper functions in COMHelpers.cpp.
SummaryIf you can use early binding with your server the #import statement is the probably the easiest way to get your server hooked into Visual C++ code. In fact, it only takes a few lines of code and gives the ability to call methods directly on an object reference.
Note
Early binding using the type library import will work only with Visual FoxPro 6.0 servers. There's a problem with the type libraries that Visual FoxPro 5.0 creates, which causes the imports to generate incorrect class information and the compiler to generate lots of errors.The Visual C++ specific #import statement takes a full path to your server's type library as an argument. When you compile this code, VC++ creates a set of special header and implementation files from your type library. The headers wrap the interface and the CoClass with a straight C++ wrapper so calling any methods and properties is done using syntax very similar to a C++ class. VC++ creates .TLH and .TLI for the header and implementation respectively. If you're interested at all in low level COM calls required, the TLH and TLI files is an excellent source for seeing how to natively call the COM interfaces directly using the smart pointer classes described in the previous section.
If you take a look at the generated files you'll find that VC++ basically makes underlying COM calls for you wrapping the error handling by throwing standard C++ exceptions with a special COM exception object. It also handles type conversions for you. With Visual FoxPro this irrelevant, as VFP can only take and return VARIANT arguments so the class only generates VARIANT wrappers that still need to be converted to and from standard types.
Here's what our server code looks like using the #import type library directive:
/// Creates wrapper class TLH/TLI files in /// Debug or Release directories #import "c:\wwapps\wc2\webtools\c_com.tlb"void ImportTlb() {// Class is created in this namespace (lowercase) using namespace c_com;_variant_t vResult; // Work Variant objectchar lcPath[MAX_PATH]; long lnValue = 5; bool llValue;// Pointer - I + lowercase class name + Ptr IcomtestPtr oServer;// NOTE: calling the object - not the pointer! oServer.CreateInstance("c_com.ComTest");// Getting a property - now we're using the pointer! vResult = oServer->GetCCURRENTPATH();/// Storing to a standard C zero terminated string strcpy(lcPath,_bstr_t(vResult));/// straight method call with static string parameter oServer->setcurrentpath(&_variant_t(OLESTR("c:\\wwapps\\wc2\\webtools")));/// straight method call with C string parameter char lcPath2[MAX_PATH]="c:\\wwapps\\wc2\\webtools"; oServer->setcurrentpath(&_variant_t(_bstr_t(lcPath2)));/// method call with result value vResult = oServer->getcurrentpath();strcpy(lcPath,_bstr_t(vResult));/// Call server with multiple numeric arguments /// Note: Variants support only longs no integers vResult = oServer->addnumbers(&_variant_t(1L),&_variant_t(lnValue));/// Convert variant to a long lnValue = vResult.lVal;vResult = oServer->istrue(&_variant_t(true));llValue = bool(vResult);// Unload the server - Optional. Will Auto Release when object releases oServer->Release(); }The type library conversion essentially creates a smart COM pointer for you. The pointer is created through a macro, which is a little confusing because the following reference to the pointer appears nowhere directly in the generated files:
IcomtestPtr oServer;
The name of the Smart Pointer is always the name of the interface in the type library plus Ptr. Visual FoxPro interfaces are always named the lowercase name of the class plus an uppercase I as the Interface prefix. The Smart Pointer is actually an object that encapsulates and wraps the COM interface for you so it is treated much like you would any other dynamically allocated class. To instantiate the actual object you call the object's CreateInstance method:
oServer.CreateInstance("c_com.ComTest");
Note that your referencing the Smart Pointer object (.) in this call, rather than a reference to your server's COM interface (which would be the indirection operator ->). This call instantiates your COM server and then returns a reference to the Interface, storing it in an internal pointer member. Through some slick operator overloading you can now access the COM interface using the -> indirection operator:
vResult = oServer->getcurrentpath();
oServer->setcurrentpath(&_variant_t(_bstr_t(lcPath2)));As you can see all method calls are made directly on the object reference indirection operator. The COM smart pointer overrides the -> operator to pass the reference to the Interface to the class methods which actually use low level COM API functions (plus error handling) to call the interface methods on your behalf. Input and output parameters are packaged up for you and then reassigned to the appropriate parameters and return values of the method call.
The code above demonstrates how to retrieve Properties and make method calls. As with the CComDispatch example you'll find that a lot of code deals with the conversion to and from variants and OLE string types. But as before the _variant_t and _bstr_t make this relatively easy even if it does mean some nested class wrappers.
Accessing your Visual FoxPro COM objects from C++ has enormous potential. It gives you the means to build scalable applications that can take advantage of system level technology and still allow Visual FoxPro to be part of the solution. COM makes this connection from C code to Visual FoxPro very straight forward in terms of logic.
In summary, use the following guidelines for building C code that accesses COM objects:
- If Early Binding is an option, by all means use the Type Library Import mechanism. It's by far the easiest way to access COM objects from VC++.
- Use CComDispatchDriver for accessing Late Bound servers via Idispatch. You should be able to use this class for most if not all of your Idispatch needs.
- If you need special handling of the Interface then you may have to use the low level APIs. The ComHelpers provided on the CD should help with some of the repetitive conversion tasks.
- You can mix and match. All smart pointer classes expose the underlying COM interface pointers, so you can get at the low level APIs when needed.
- Take advantage of the _variant_t and _bstr_t objects. They work well as long as you don't need to support binary strings.