Channels ▼
RSS

.NET

The Windows CE 2.0 Remote API

Source Code Accompanies This Article. Download It Now.


Sep98: The Windows CE 2.0 Remote API

Andrew works on development tools for Windows CE at BSQUARE Corporation. He can be reached at [email protected]


Communication between a desktop PC and an external device is one of the fundamental features all Windows CE users expect. To this end, CE provides a wide range of options including dial-up ability with Remote Access Services (RAS), WinSock, and the Remote API (RAPI). Unlike the first two, RAPI is unique to CE and provides a high-level interface to query the device for information (such as the processor type and OS version), as well as manipulate the device's object store and file system.

With the introduction of CE 2.0, several functions were added to RAPI including CeRapiInitEx, CeGetTempPath, and CeRapiInvoke. The CeRapiInvoke function, in particular, is a confusing but useful method for providing a simplified version of a Remote Procedure Call (RPC) between a desktop machine and a device. In this article, I'll examine CeRapiInvoke and share tips on how to avoid common usage pitfalls.

Using CeRapiInvoke

Assume you are writing a Windows NT application that needs to display the list of all the DLLs loaded on your CE device. How would you go about doing this? One way would be to launch a program on the device that writes to a file or the registry, then read that data via RAPI. This approach is problematic because it is difficult to determine when all of the data has been written. Another possibility is to have a program on the device create a socket connection to pipe the data through. This works -- but for such a small transaction, it requires a lot of overhead. CeRapiInvoke solves this dilemma by providing a way to call a function in a DLL on the device and have it pass back a block of data or a pointer to an IRAPIStream COM object that allows bidirectional communication.

The prototype for CeRapiInvoke looks like Example 1. lpwszDll is the name of a DLL in the Windows directory on the device, and lpwszFunc the name of a function exported from that DLL that you want to call. The dwInputCount and pcbIn parameters specify the data you want to pass to the remote function. These are optional, but if one contains valid data, they both must contain valid data. The pcbOutput and ppbOutput output parameters allow the remote function to return a buffer to the caller. ppStream provides the COM interface for sending and receiving data and determines the mode that this call to CeRapiInvoke is using. The last parameter is reserved for future use and should be set to zero.

CeRapiInvoke can be used in either block or stream mode. In block mode, the ppStream parameter must be NULL, the remote function is called synchronously, and data is passed back through the pcbOutput and ppbOutput parameters. By "synchronously" I mean that the call to CeRapiInvoke will not return until the remote function has completed. When ppStream is not NULL, the remote function is called asynchronously, CeRapiInvoke returns without waiting for lpwszFunc to complete, and ppStream points to an initialized IRAPIStream object that is connected to the corresponding stream on the device. Data can now be sent back and forth until the stream is closed on both ends. Even though pcbOutput and ppbOutput are used only for block mode, they must both not be NULL or CeRapiInvoke will return E_FAIL with a last error value of ERROR_INVALID_PARAMETER.

Though the name of the remote function can be that of any export, RAPI requires the prototype be like Example 2, where the first two parameters are copies of the input parameters from CeRapiInvoke. The next two parameters, pcbOutput and ppbOutput, are filled in by RemoteFunc and passed back to the caller of CeRapiInvoke in block mode. RAPI requires that the memory for ppbOutput be allocated with LocalAlloc to ensure that it is freed correctly. The last parameter is a pointer to an IRAPIStream that RAPI will have allocated if the ppStream parameter in the call to CeRapiInvoke is not NULL.

That last point about IRAPIStream is an important one -- RAPI allocates and frees the object for you. In other words, to call CeRapiInvoke in stream mode, the ppStream parameter should be the address of a pointer to an IRAPIStream object that is initialized to NULL (Listing Four, available electronically; see "Resource Center," page 3). RAPI does not allow use of an object that you allocate; attempting to do so will result in a memory leak. When both sides are finished with the stream, they just need to call Release to decrement the reference count and allow the object to be freed.

