Using Visual Basic for handheld applications is not standard fare, but it's not that hard either.
This article will show how to extend embedded systems written predominantly in C/C++ using VB (Visual Basic) modules. VB is popular with programmers as well as nonprogrammers. The core system can be robust, efficient, and well made, but open to end users who wish to try programming, but are not ready to try C/C++. As handheld computers such as the Pocket PC become more powerful, there is a real benefit to exploring more complex software architectures on these small but capable machines. A top of the line Compaq iPAQ has a 200 MHz processor with 64 MB memory. There are even 1 GB hard drives available. Only a few years ago, this described a high-end PC; now it fits in a pocket or purse. Consequently, it is appropriate to discuss mixed languages, interprocess communications, and complex architectures for such powerful computers, as well as explore what can be provided for users.
The benefit of supporting VB extension has already been demonstrated; it is one cornerstone of the Microsoft product strategy. Desktop VB uses the same code generator and compiler as C/C++ and can produce COM objects as well as use them. Desktop VB can implement COM interfaces and produce modules that work where VB was not even anticipated. The late model versions of desktop Win32 support DCOM (Distributed Component Object Model).
Desktop VB is far more powerful than its cousin embedded VB. Embedded VB has a PCODE run time and cannot produce COM objects or DLLs. Therefore, there is no simple way to call an embedded VB program from another environment or even a separately compiled VB program. Fortunately, embedded VB can use COM objects written in C or C++. If this were not the case, it would be unfair to even suggest that it was VB. This article uses a COM object that implements the ActiveX COM interfaces to provide an IPC (interprocess communications) link between an embedded VB program and some other program written in a different language (in this example, C).
VB programmers are very familiar with using ActiveX controls designed to help solve a particular problem. These controls have Properties, Methods, and fire Events, which are callbacks into the VB environment. The ActiveX control should hide system programming issues and worries from the VB programmer as much as possible. If designed carefully, VB programmers will appreciate it as a useful and seamless extension to their normal working environment.
There are other ways to extend a VB program than with an ActiveX control. These might appear easier to implement, but this is not so when the different programming skills and project concerns held by VB and C programmers are fully considered. With some effort and cleverness, most of the Windows API can be called from VB on the desktop as well as the Pocket PC. There are limitations especially in the way the VB run-time system interacts with window messages and the general state of its windows. Except for events in an ActiveX control, there is no way for some routine to execute a callback into the VB environment. These limitations are present on the Desktop version but are far more ticklish on the Pocket PC version. When a set of DLL calls becomes too numerous, or is designed without comprehension of the VB data types, it can become very disruptive to the normal programming flow on a typical VB project. An interface must be designed for its audience. I dont want to spend time explaining how to do something the wrong way. It is enough to say that the VB run-time system can provide challenging and even mysterious technical problems, while VB programmers are often not comfortable with a complex API and translations out of their own programming environment.
Four separate executable elements must be implemented to demonstrate extending a C/C++ program with a VB plugin:
- An ActiveX COM component inside a DLL implemented with ATL and C++. This component is referred to as PluginOCX.dll.
- A simple C Win32 Program that starts the VB program and then sends and receives IPC messages with the PluginOCX ActiveX control in the VB program. This is referred to as HostProg.exe.
- A simple VB program is provided that demonstrates using PluginOCX, the ActiveX object provided in this demonstration; this program is Project1.vb, the default name for VB programs.
- In order to support the Design Time use of the control in the embedded VB desktop integrated development environment, PluginOCX.dll must also run in part on the desktop, so the control must be ported so that it works on Windows NT, 2000, and XP. All Windows CE programs are Unicode. Compile the desktop version with Unicode to avoid possibly unpleasant porting problems.
The IPC mechanism used here is based on the SendMessage Win32 API call and the message, WM_COPYDATA. It was chosen for simplicity in copying blocks of memory from one process to another. Shared memory could have been used. It might have been fun to port Sun RPC as well something I have done on other projects. In a real world implementation, these might well be worthy options.
Custom messages for SendMessage could be defined. Windows provides two ranges of message values for user definition on two constants: WM_USER and WM_APP. Many programmers mistakenly use the WM_USER range for cross-process messages. This is expressly forbidden in the specification. WM_USER should be used only within an application, and WM_APP should be used for crossing application process boundaries. Messages devised this way have no marshalling capability and are only able to provide a pair of 32-bit values. WM_APP could be used, but the fish to be fried in this demonstration are longer than 32 bits. WM_APP is more efficient though and the correct solution for simpler messages.
WM_COPYDATA is a window message that passes the host window handle in WPARAM and a pointer to a 12-byte data structure defined as COPYDATASTRUCT. COPYDATASTRUCT has a double word message identifier, a pointer to memory, and a count of the bytes in the structure pointed to. This code fragment from Hostprog demonstrates sending data:
LRESULT lrResp; wchar_t szSomethingToSay[] = L"Hi There VB World!"; COPYDATASTRUCT CopyData; CopyData.dwData = COPYDATA_TEXT; CopyData.cbData = sizeof(szSomethingToSay); CopyData.lpData = szSomethingToSay; lrResp== SendMessage( g_Plugin.hwndControl, WM_COPYDATA, (WPARAM)hWnd, (LPARAM)&CopyData ) );All Programs in Windows CE are built using wide characters, or 16-bit Unicode code points. COM is also more comfortable with Unicode. Note the use of sizeof(szSomethingToSay) to provide the number of bytes. Use of strlen(szSomethingToSay) returns the number of characters, which is the wrong value. This is another area where people familiar with ANSI programming techniques could be unpleasantly surprised by the Unicode version of Win32 where the number of bytes and the number of characters is different. I consider it wise porting practice to make the distinction between buffer size and character count. This code fragment demonstrates receiving WM_COPYDATA, also based on the source for HostProg. The fragment appears in the switch on window messages for the main window.
case WM_COPYDATA: { PCOPYDATASTRUCT lpCopyDataStruct = (PCOPYDATASTRUCT)lParam; if (NULL!= lpCopyDataStruct) { if (COPYDATA_TEXT == lpCopyDataStruct->dwData) { ::MessageBox ( hWnd, (LPTSTR)lpCopyDataStruct->lpData, L"Host Message", MB_OK); } } } return 1;Here lpData (a void*) simply needs to be cast to the character type expected by MessageBox.
To use WM_COPYDATA or any other SendMessage, a handle to a window in the other process is needed. There are several ways to find the handle; all depend on looking for some window characteristic we know to uniquely identify the desired handle. Each method might be best in some applications and not in others. A window can be identified by its text, class name text, or a long value, which is stored and whose use is user defined.
Hostprog uses the text of the parent window, which in the case of VB is also the title that appears in the VB Form and defaults to the VB Name of the form object for the VB program. This is done because the class name of a window form is not easily controlled and not likely to remain the same in subsequent releases of the VB run-time system. PluginOCX uses the class name of the main window of HostProg.exe. Either method is accomplished with the Win32 API call, FindWindow, which takes two parameters, one specifying the class name string and the other the text string for a possible window sought. One of these can be NULL, meaning that any value is acceptable.
Once a handle to the main window for the other process task is found, the proper child window must be found:
for ( hwndControl = GetWindow (hwndPlugin, GW_CHILD); hwndControl != NULL; hwndControl = GetWindow (hwndControl, GW_HWNDNEXT)) { GetWindowText (hwndControl, szControlText, MAX_PLUGINNAME-2); if (0== wcscmp (szControlText, L"PluginOCX") ) break; }(Note the use of wcscmp, which is the wide character version of strcmp.)
At this point, a simple IPC mechanism has been defined, but there still is no reasonable way to exploit it with a VB program on the Pocket PC. This IPC mechanism must now be fitted to an ActiveX control designed for the purpose. The Visual C++ compiler generates most of the code implementing PluginOCX.
This ActiveX control is designed for simplicity, only enough to illustrate this article. A more typical control for VB application programmers might represent a business problem and not a technical solution. If used in a hospital, the control might have a collection of Patients consisting of Patient objects, each with attributes defining their name, doctor, treatment, and so on. Adding and removing patients or changing information about them would then interact with some C program through the IPC mechanism, but the VB programmer would be working in the business world and not the world of system programmers.
The business problem that PluginOCX is designed to solve illustrates a technique to C programmers. The code generating instructions are based on this simple design: it will behave similar to a Button control in VB. It will have properties that allow us to change its Caption or the text legend that appears in it, and it will have properties that allow us to direct how it behaves in its role in IPC processing. The control ultimately must support VB Events or callbacks into the VB environment.
The code provided with this article (available for download at <www.cuj.com/code>) consists of two archives. Phase1.zip is untouched by human hands, generated entirely by the IDE (including HostProg). Phase2.zip contains my work, with my comments that define what code I wrote and what springs from the robotic machinations of the embedded Visual C++ IDE. A good portion of code appearing in Phase2.zip was also generated by the IDE.
To create Phase 1 of PluginOCX, fitting with the plan defined above, I started embedded Visual C++ and ran the WINCE ATL COM App Wizard for a project named PluginOCX. I told it I didnt want MFC (Microsoft Foundation Classes). This step produces stub code for a DLL that can host one or more ATL COM objects. Next I selected Insert/New ATL Object from the menu. From the dialog box, I chose the Control Wizard and then a Full Control and clicked Next>.
There are several steps taken in the Visual C++ development environment to produce the Phase 1 code:
- On the Names tab, type PluginConnect in the short name. This will also be the name of the control a VB programmer sees. You will notice that all the other text boxes on this daunting form are filled in for you. Leave them alone.
- Select the Attributes tab. Pick Single Threading model, Dual Interface, and No aggregation (defaults). This simplifies the control. Be sure to check Support Connection Points since that is COM speak for Events.
- The Miscellaneous tab is most important and so named to deter the uninitiated because the choices made here really define the code that will be generated. Select Button from the Add Control based on combo box and check the box Acts like Button as well. Clear the check box that says Normalize DC.
- The Stock Properties tab defines properties that typify ActiveX controls and have defined offsets in the COM interface. I would only select the Caption property.
- Click OK.
Following the above steps should yield generated code quite similar to the Phase1.zip archive in the online code samples.
The control appears in a VB object known as a Form. The form is implemented with a Win32 window. The control I created is implemented with a window, and this is a child of the form window. However, the part of the control that will be seen is yet a third window, a child of the control window and a grandchild of the form. The control (unseen) window handles the receipt of all Win32 messages, in particular the IPC messages. Since the control window is never really seen, I am free to change its text (or other characteristics) so that HostProg can find its window handle. The FindWindow calls explained above find first the form window and then its child, the control window. The grandchild is only of concern inside the ActiveX control. All this design was implemented by clicking the correct buttons in the IDE and not through programming.
The ActiveX control needs two events to implement callbacks into the VB environment. The events exist in a connection point object used to execute back to the client, using the ActiveX architecture. ActiveX events also provide the VB development environment with information to provide the VB programmer with a list of available events and, if selected, a stub of the event call.
PluginOCXs interface structure is defined by PluginOCX.idl, using the Microsoft variant of the IDL (Interface Definition Language). There is little time to delve into IDL here. Looking at the generated IDL is instructive and a good way to get working knowledge. In this particular ActiveX control, the IDL is generated for us except in this one particular case.
The IDL initially contains an empty stub defining events. This is what was generated:
dispinterface _IPluginConnectEvents { properties: methods: };Now change the generated code above to this:
dispinterface _IPluginConnectEvents { properties: methods: [id(1)] HRESULT OnHostNotice(BSTR bstrMessage); [id(2)] HRESULT OnButtonClick(); };OnHostNotice is the event that will call the VB program with the text that originated from Hostprog. The other event is simply to be fired when the button is clicked. You still need an implementation of this interface. Happily, the VC++ IDE will create all but two lines of code by using the IDL added above. In the Class View of the IDE, right click CPluginEvents and select Implement Connection Points. Pick the two events shown above. The implementation will be generated for you. Take a look at the generated code in PluginOCXP.h. There are two C++ routines defined as methods in class CProxy_IPluginConnectEvents. They are Fire_OnHostNotice(BSTR bstrMessage) and Fire_OnButtonClick.
What remains is to fasten the arrival of the WM_COPYDATA message to calling these fire event routines in the C++ code that implements the control.
The IDE generates a message map, which directs Windows messages to the proper methods in the C++ object. Another bit of work is to add the message handler and command code handler lines to the generated code for the message map:
BEGIN_MSG_MAP(CPluginConnect) ... MESSAGE_HANDLER(WM_COPYDATA, OnHostMsg) COMMAND_CODE_HANDLER(BN_CLICKED, OnButtonClick) ... END_MSG_MAP()Next the code must be added to PluginConnect.cpp to implement OnButtonClick:
LRESULT CPluginConnect::OnButtonClick (WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { return Fire_OnButtonClick(); }All that is needed is a routine to link the BN_CLICKED window command message to the Fire_OnButtonClick call that was generated for us. Clicking our little button control will invoke a VB routine (as described in the IDL) called OnButtonClick.
Here is the implementation of OnHostMessage that responds to WM_COPYDATA and calls Fire_HostNotice:
LRESULT CPluginConnect::OnHostMsg( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { PCOPYDATASTRUCT lpCopyData = (PCOPYDATASTRUCT)lParam; if (NULL != lpCopyData) { switch (lpCopyData->dwData) { case COPYDATA_TEXT: m_strHostMessage = (LPCOLESTR)lpCopyData->lpData; break; // Other kinds of WM_COPYDATA messages // can be added here as other Cases. ... default: m_strHostMessage = L"Err: Bogus Data"; } } else { m_strHostMessage = L"Err: Bogus Message"; } // Fire an event into our host VB // Program, using generated code. return Fire_OnHostNotice (m_strHostMessage); }m_strHostMessage is implemented with the ATL class CComBSTR. CComBSTR implements a BSTR (basic string), which is needed to pass character data into the VB environment. BSTR management is very easy to mess up and is one of the more obscure aspects of COM programming. CComBSTR handles the details and has some very handy overloaded operators. The class properly allocates a BSTR and copies the wide character string into it. It also handles reallocation for subsequent string copies and frees it when destroyed. You need this assistance since WM_COPYDATA sent wide character, Unicode data that must now be converted to a BSTR. At the system level, a BSTR is a pointer to an array of shorts (wide characters) allocated from a special pool (using SysAllocString). The pointer is preceded by a long count of the number of characters provided. Memory leaks, occasional failures, and other unpleasant bugs in ActiveX controls often are traced to erroneous use of BSTR-related calls. CComBSTR removes a good part of these problems. Use the IDE to add the HostMessage property. The code to implement the property is very similar to the code used in HostProg to send a message, except in PluginOCX the property implementation sends the string value of the property to Hostprog. This implements the return bidirectional message capability. What about the VB program? Here it is:
Private Sub Form_OKClick() App.End End Sub Private Sub _ PluginConnect1_OnButtonClick() MsgBox ("Button Click") PluginConnect1.HostMessage = _ "Hi there C World!" End Sub Private Sub _ PluginConnect1_OnHostNotice( _ ByVal bstrMessage As String) MsgBox bstrMessage, vbOKOnly, _ "Host Notice" End SubWhen HostProg sends a WM_COPYDATA message, the event is fired, and PluginConnect1_OnHostNotice is called in the VB program. This VB program then displays the text in a message box. (Of course, the VB programmer can do something else with the message if desired.)
When someone clicks on the control, it actually could be some other stock button control, the control fires the event, and a variant on Hello World is put in the Host Message property. This becomes a WM_COPYDATA message inside the control and is received in HostProg (which uses the text to display a message box).
Connecting dissimilar environments is not just about moving the bits from one to the other. I wanted to show a way to do this that respects the culture of both C and VB programmers and enhances the programming experience for both groups rather than burdening either with the others absurdity. Although an ActiveX control is complex, I have tried to show that the tools reduce a great deal of the effort. The programming effort for all presented here took less than two days. The key is to get the job done and not write code when a tool can do the job. As Tim, the Tool Man, used to say on Home Improvement, Let the tool do the work. Now, where the heck is Pamela Anderson?
Bibliography
Brent Rector and Chris Sells. ATL Internals (Addison Wesley, 1999).
Douglas Boling. Programming Microsoft Windows CE, Second Edition (Microsoft Press, 2001). Comes with the programming environment to work the companion code in this article.
Geoffrey Feldman is currently an independent consultant who specializes in connecting different platforms and environments. He worked for several years at Bell Atlantic as a system architect and prior to that nine years at Digital Equipment Corp as a product developer in the AI Technology group and High Performance Systems group. He began his career 25 years ago as a logic and microcode designer. Geoffrey can be reached at geoff@seabasecns.com.