A C++ Telephony Interface
TAPI standardizes the business of making a phone call. This interface hides the messy details.
Microsoft's TAPI (Telephony Applications Interface) makes it possible to create distributed applications across a telephone network. However, TAPI is a C API and can be quite complex to understand and use. This article features a pair of C++ wrapper classes, CLocalConnect and CRemoteConnect, that make it easy to add dial-up capability to new or existing MFC Win32 applications.
In this article, I define a simple distributed model that has two modes of operation, remote and local. Remote mode uses the telephone network, in which a server application can accept calls from one or more client applications. Local mode does not use the phone network, but may use a direct serial connection such as a null modem cable to another node in the distributed system.
An Overview of TAPI
TAPI provides a Windows desktop application with the ability to communicate voice, fax, and data media over the public telephone network. Each TAPI application begins by opening a logical line device to an underlying physical device (i.e., a modem). The logical line device provides an abstraction layer. Multiple logical devices can map to the same physical device, and TAPI determines which application gets access to the physical line. (This discussion refers to the standard Windows 95 telephony driver UniModem. UniModem supports only the typical residential analog phone line, not the more sophisticated digital phone lines. The analog phone lines are known as POTS, or "Plain Old Telephone Service" lines.)
A logical line is defined here as a single POTS line supporting data media, with one phone number per line and one call per phone number. Once a logical line is opened, the application supplies a phone number (unless accepting a call) and then is ready to make a phone call. When the application opens the logical line, TAPI provides information about the line, primarily whether it is suitable for the type of media to be transferred (which is data in this case).
Each call made by the application transitions through a number of TAPI states, starting from Idle at call initiation to Disconnect at call completion. Intermediate states are Dialtone, Ringing, Call Offered, and Connect. The Connect and Disconnect states are the most important to the application because they represent the conditions for which application data can be transferred and terminated, respectively.
The sequence of transitions through TAPI states is shown as follows:
Client outbound call:
Idle -> dialtone -> dialing -> proceeding -> ringback -> Connected -> Disconnected -> Idle
Server inbound call:
Idle -> Call Offered -> Call Accepted -> Connected -> Disconnected -> Idle
TAPI uses many C structures, all supplied by tapi.h. However, using these structures is a bit more complicated than using a normal C struct because many of them are dynamically sized. These structures contain two special fields, dwTotalSize and dwNeededSize. dwTotalSize represents the size of the currently allocated struct as measured by sizeof. dwNeededSize is filled in by various functions, such as TAPI's lineGetID, with the size actually needed. Figure 1 shows the variable-size struct VARSTRING (from tapi.h) used with function lineGetID to retrieve the modem handle and name . After lineGetID returns, the code checks for (needed > total). If true, the structure is reallocated to the needed size and the function is invoked again.
This dynamic sizing is due to environmental factors on the Windows platform that may render the basic header structures insufficient.
Software facilities such as TAPI are continually being updated, which could break an existing application and cause it to crash. Since TAPI is realized in a DLL, replacing the DLL with a newer version illustrates such a situation. To avoid breaking the code, TAPI requires the application to negotiate a version that is agreeable to both the application and the DLL. When the application is developed it records which TAPI version is in use. It later uses this version number (actually a range of versions) when invoking the function lineNegotiateAPIVersion. TAPI then determines if the specified version is supported by the DLL in use, and if so, a version number common to both is returned. The application then specifies this use-version number in subsequent TAPI function invocations for which versioning is required. If the versioning process fails, the application can fail gracefully and supply an error message stating the version incompatibility. To those familiar with versioning from Microsoft's COM, the concept is the same although the mechanism is quite different. Figure 2 illustrates the versioning mechanism used by TAPI.
Asynchronous Procedure Calls
TAPI needs APCs (Asynchronous Procedure Calls) because the application's event initiation (e.g., making a connection) does not happen synchronously following the mere return of a function call. The external environment affects the timing and success of an application request. An appropriate application response depends on a mechanism to inform it when such asynchronous events occur, which is the purpose of the APC mechanism.
TAPI v2.0 provides an event-driven APC method that uses a Win32 event object. However, this method is currently supported only under Windows NT, whereas TAPI v1.4's "hidden window" method is supported under both NT and 95 and is the method I used for this article. The hidden window method uses the normal Windows thread messaging scheme to signal asynchronous events. Windows GUI developers know that window message events are dispatched to the thread where the window was created and entered on the thread's message queue. The thread has a message loop which processes each message on the queue by activating the registered window procedure (also known as a callback function) for that window and supplying the message as a parameter.
Likewise, TAPI asynchronous events are dispatched to the thread that registered the callback. Note that, in Win32 terminology, this thread cannot be a non-GUI or worker thread; it must be a UI (user-interface) thread because a UI thread has a message queue. With this mechanism in place, asynchronous events such as Call Connected, Disconnected, Idle, and so forth can now be communicated to the application. For C++ considerations, the callback thread must be a static function because TAPI is based on the C model. Figure 3 demonstrates callback registering via function lineInitialize (Figure 3a); this pointer linkage via lineOpen (Figure 3b); and subsequent callback linkage via the dwInstance parameter (Figure 3c).
The Raw TAPI API
I have previously referred to a number of TAPI functions without mentioning their parameters. Here is a brief overview of those parameters:
lineInitialize(&hTAPI, hInstance, lpfnCallbackFuncton.szAppName, &numLines)
hTAPI returns the Windows API handle; hInstance is the application instance handle; lpfnCallbackFunction points to the application callback function; szAppName is an application-unique name; and numLines returns the number of logical line devices, also known as Telephony Service Providers (TSP). The last parameter reflects the number of TSPs installed under the modems applet in Control Panel.
lineNegotiateAPIVersion(hTAPI, deviceLine, lowVersion, highVersion, &versionToUse, &extensions)
hTAPI is the API handle; deviceLine is the logical line device to be tested for compatibility with the application's range of versions; lowVersion/highVersion specify the version range the application can work with; versionToUse is the returned negotiated version; and extensions returns feature extensions beyond standard TAPI for this TSP. The last parameter can normally be ignored, but if used must also be negotiated via lineNegotiateExtVersion (not discussed here).
lineOpen(hTAPI, deviceLine, &hLine, versionToUse, extensionVersion, instanceData, privileges, mediaModes, lpCallparams)
hTAPI is the API handle; deviceLine is the logical line to open; hLine returns the handle to the line device; versionToUse is the negotiated TAPI version; extensionVersion is the negotiated extension (set to 0 for UniModem); instanceData is 32-bit application-specific data that is passed to the callback function, and is where a this instance pointer is assigned (as seen later); privileges specifies how the owner wants to handle calls; and mediaModes specifies which types of calls to answer or monitor.
lineGetID(hLine, addressID, hCall, select, lpDeviceID, lpszDeviceClass)
hLine is a handle to the open logical line; addressID and hCall specify the address and call handles for the modem; select specifies which of the first three parameters lineGetID should use to retrieve the modem handle; lpDeviceID points to a VARSTRING structure to which Windows assigns the modem handle and name; and lpszDeviceClass specifies the device class, which for Win32 must be comm/datamodem.
The TAPI Class Design
Following is the rationale for applying object orientation to the telephony design.
- The complexity of the TAPI architecture makes it an excellent candidate for encapsulation.
- A need for inheritance arises from the notion of developing a local or null modem interface, and then deriving a remote modem interface from it. Class CRemoteConnect is accordingly derived from the base class CLocalConnect. Finally, the application may want to override some features of either interface (e.g., the I/O thread), so polymorphism is needed.
- Another design consideration is deciding how the class should be packaged. Since the class will require unique dialog resources independent of the client application, I decided to use a DLL for this purpose. I opted for Microsoft's Visual C++ MFC extension DLL method (v5.0) for implementation. I selected the extension DLL because I can export the entire class using the storage modifier AFX_EXT_CLASS. Moreover, shared linkage makes for much smaller executables and is excellent for development, since link time is much faster. Figure 4 and Figure 5 list portions of the null modem interface and derived modem class declarations, respectively.
The rationale for encapsulating the TAPI class design as a DLL should make it clear that the user of this class is alleviated from the TAPI details while also benefiting from not having to augment the client GUI with additional resource dialogs to support user interaction. Moreover, using inheritance and polymorphism extends the class flexibility by allowing the user to tailor the class behavior to the specific needs of the client application.
A detailed description of TAPI and the requisite source code is beyond the scope of this article. An excellent text from the Microsoft Programming Series is titled Communications Programming for Windows 95 and is my primary reference for this article . This book provides a C demo program that was the starting point for my class design. I also consulted a good text on multithreading: Multithreading Applications in Win32 . This book is an invaluable reference for Win32 thread programming, was quite useful for developing the callback and default I/O threads, and even provided some helpful hints on DLLs.
My initial challenge was to migrate the demo program to an encapsulated class-based design. The demo program runs from the primary (GUI) thread which also serves as the callback thread. I lifted the core TAPI architecture from the demo program and interfaced it to a dedicated callback UI thread while converting C functions to C++ member functions. However, this presented a memory management problem, since what was once a single thread became multithreaded so that memory allocated in the application thread at instantiation could be freed by the callback thread or vice versa. This situation created an unsafe scenario in multithreaded applications. I solved this problem by replacing calls to malloc/free with HeapFree/HeapAlloc as seen in Figure 1.
Another problem was the very likely situation in which multiple telephony drivers, or TSPs, are installed. I decided to use a listview control that would display all installed modem device strings and an associated TAPI icon for the user to select as the line device to open. I used this method to fill an MFC ClistCtrl object, remembering to close each logical line after the information was obtained.
My final challenge concerned shutting down the TAPI device. I noticed that periodically the shutdown process could take seconds, possibly tricking the user into thinking that the application had somehow stopped responding. If that happened and the user aborted the application during debug or normal operation, the device shutdown would not complete and subsequent attempts to make a new call would result in system errors, correctable only via system reboot. Researching this problem an artifact of using the Windows 95 tapi32.dll, which performs address thunks to the 16-bit version I concluded that the simplest and perhaps only way to avoid this was to send status messages to the user while shutdown was in progress. Therefore, I used a modeless dialog listbox, which displays status messages during shutdown so the user will know the application is actively shutting down. TAPI v2.0, supplied with Windows NT 4.0, is a true 32-bit implementation, so such system errors may now be avoidable.
Consider the simple distributed model: a server application that is waiting to accept an incoming call, and a client application that is about to make a call to the server. Both applications will instantiate the CremoteConnect class and request use of the default I/O thread.
As shown in Figure 6, the server thread instantiates CRemoteConnect to answer a call and requests use of the default I/O thread . The constructor invokes TAPI functions as necessary to prepare for an incoming call. If no TAPI error messages are reported, the server thread is ready to answer a call so it blocks using Win32's WaitForMultipleObjects until either a connection is made or an exit command has been received. On connection, the callback in my DLL spawns the default I/O thread and sets the Connect event. In turn, the server thread's WaitForMultipleObjects returns, indicating that a client connection has occurred. The server thread now enters a while loop which terminates on client disconnect or thread exit.
The server thread uses the message I/O methods from the base class CLocalConnect. I also added the synchronous read member function SyncRead to allow the thread to block until either a data request arrives or a timeout occurs. When the client call terminates, the DLL callback will ensure that member function isModemConnected returns false. The server thread detects the disconnection and automatically prepares to answer the next incoming call by calling member function ReAnswerModem, which simply reconstructs the object to answer the next call.
The client thread shown in Figure 7 instantiates CRemoteConnect to make a call and requests default I/O. In addition to bringing up a Properties dialog, the class constructor will prompt the user for phone number and area code. The Dialing Properties dialog also appears to optionally specify long distance and/or credit card number. This dialog can also be viewed from the Modems applet under Control Panel . If no errors are reported, the call is made and upon connection or termination, control returns to the application.
On connection, the DLL callback spawns the default I/O thread. The callback will set the connect event and a flag such that base class member function PortInitOk returns true. The client thread uses the message I/O methods from the base class CLocalConnect. Again, I use the synchronous read member SyncRead to allow the client thread to block until either a data response arrives or a timeout occurs. When data arrives, the client thread writes it to an output file.
Connection termination is first initiated by the client thread by allowing the CRemoteConnect object to go out of scope, which happens when a thread exit event has been received from the application menu. The destruction of the client object displays a shutdown progress dialog while the shutdown procedure for TAPI is carried out. At the server end, the disconnect event is reported by TAPI and, as already discussed, the server thread prepares to answer the next call.
Note that once a connection is made, the handle to the opened modem can be used as if it were a handle to a Win32 serial port, so all Win32 I/O methods are applicable, including the exotic I/O completion port method popular with Windows NT servers. Speaking of serial ports, the features of base class CLocalConnect can also be used if a null modem connection is used. In this case, the server and client threads instantiate CLocalConnect. If PortInitOk returns true, data transfers can proceed using the same I/O methods.
The abstraction layer provided by TAPI comes at a price in the complexity of the programming model. This alone makes it an excellent candidate for C++ class encapsulation. Moreover, since a modem handle obtained via TAPI inherits all the Win32 serial port functionality, it makes sense to first develop a null modem base class with a default, virtual thread I/O interface which the derived TAPI class can redefine if necessary. I developed my DLL for a single application and found it quite simple to reuse in other applications.
For readers familiar with Microsoft COM, my DLL could easily be extended to a COM interface simply by inheriting from IUnknown and defining GUIDs for the local and remote interfaces. In fact, TAPI v3.0, scheduled to be included in NT 5.0 (and currently included in NT 5.0 Beta 1) is advertising support for a COM version of TAPI. The advantage here is that explicit linkage to the DLL disappears in favor of run-time QueryInterface calls to the COM library with the specified GUID. Moreover, the TAPI and COM versioning methods could be combined such that newer TAPI version methods such as v2.0 would be realized in a unique COM interface with its own GUID. This would allow newer applications to query for the latest TAPI version without breaking older applications relying on older TAPI versions.
Notes and References
 Some of the figures in this article refer to the data member M_TapiStruct. This is an instance of a user-defined structure that bundles most of the TAPI structures into one for convenience. It also keeps track of which structures have been dynamically allocated.
 The Properties dialog, which appears when CRemoteConnect is constructed, can also be viewed from the Modems applet under Control Panel, noting that an additional Options tab is available only from TAPI. The Options panel provides the ability to bring up a terminal window to manually send AT commands to the modem before and/or after dialing. (Hit F7 to send the AT command.) Under Options, you can also manually dial the phone number if automatic dialing is not possible. Unless you experience trouble with a particular modem, the Properties dialog should not require any changes.
John Petz is an R&D engineer at Advanced Control Systems in Atlanta, GA. He has a Master's degree in Computer Engineering and has been designing real-time embedded systems for over ten years. His current interests are in distributed computing and component middleware.