IRAPIStream is derived from the standard COM IStream interface and provides the two additional functions shown in Example 3. Currently only one flag is supported -- STREAM_TIMEOUT_READ. The online documentation for STREAM_TIMEOUT_READ states that it allows setting timeouts for the IStream::Read method, but never indicates what type of units are expected (seconds, milliseconds, whatever). When GetRapiStat is called with a nonNULL pdwValue parameter, it returns the Win32 code ERROR_INVALID_PARAMETER (which is not a valid HRESULT) and calling it with a NULL parameter results in an access violation exception. Conversely, calling SetRapiStat with any nonzero value in dwValue did not cause Read to timeout in any way that I could tell. Additionally, the declaration of IRAPIStream is not included in the Windows CE 2.0 headers and must be retrieved from the NT RAPI.H include file. For these reasons, I suggest you stick with just the IStream level features and don't use any IRAPIStream specifics.

Similar to GetRapiStat with a non-NULL second parameter, the fact that the return value from CeRapiInvoke is an HRESULT is deceiving. In reality, it can be an HRESULT, the return code from lpwszFunc, or a Win32 error code indicating that the DLL did not exist on the device, the function requested was not exported from the DLL, or the remote function caused an exception. The effect of this is that you must do something similar to Listing One where I explicitly check for values that are not HRESULT before using the SUCCEEDED macro. There is still a problem, however -- in block mode, the return value from CeRapiInvoke will be the return value from lpwszFunc if no errors occurred. This means that if lpwszFunc innocuously returns the same value as one of the Win32 error codes, there is no way to discern whether an error occurred. I recommend you only return S_OK or S_FALSE from the remote function to avoid this situation. This problem does not occur in stream mode because, due to the asynchronous nature of the call, there is no way to retrieve the return value from the remote function.

A Module Name Dialog Example

Now that I've covered how to use CeRapiInvoke, let's look at some code that solves the original problem presented (getting a list of all DLLs loaded on the device) and provides a simple dialog-based chat example. The first part will use CeRapiInvoke in block mode while the second will utilize stream mode. The source example code as well as the Visual C++ for Windows CE project that was used to build them is available electronically; see "Resource Center," page 3. The examples are organized into two NT executables, CRIBlock, and CRIStream, and one CE DLL, CRIDLL, that provides the remote functions.

Listing Two is CRIBlock. The first thing WinMain does is initialize RAPI using the new CeRapiInitEx function. CeRapiInitEx, unlike CeRapiInit, lets you time out after a specified interval and cancel the operation. If the initialization does not succeed after 15 seconds, you cancel the request and display a dialog box stating that you failed to connect. Once RAPI is initialized we then call the GetLoadedModules routine in CRIDLL using CeRapiInvoke and test for success using Listing One. If the call succeeds, you then use the Win32 DialogBoxParam API to display the module list passed back through the output parameters of CeRapiInvoke; see Figure 1. When users close the dialog box, DialogBoxParam will return and you free the module list using CeRapiFreeBuffer.

The dialog box on the host side is pretty boring. In the WM_INITDIALOG handler, you parse the newline-delimited module list using the C run-time routine _tcstok, then add the strings to a listbox. The WM_COMMAND handler takes care of closing the dialog when the user clicks the OK button or Cancel button. All of the real action takes place in CRIDLL routine GetLoadedModules in Listing Three (also available electronically).

GetLoadedModules uses the Windows CE TOOLHELP routines to retrieve the list of currently loaded DLLs. The call to CreateToolhelp32Snapshot uses the TH32CS_ SNAPMODULE and TH32CS_ GETALLMODS flags to accomplish this. If you've used TOOLHELP before on Windows 95 you'll notice that the TH32CS_GETALLMODS flag is not provided there and is specific to Windows CE. If you didn't include it, you would retrieve the names of the modules loaded for the current process instead of the entire operating system. The code then builds up the module list using the Module32First and Module32Next functions, allocating memory along the way with LocalAlloc and LocalReAlloc as required by RAPI. When Module32Next returns FALSE, you drop out of the loop, close the TOOLHELP handle and set the ppOutput and pcbOutput parameters.

Using LocalAlloc and LocalReAlloc in this manner exposed a quirk in the Windows CE implementation. In the past, when I used them in a loop like this on NT, I used LPTR as the flag to LocalReAlloc, which allowed me to allocate up to about 512 KB. On Windows CE, this same scenario never allocates more than one KB. This size limitation it too small even for our process list. Changing the LocalReAlloc flag to LMEM_MOVEABLE works on both NT and CE and allows memory up to the size of the biggest free block to be allocated since the location can be moved after the call to LocalAlloc rather than having to be expanded in place.

