Richard Hale Shaw
In previous articles, I've discussed how Dynamic Data Exchange (DDE) can be used to transfer data among Presentation Manager (hereafter "PM") applications. DDE is basically a protocol that uses PM message queues to regulate the passing of data. But since DDE can be used only by programs that open PM message queues, OS/2 kernel applications cannot access DDE directly. If a kernel application does open a PM message queue, all I/O has to come through Presentation Manager, which means that the application will not be able to access the OS/2 subsystems (Vio, Kbd, Mou) from its screen group.
When I wanted to provide Clipboard services to OS/2 kernel applications, I wrote a PM program, PMServer (see "Accessing Presentation Manager Facilities from Within OS/2 Kernel Applications," MSJ, Vol. 5, No. 1). This month, I'm expanding PMServer to provide DDE services to kernel applications. As I discussed in the first PMServer article, the natural vehicle for this expansion is a Presentation Manager object window-specifically, an object window for each kernel client that PMServer is servicing. Once I've added object window facilities to PMServer, I'll expand the sample program, PMAccess, that was originally presented with PMServer (see Figure 1).
CLIENT.C
PMServer's DDE processing code is contained in CLIENT.C (see Figure 2). This module consists of three functions: CreateClient, the object window thread function; DdeClientProc, the window procedure function for the object window; and MakeDDESeg, a support function that creates data packets suitable for transfer via DDE.
Since the purpose of CreateClient is to create a window and message queue (even though the queue is in a separate thread of execution), it's not unlike the main function of PMServer. It takes one parameter, a pointer to a CLIENT data type:
typedef struct _client
{
USHORT isddeclient;
HQUEUE qhandle;
PID kclient_pid;
HWND hwnd;
HWND hwndpmserver;
PVOID threadstack;
CHAR appname[MAX_APPLICATION_LEN];
CHAR topicname[MAX_TOPIC_LEN];
} CLIENT;
typedef CLIENT *PCLIENT;
This definition is in PMSVMSGS.H (see Figure 2). The header file contains all the message definitions and structures common to PMSERVER.C and CLIENT.C.
The CLIENT data type is an expanded version of that used in the earlier form of PMServer. Originally, it contained only a queue handle and a process ID for each kernel client that registered itself with PMServer. Now it has been expanded to include several additional items: a Boolean switch that indicates whether a client is engaged in a DDE conversation, the handle of the object window managing the conversation, PMServer's window handle, a pointer to the object window's stack, and the application and topic names that identify a DDE conversation.
PMServer initializes all these items except for the object window handle, which won't be initialized until the object window has been created by CreateClient. CreateClient inserts the handle into the CLIENT structure when it creates the object window. As a result, the CLIENT structure provides information that can be used by any thread (the main thread, the Queue Manager thread, or the object window thread) or by either window (the main window or the object window). CreateClient begins by saving the CLIENT pointer passed to it from _beginthread. Then it calls WinInitialize, opens the new message queue, and registers the new DdeClient window class.
The CLIENT pointer, anchor block handle, and window handle are all stored in a CDATA structure:
typedef struct clientdata
{
HAB hab;
HWND hwndServer;
PCLIENT pclient;
} CDATA;
typedef CDATA *PCDATA;
Using WinCreateWindow, the address of this structure is passed to the newly created object window as the first message parameter accompanying WM_CREATE. This makes all three components of CDATA and the elements of the CLIENT structure available to the new window. Note that this was accomplished in a single call; there is no need for additional messaging and interprocess communication.
What happens when DdeClientProc, the object window's window procedure, receives the WM_CREATE message? First, the CDATA parameter is saved from mp1, and a local CLIENT pointer is initialized from the one stored in the structure. Then WinSetWindowPtr is called to save the CDATA pointer in the reserved area of a window, allowing subsequent calls to WinQueryWindowPtr (at the beginning of the function) to reinitialize the pointer upon each call to the window procedure.
Next, the pointer's hwndServer member is initialized to NULL. The value of this handle will be used to determine whether a PM application has joined the proposed DDE conversation as a server. Finally, the function calls WinDdeInitiate to propose the conversation (by broadcasting WM_DDE_INITIATE to every PM application).
Note that the call to WinDdeInitiate is followed by a call to WinStartTimer. This function starts a timer that will generate WM_TIMER messages at specified intervals, in this case every tenth of a second. When the object window receives a WM_TIMER message, it looks to see if the hwndServer handle is NULL. If so, it assumes that no Presentation Manager program has posted a WM_DDE_INITIATEACK to respond positively to the WM_DDE_INITIATE and posts a PMS_DDE_INITNAK to the kernel client and a WM_QUIT to itself. (The consequences of this message are described below.)
If a WM_DDE_INITIATEACK is received, the handle of the responding server application (the DDE server) is saved in the hwndServer of the CDATA pointer and a PMS_DDE_INITACK message is sent (via MsgQSend) to the kernel client, signaling the start of the DDE conversation.
Managing the DDE Conversation
Once the object window is in place and the DDE conversation has been initiated, the kernel client can begin to make data requests and receive data. A kernel client can post a PMS_DDE_REQUEST or a PMS_DDE_ADVISE message to the Queue Manager to initiate a data request or receive data updates. These messages should be accompanied by the data item name. The Queue Manager will comply by sending a PMSV_REQUEST or PMSV_ADVISE message to the object window handle.
Upon receiving either of these messages, the object window will create a DDESTRUCT data block (via a call to MakeDDESeg) and post a WM_DDE_REQUEST or a WM_DDE_ADVISE via WinDdePostMsg. If no data is available, the DDE server will set the appropriate bits and reply with a WM_DDE_ACK. The object window will then post a PMS_DDE_NODATA message to the kernel client.
However, if data is received from the DDE server, it will arrive as a DDESTRUCT data block in a WM_DDE_DATA message. Upon receiving this message, the object window will allocate a piece of memory and reference it with a pointer to the ITEMREQ structure:
typedef struct itemreq
{
CHAR item[MAX_ITEM_LEN];
CHAR value[1];
} ITEMREQ;
typedef ITEMREQ *PITEMREQ;
The item component is then copied as a NULL-terminated string from the data block into the ITEMREQ structure and is followed by the data component. Since PMServer supports only the transfer of text (CF_TEXT) data via DDE, the data is assumed to be text and therefore a NULL-terminator is placed after it in the ITEMREQ structure. Because the ITEMREQ space is big enough for an ITEMREQ structure plus the data component, there is plenty of room for both in the new space. After the data is copied into the ITEMREQ structure, the new data package is passed to the kernel client as part of a PMS_DDE_DATA message. Then the object window frees the DDESTRUCT segment and the ITEMREQ pointer.
Note that the code for handling WM_DDE_DATA specifically checks to see if the application name is Microsoft Excel. The documentation for DDE specifies that the data size component of a DDESTRUCT (the cbData member) contains the size of the entire data block. However, Microsoft Excel sets the size of the cbData member as the size of the data component only, so an exception has to be made when interpreting a DDESTRUCT passed from this application.
Closing the Object Window
When the DDE server terminates the conversation, it sends the object window a WM_DDE_TERMINATE message. The object window will respond with a WM_DDE_TERMINATE of its own, then it posts a PMS_DDE_TERMINATE message to the kernel client and a WM_QUIT message to itself. The same thing occurs if PMServer needs to terminate the object window or if the object window receives a WM_CLOSE.
Upon receiving a WM_QUIT, the object window thread breaks out of the message processing loop in CreateClient. It calls WinDestroyWindow and WinDestroyMsgQueue to remove the window and the message queue. It then posts a PMSV_THREAD_TERMINATE to PMServer, calls WinTerminate to discard the PM resources used by the thread, and finally calls DosExit to terminate the thread.
Since receipt of PMSV_THREAD_TERMINATE from an object window thread will cause PMServer to deallocate the thread's stack, it's essential that it not do so until the thread executing CreateClient has terminated. Thus, a call to DosEnterCritSec has been inserted before the point where the PMSV_THREAD_TERMINATE message is posted. This temporarily freezes all the other threads in the process and allows CreateClient to complete execution. Once the CreateClient thread has terminated, the other threads are unfrozen as if DosEndCritSec had been called. PMServer can then safely process the termination message from the object window.
Note that the object window is capable of generating WM_DDE_EXECUTE and WM_DDE_POKE messages, although PMAccess does not take specific advantage of it. These messages are used for submitting commands and unsolicited data to the DDE server. The object window can also process two other commands from the kernel client: one command lets it receive notification of data changes without an update (a variation of WM_DDE_ADVISE), and another command ceases the notifications (WM_DDE_UNADVISE).
PMAccess
With the DDE object window code in place in PMServer, an OS/2 kernel application can use it to receive data from PM applications via DDE. The sample program that demonstrated PMServer's Clipboard-handling facilities, PMAccess, will now be expanded to display its new DDE capabilities (see Figure 3). The original PMAccess is a message-based Vio program that has five buttons at the bottom of its window. It enables the user to perform cut, copy, and paste operations on text.
You may recall that PMAccess uses OS/2 queues to let its threads communicate with each other. Architecturally this means that PMAccess is largely a reactive program not unlike a Presentation Manager application, rather than a proactive one as a DOS program usually is. For instance, the keyboard and mouse threads pass messages to a queue where they are received by PMAccess' main thread. This thread can then interpret the messages and perform the appropriate actions (such as moving the cursor, clearing the screen, or interacting with PMServer to copy or paste data from the clipboard). This method of message handling also allows PMAccess to react to screen buttons, so it can respond when a message from the mouse thread indicates that a button has been pressed.
Thus, when the user presses a screen button (by clicking it with the mouse or pressing the button's accelerator key), the main thread receives a message associated with the button. For example, if the user presses the Copy button, PMAccess will receive the MSG_COPY message and pass the selected screen text to PMServer as part of a PMS_CLPBRD_COPY message. PMServer will then put the text in the Clipboard. If PMAccess receives a PMS_CLPBRD_DATA message, PMServer is indicating that Clipboard data is available in response to a previously generated PMS_CLPBRD_QUERY-probably sent by PMAccess' Request thread, which periodically queries PMServer about the state of the Clipboard.
The most natural way, therefore, to expand PMAccess to take advantage of PMServer's DDE facilities is by adding support for the additional set of "PMS_" messages (now defined in PMSERVER.H, Figure 4). To initiate a DDE conversation, PMAccess will have to send a PMS_DDE_INIT to PMServer, accompanied by the application and topic names. Upon receiving a PMS_DDE_INITACK from PMServer (indicating that a DDE server is available), PMAccess can send PMS_DDE_REQUEST or PMS_DDE_ADVISE messages to request data or receive updates when the data changes. These messages must be accompanied by the data item name. PMServer will respond to these with either PMS_DDE_DATA (if data was received) or PMS_DDE_NODATA. Finally, PMAccess can send PMS_DDE_TERMINATE if it wishes to end the DDE conversation.
Along with internal support for these messages, PMAccess required additions to its user interface. Provisions had to be made to allow a user to initiate a DDE conversation, request data or updates, and terminate the conversation. Thus four new buttons (with associated messages) were added to the user interface: Initiate, Request, Advise, and Terminate (see Figure 5). When the user presses Initiate, for example, a MSG_INIT message is produced, and PMAccess executes the code to generate a PMS_DDE_INIT to PMServer.
Three entry buttons were added to allow a user to specify the Application, Topic, and Item Name parameters of a DDE conversation. When the user clicks one of the entry buttons with the mouse, the cursor is moved to that button so that he or she enters the appropriate DDE parameter from the keyboard. (To do this, a change had to be made in the handling of the MSG_B1DOWN message, which processes left mouse button presses. This handler now resets the cursor to the position of the mouse pointer when clicked in the text display/entry area above the buttons. This gives the user a means of getting out of the entry buttons.)
When a user presses the Initiate button, PMAccess' main thread will read the characters contained in the Application and Topic name button fields from their positions on the screen. This is facilitated by a new function, ButtonRead in BUTTON.C (see Figure 3), which uses VioReadCharStr to read the screen. For instance, to communicate with Microsoft Excel (after it has been started), the Application Name button should contain "excel." The Topic Name button should contain the name of a worksheet, such as the sample worksheet, "east.xls," that comes with Microsoft Excel (see Figure 6). The thread then sets up a packet for PMServer containing its own process ID and the application and topic name retrieved from the entry buttons. The packet is then sent as part of a PMS_DDE_INIT message via MsgQSend.
Once PMServer processes the PMS_DDE_INIT, starts the new object window, and the object window receives a WM_DDE_INITIATEACK from a PM application, the object window posts a PMS_DDE_INITACK back to PMAccess. When PMAccess receives this message, it will highlight the Initiate button, indicating that a DDE conversation has been established. The user can then fill in the item button with the name of the data item to be requested. Citing the earlier example, the user can request rows 15 and columns 23 by entering "r1c2:r5c3" in the Item button. The user should then click the Request or Advise buttons (see Figure 7).
When a user selects Request or Advise, PMAccess will again set up a message packet to be sent to PMServer. The message, either PMS_DDE_REQUEST or PMS_DDE_ADVISE, will be accompanied by PMAccess' process ID and the data item name (again, read from the Item Name button). Once PMAccess receives a PMS_DDE_DATA message in response, it will display the data received at the current cursor position in the upper part of the display. This is the same code used to process a PMS_CLPBRD_DATA message, except that the data item name is checked against the one most recently read from the Item entry button.
The user can terminate the DDE conversation by clicking the Terminate button, which causes PMAccess to send a PMS_DDE_TERMINATE message to PMServer. The latter will then terminate the DDE conversation with the DDE server and shut down the associated object window.
An interesting consequence of managing the new buttons was the decision to have PMAccess' main message-processing thread generate messages to itself, as is done in Presentation Manager programs. Instead of duplicating the code necessary to turn the Paste and Initiate buttons on and off if the user pressed the Clear button, PMAccess generates a new message to itself to reset the buttons accordingly. You can find this in the processing of the MSG_CLR and PMS_CLPBRD messages, which generate MSG_RESETPASTEBUTTON or MSG_RESETINITBUTTON (see PMACCESS.C in Figure 3).
Conclusion
Adding DDE and Clipboard facilities to PMAccess was an interesting experience, but not as interesting as writing PMServer. With its use of multiple threads, IPC, and access to PM facilities, PMServer is hopefully a good model for your own multithreaded PM programs, as well as a means of letting your Vio applications make use of PM's Clipboard and DDE facilities. u
Figure 1
Initiate
1. After filling in the Application, Topic, and Item Name fields, the user clicks the Initiate button on PMAccess' interface.
2. PMAccess' mouse thread reads button event and generates a MSG_INIT to PMAccess' main thread.
3. Upon receiving MSG_INIT, the main thread sends PMS_DDE_INIT, with Application and Topic names, through queue to Queue Manager thread of PMServer.
4. Queue Manager reacts to PMS_DDE_INIT, sets up CLIENT data structure, allocates stack space for an object window thread, and starts a new thread with a call to _beginthread.
5. New thread begins execution of CreateClient, opens new (Object) window, enters message loop with DdeClientProc as its window procedure.
6. Object window gets WM_CREATE from PM and calls WinDdeInitiate to broadcast WM_DDE_INITATE.
7. Microsoft Excel receives WM_DDE_INITIATE, sees that it can support DDE conversations on requested Topic (worksheet name), calls WinDdeRespond to post WM_DDE_INITIATEACK.
8. Object window receives WM_DDE_INITIATEACK from Microsoft Excel, and sends PMS_DDE_INITACK to PMAccess through the latter's queue.
9. PMAccess receives PMS_DDE_INITACK, highlights Initiate button for user, and beeps.
Request
10. User clicks Request button.
11. Mouse thread generates MSG_REQUEST to main thread.
12. Main thread sends PMS_DDE_REQUEST, with Item Name (worksheet range of cells), through queue to Queue Manager.
13. Queue Manager posts PMSV_REQUEST to Object window.
14. Object window posts WM_DDE_REQUEST to Microsoft Excel via WinPostDdeMsg.
15. Microsoft Excel, seeing that the requested range of cells is available, posts them back to Object window as part of WM_DDE_DATA message.
16. Object window sends data with PMS_DDE_DATA message to PMAccess through the latter's queue.
17. PMAccess, upon receiving PMS_DDE_DATA, writes the accompanying data to the display.
Terminate
18. User clicks Terminate button.
19. Mouse thread generates MSG_TERM to main thread.
20. PMAccess' main thread sends PMS_DDE_TERMINATE through queue to Queue Manager thread.
21. Queue Manager posts PMSV_TERMINATE to Object window.
22. Object window posts WM_DDE_TERMINATE to Microsoft Excel to terminate DDE conversation.
23. It follows with a PMS_DDE_TERMINATE to PMAccess...
24. ...and a WM_QUIT to itself.
25. Object window thread breaks out of message loop when WM_QUIT is received, then calls DosEnterCritSec, temporarily freezing the other threads. Then it posts PMSV_THREAD_TERMINATE to PMServer, and calls WinTerminate and DosExit to terminate itself, thereby unfreezing the other threads. PMServer's window procedure deallocates thread stack upon receiving PMSV_THREAD_TERMINATE.
26. PMAccess receives PMS_DDE_TERMINATE; the user sees it clear the fields as it readies itself for additional DDE instructions from the user.