By Herman Rodent, a much-traveled, small, furry animal
Microsoft Developer Network Technology Group
Created: October 1, 1992
Click to open or copy the files in the PMGRPAPI sample application for this technical article.
Dynamic data exchange (DDE) is normally associated with applications. The basic DDE mechanism uses WM_DDE... messages sent between application window procedures. This article shows how DDE operations can be performed from a dynamic-link library (DLL) using the dynamic data exchange management library (DDEML) to do most of the work. Using DDEML instead of raw DDE messages makes adding DDE functionality to a DLL a trivial exercise.
PMGRPAPI, the sample code for this article, implements a DLL with an application programming interface (API) to the Microsoft® Windows™ Program Manager for controlling the creation and deletion of groups and items. The application is of use in its own right as an aid to creating setup programs.
The following list shows the APIs implemented in the sample code DLL. These calls manipulate groups or group items in the Microsoft® Windows™ Program Manager.
Table 1. Functions Implemented in PMGRPAPI.DLL
API Name | Description |
pmCreateGroup | Creates a new group |
pmDeleteGroup | Deletes a group |
pmShowGroup | Shows a group |
pmAddItem | Adds an item to a group |
pmDeleteItem | Deletes an item from a group |
pmReplaceItem | Replaces an item in a group |
pmReload | Removes and reloads an existing group |
pmExit | Exits from the Program Manager |
The Windows Program Manager supports a set of commands that can be sent to it through the DDE execute protocol. The Microsoft Windows version 3.1 Software Development Kit (SDK) Programmer's Reference, Volume 1: Overview has details of the set of commands supported in Section 17.2, "Command-String Interface."
Each command consists of a leading square bracket, the name of the command, an opening round bracket, a comma-separated parameter list, a closing round bracket, and finally a closing square bracket. As an example, here's the command string required to add an item called Command.com with a caption of "DOS Box":
[AddItem("Command.com","DOS Box")]
Note that DDE execute strings are not case-sensitive. Multiple commands may be concatenated together into a single string:
[DeleteGroup(Group1)][DeleteGroup(Group2)]
Attempts to send incorrectly formatted commands will simply result in the failure of your request to execute. Currently, no established convention exists to get any error information back from an execute request. Look for an article on this subject in future releases of the Microsoft Developer Network CD.
Although this example is written specifically to interface with the Windows Program Manager, it also works with the Norton Desktop, which responds to the same set of DDE execute commands.
The PMGRPAPI sample dynamic-link library functions as a DDE client application. When an application calls one of the APIs in the PMGRPAPI DLL, the API code takes the following steps:
Here's the code for the pmDeleteGroup API:
BOOL FAR PASCAL pmDeleteGroup(LPSTR lpszGroup)
{
char buf[256];
if (lpszGroup && lstrlen(lpszGroup)) {
wsprintf(buf,
"[DeleteGroup(%s)]",
lpszGroup);
}
return SendExecCmd(buf);
}
The SendExecCmd function takes the formatted command string and performs these steps:
Here's the code for the SendExecCmd function taken from the PMGRPAPI sample:
static BOOL SendExecCmd(LPSTR lpszCmd)
{
DWORD dwDDEInst = 0;
UINT ui;
HSZ hszProgman;
HCONV hConv;
HDDEDATA hExecData;
//
// Initialize DDEML.
//
ui = DdeInitialize(&dwDDEInst,
DDECallback,
CBF_FAIL_ALLSVRXACTIONS,
0l);
if (ui != DMLERR_NO_ERROR) {
return FALSE;
}
//
// Initiate a conversation with the PROGMAN service on
// the PROGMAN topic.
//
hszProgman = DdeCreateStringHandle(dwDDEInst,
"PROGMAN",
CP_WINANSI);
hConv = DdeConnect(dwDDEInst,
hszProgman,
hszProgman,
NULL);
//
// Free the HSZ now.
//
DdeFreeStringHandle(dwDDEInst, hszProgman);
if (!hConv) {
return FALSE;
}
//
// Create a data handle for the execute string.
//
hExecData = DdeCreateDataHandle(dwDDEInst,
lpszCmd,
lstrlen(lpszCmd)+1,
0,
NULL,
0,
0);
//
// Send the execute request.
//
DdeClientTransaction((void FAR *)hExecData,
(DWORD)-1,
hConv,
NULL,
0,
XTYP_EXECUTE,
1000, // ms timeout
NULL);
//
// Done with the conversation now.
//
DdeDisconnect(hConv);
//
// Done with DDEML.
//
DdeUninitialize(dwDDEInst);
return TRUE;
}
It seems reasonable that when the DLL was first loaded we would:
This would make the SendExecCmd function simpler and presumably faster. When the WEP function for the DLL was called just prior to the DLL being unloaded, we would:
Unfortunately, although the initialization steps are quite reasonable, the termination steps are not. Because of the way the Windows kernel works, all code in a WEP function needs to be in a fixed segment and so does any code it calls. If this is not the case, it's possible the code would have to be loaded during the time WEP was being called, and this causes all sorts of problems. More information about the WEP procedure can be found by searching the Microsoft Developer Network CD for WEP.
The best way to control initialization and termination is to provide exported functions to do the initialization and termination procedures and have each client application of the DLL call them. For example, you might provide pmInitialize and pmTerminate functions. An application wanting to use PMGRPAPI.DLL would first call pmInitialize, then call whatever other APIs it needed to call, and finally it would call the pmTerminate function to shut things down. This puts the responsibility of doing it right on the application, which is by no means an ideal situation. It is, however, the only reliable practical solution to the problem of providing a termination procedure in a DLL.