The CRIStream Example

Listing Four (also available electronically) presents my second example, CRIStream. WinMain calls CeRapiInitEx just like CRIBlock and then displays the dialog in Figure 2. When the dialog is first displayed, the edit box and Send button are disabled. After users click on the Connect button a dialog box is created on the CE device, the edit box and Send button are enabled, and the Connect button changes to Disconnect. Clicking on the Send button sends the text in the edit box to the device that is then displayed on the device dialog in a read-only edit box. The dialog box on the device does not accept any user input and is destroyed automatically when users click on the Disconnect button. The connection can be created and destroyed as many times as users like and clicking the close icon in the upper-right corner or hitting ESC on the keyboard will close the dialog box.

The WM_INITDIALOG handler in DlgProc takes care of initially disabling the edit box and Connect buttons, as well as limiting the edit box to the maximum number of characters that the user is allowed to input. The WM_COMMAND case handles all of the user input. IDCANCEL simply calls EndDialog after calling Disconnect to make sure that you don't close the dialog while still being connected to the device. The case for IDC_CONNECT checks to see if you are currently connected then calls the appropriate Connect or Disconnect routine.

The Connect routine uses DoCRI to call CeRapiInvoke in stream mode, passing a pointer to the global g_pStream that is initialized to NULL. If the call to CeRapiInvoke succeeds the user interface is updated to the connected state. The Disconnect routine writes a "magic string" to the stream, telling the device that it is disconnecting (this will be explained more in the following paragraph). It then updates the user interface to the disconnected state. The final user input that WM_COMMAND handles is IDC_SEND, which gets the data from the edit box and sends it over g_pStream by first writing the number of bytes being sent, then the bytes themselves.

DlgStreamTest (available electronically) initializes the output variables to NULL and then displays a dialog with DialogBoxParam, passing the pointer to the IRAPIStream in the DlgProc's LPARAM. After limiting the edit box to the maximum number of characters, the WM_ INITDIALOG handler starts a separate thread to receive the text, passing the dialog's hWnd and the IRAPIStream pointer in a structure through the thread function's formal parameter. If you were successful in starting the thread, you call SetForegroundWindow with the dialog box's hWnd to make sure that it is not hidden behind some other window.

The thread function ReceiveText takes over next and, after validating its parameters, sits in an endless loop waiting for data from the IRAPIStream. If you are able to read both the size of the data and the data itself, you then check to see if you received the magic string indicating that the user wants to disconnect. If you did not receive the magic string, you just use SetWindowText to display the text in the edit box; otherwise you break out of the loop, post an IDC_FINISHED notification to the dialog box, and call Release on the stream pointer. When DlgProc receives the IDC_FINISHED notification, it waits for the thread to finish executing, and then kills itself with a call to EndDialog.

The shutdown sequence for this dialog, though it may seem like overkill, is very important. When I first wrote this example, I just had ReceiveThread call SendMessage, and DlgProc did not wait for the secondary thread to exit. This allowed the dialog to close and DlgStreamTest to return before ReceiveThread had completed. The result was that any subsequent connections after the first one would cause intermittent access violation exceptions and crash the CE RAPI component. Changing to the current method solved this problem. This serves as just another example of how important it is to be careful when doing multithreaded programming -- it's easy to go wrong in even as simple a situation as this.

One final note about writing dialog-based applications for CE. The taskbar only contains buttons for visible, top-level windows that have the WS_OVERLAPPED style. By default, dialog boxes have the WS_POPUP style, and thus, do not get taskbar buttons. If you want your application to have a command bar button, the solution is to change the dialog box to have the WS_OVERLAPPED style instead of WS_POPUP either with the resource editor or by hand editing the RC file.

Debugging CeRapiInvoke DLLs

Debugging an application that utilizes CeRapiInvoke presents a bit of a unique problem, since it involves a process on the desktop and a DLL on the device. My solution is to debug the desktop application with Visual C++ and the CE DLL with WinDbg. Placing a call to DebugBreak in the routine being called by CeRapiInvoke lets you step through the code and debug as you normally would. See the Windows CE Embedded Toolkit for Visual C++ or the Windows CE Platform SDK for WinDbg and how to set it up to debug CE applications.

Conclusion

CeRapiInvoke is a useful and versatile addition to the RAPI arsenal. The examples I've shown only just begin to scratch the surface of its possible applications.

