Performing DDE from a Dynamic-Link Library

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.

Abstract

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 API Set

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 Program Manager DDE Interface

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.

How the Sample Application Works

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:

  1. Format the required command string from the parameters passed to the API.

  2. Call SendExecCmd to perform the remainder of the operation. SendExecCmd is an internal function in PMGRPAPI.C.

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:

  1. Initialize DDEML.

  2. Initiate a DDE conversation with the Program Manager.

  3. Construct a DDE data handle for the execute string.

  4. Send the execute request to the Program Manager.

  5. Terminate the conversation with the Program Manager.

  6. Close down DDEML.

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;
}

Why Not Initialize DDEML When the DLL Loads?

It seems reasonable that when the DLL was first loaded we would:

  1. Initialize DDEML.

  2. Establish a conversation with the Program Manager.

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:

  1. Terminate the conversation with the Program Manager.

  2. Shut down DDEML.

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.