DDJ

Listing One

#include <windows.h>/* This handles the overloaded return value from CeRapiInvoke. */


</p>
BOOL CRI_Success(HRESULT hr)
{
    BOOL bOK = TRUE;
    if ( hr == ERROR_FILE_NOT_FOUND         ||
         hr == ERROR_CALL_NOT_IMPLEMENTED   ||
         hr == ERROR_EXCEPTION_IN_SERVICE )
    {
        bOK = FALSE;
    }
    else
        bOK = (SUCCEEDED(hr));
    return bOK;
}

Back to Article

Listing Two

#include <windows.h>#include <tchar.h>
#include <rapi.h>


</p>
#include "..\common.h"


</p>
BOOL CALLBACK ModuleDlgProc(HWND hwndDlg, UINT uMsg, 
                            WPARAM wParam, LPARAM lParam); 
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                            LPSTR lpCmdLine, int nCmdShow)
{
    RAPIINIT ri = { sizeof(RAPIINIT) };
    if ( SUCCEEDED(CeRapiInitEx(&ri)) )
    {
        // wait for 15 seconds for the connection...
        if ( (WaitForSingleObject(ri.heRapiInit, 15000) == WAIT_OBJECT_0) && 
             SUCCEEDED(ri.hrRapiInit) )
        {
            HRESULT hr;
            DWORD   dwBuf;
            BYTE    *pbuf;
            hr = CeRapiInvoke(_T("cridll.dll"), _T("GetLoadedModules"), 
                                        0, NULL, &dwBuf, &pbuf, NULL, 0);
            if ( CRI_Success(hr) )
            {
                if ( pbuf == NULL )
                {
                    MessageBox(NULL, _T("Loaded Module List"), _T("No 
                               loaded modules found on device"), MB_OK);
                }
                else
                {
                    // display dialog and pass pointer to module list
                    DialogBoxParam(hInstance,
                                    MAKEINTRESOURCE(IDD_BLOCKDLG), NULL, 
                                    (DLGPROC)ModuleDlgProc, (LPARAM)pbuf);
                }
                // free the memory allocated by the call to CeRapiInvoke
                CeRapiFreeBuffer(pbuf);
            }
            else
            {
                MessageBox(NULL, L"CeRapiInvoke failed", 
                                 L"CeRapiInvoke failed", MB_OK);
            }
        }
        else
        {
            MessageBox(NULL, _T("CeRapiInitEx failed"), 
                             _T("CeRapiInitEx failed"), MB_OK);
        }
        // have to call uninit even if the connection did not succeed
        CeRapiUninit();
    }
    else
    {
        MessageBox(NULL, _T("CeRapiInitEx failed"), 
                         _T("CeRapiInitEx failed"), MB_OK);
    }
    return 0;
    if ( CeRapiInit() != E_FAIL )
    {
    }
    else
    {
        MessageBox(NULL, L"CeRapiInit failed", L"CeRapiInit failed", MB_OK);
    }
    return 0;
}
// window function for the dialog box
BOOL CALLBACK ModuleDlgProc(HWND hwndDlg, UINT uMsg, 
                            WPARAM wParam, LPARAM lParam)
{
    BOOL    bRet = FALSE;
    switch( uMsg )
    {
        case WM_INITDIALOG:
        {
            LPWSTR lpwszMods = (LPWSTR)lParam;
            // parse modules from passed list and display in listbox
            LPWSTR lpwsz = _tcstok(lpwszMods, _T("\n"));
            while (lpwsz)
            {
                SendMessage(GetDlgItem(hwndDlg, IDC_MODLIST), 
                            LB_ADDSTRING, 0, (LPARAM)lpwsz);
                lpwsz = _tcstok(NULL, _T("\n"));
            }
        }
        break;
        case WM_COMMAND:
        {
            int     nID = LOWORD(wParam);
            int     wNotify = HIWORD(wParam);
            // handle click on OK or Cancel
            if ( wNotify == BN_CLICKED )
            {
              switch( nID )
              {
                case IDCANCEL:
                case IDOK:
                    EndDialog(hwndDlg, nID);
                    bRet = TRUE;
                    break;
              }
            }
        }
        break;
    }
    return bRet;
}AUCTION

Back to Article


Copyright © 1998, Dr. Dobb's Journal

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.