Ray Patch and Alok Sinha
{ewc navigate.dll, ewbutton, /Bcodeview /T"Click to open or copy the code samples from this article." /C"samples_4}
NetBIOS programming for the MicrosoftÒ WindowsÔ operating system presents special challenges. The nonpreemptive nature of Windows1 requires you to use asynchronous NetBIOS commands. The fact that Windows can move your code and data segments during program execution complicates things further. You have to work around data and code segment movement without fixing all data and code segments of an application in memory. We present a sample program to show you how.
Windows-based programs can use protocols such as the NetBIOS interface, Microsoft LAN Manager named pipes, NovellÒ NetWareÒ SPX/IPX interfaces, or RPC interfaces to communicate over a network. The advantage to NetBIOS is that it is supported by most major LAN software vendors, so the same program can run unchanged in multiple LAN environments.
As mentioned in “An Introduction to Network Programming Using the NetBIOS Interface,” MSJ (Vol. 7, No. 2), application programs use the NetBIOS programming interface to submit requests for services. NetBIOS drivers can be implemented as applications, TSRs, or device drivers. A standard 64-byte data structure called a Network Control Block (NCB) is used to send commands to the NetBIOS driver (see Figure 1).
To submit an NCB to the NetBIOS driver in the MS-DOSÒ operating system, you need to set ES:BX to the address of an NCB structure and either use INT 5CH or call the C function Int86x.
In Windows, you call the Windows API NETBIOSCALL from an assembly-language routine so that ES:BX can be set to the address of the NCB prior to making the call (see Figure 2). NETBIOSCALL uses INT 5CH on behalf of the calling application.
The result of an NCB command is returned in the NCB’s ncb_retcode field. In case an error is encountered in making the call itself (as opposed to errors caused by execution of the NCB command), AX may contain a return value, or zero for success. For asynchronous NCB commands, the immediate value of ncb_retcode is set to FFH. Once the command completes, the final error codes (or zero for success) are placed in the ncb_retcode and ncb_cmd_cplt fields of the NCB.
As mentioned in the previous article, NetBIOS commands can be executed synchronously or asynchronously. Asynchronous commands are important in NetBIOS programming for Windows because they let you avoid blocking the system. For example, you can submit an NCB with the command field set to NCB.ADD.NAME, which does not return until the name has been added properly to the LAN Adapter (LANA) name table or an error occurs. Of course, the application is completely blocked until the NetBIOS command completes. To avoid this, you can set the command field of the NCB to NCB.ADD.NAME | ASYNCH, where ASYNCH is defined as 80H. The NCB submission completes right away and returns the pending status code NRC_PENDING in the ncb_retcode field. The application can continue processing. When the name has been added or an error occurs, NetBIOS sets the ncb_cmd_cplt field of the NCB. The application needs to poll the ncb_cmd_cplt field to determine if the command completed.
An asynchronous NCB must not be unlocked or freed until the command completes. This is very important as Windows can move or discard the memory block containing the NCB structure. You can specify timeout periods in the ncb_rto (Receive Time Out) and ncb_sto (Send Time Out) fields of the NCB. After the NCB command completes or times out, the NetBIOS driver sets the ncb_retcode and ncb_cmd_cplt fields of the NCB.
While all NetBIOS commands (synchronous and asynchronous) are allowed under Windows, the synchronous versions of most commands should be avoided since they will block waiting for NetBIOS to complete. Since Windows is nonpreemptive, programming practices that tie up the processor for long periods, such as looping, blocking, or excessive file operation, must be avoided. This rule is not universal since some synchronous commands will complete quickly.
An asynchronous NetBIOS command has two types of return codes: immediate and final. The immediate return code is placed in the ncb_retcode field upon return from the call to NetBIOS. If this field has the value FFH, the command is still pending. Upon completion, the final return code will be placed in the ncb_cmd_cplt field. If the ncb_retcode field is not FFH, the command completed immediately and its value is the return code. The ncb_post field must either be set to 0 or contain the segment:offset of a post routine. When the ncb_post field is 0, the application must poll the ncb_cmd_cplt field. When the ncb_post field is filled with the address of a post routine, NetBIOS will call that routine when the command has completed. At the time NetBIOS executes the call, ES:BX is filled in with the segment:offset of the completed NCB, the CPU flags have been pushed onto the stack, and interrupts have been disabled. This calling convention is defined by NetBIOS but can vary from implementation to implementation.
Figure 3 illustrates the flow of control between NetBIOS, the post routine, and the application.
Figure 3 The relationship between NetBIOS, Windows, the post routine, and your executable.
Post routines are the preferred way to write NetBIOS applications for Windows since polling the ncb_cmd_cplt field and blocking on synchronous calls can be avoided. A post routine is a function in your application code that gets called after an asynchronous call completes or times out. It is useful for applications that wish to use asynchronous NetBIOS commands without polling ncb_cmd_cplt for completion of the NCB.
Post routines and asynchronous NCB calls do have drawbacks. Since Windows has the ability to move or discard unlocked data or code, it is likely that the post routine, the NCB, and the NCB buffer addresses (ncb_buffer field) will not be in the same place they were when the NCB was submitted. If any of these addresses are relocated in memory, a protection violation (UAE) might occur or the system might crash when NetBIOS attempts to use them. This can be avoided by making sure all code and data addresses are locked in memory when interacting with NetBIOS. To ensure a safe call to the post routine, place it in its own DLL and declare its code segment as fixed in the DLL’s DEF file. As for the NCB and its buffer address, both should be dynamically allocated using GlobalAlloc and locked in memory using either GlobalLock or GlobalWire. These addresses should be locked just prior to the call to NetBIOS and unlocked upon completion of the NCB. GlobalWire is preferred to GlobalLock if the data object needs to be locked for long periods of time since it will lock the memory at the lowest possible location. NCBs and NCB buffers should not be declared statically within the application’s data segment unless the data segment has been locked.
There are other drawbacks to post routines. The post routine is called in interrupt context when hardware interrupts have been turned off and therefore it must do all its processing quickly. Most Windows APIs and C run-time functions should not be called inside the post routine. The post routine is responsible for saving, loading, and restoring the DS register if it needs to access any of its own global data items. The best way to implement the post routine is for it to note when a particular NCB has completed, return to NetBIOS, and process the NCB at a less critical time. The most elegant way to accomplish this is to place the NCB’s address on a queue. Fortunately, instead of creating and manipulating a queue yourself, you can let Windows do the work for you. You can call PostMessage within the post routine to place a message in your application’s message queue. The NCB can be processed when this message is received. It is safe to call PostMessage because it does not block; it just posts the message and returns immediately.
PostMessage(hWnd, WM_NETBIOS, 0, (LONG) &ncb)
Here, hWnd is the handle of the desired window procedure, WM_NETBIOS is a user-defined message, and lParam contains the address of the completed NCB. The third parameter, wParam, is zero here, but it could be used to hold such things as the completed NCB command, a priority, a submessage to WM_NETBIOS, and so on.
Since Windows is event-driven, it would be nice to make the messaging between Windows and the post routine more robust. The above example provided only one type of message, WM_NETBIOS, and only one window was able to receive the message, the window identified by hWnd. To allow any message to be sent to any window, you need to first define an interface to submit the NCB and request that a specific message be sent to a specified window when the NCB completes. The interface should be extended to include synchronous NCBs as well. The function could be called something like NetBiosPostMessage.
WORD far pascal NetBiosPostMessage(hWnd, iMessage,
wParam, lParam),
Above, hWnd is the handle of the window that will receive the message, iMessage is the message to send when the NCB completes, and lParam is the segment:offset of the NCB to submit. We recommend that NetBiosPostMessage and the post routine be implemented in a DLL. That way, you don’t have to worry about locking NCBs and the post processing routine. When an asynchronous NCB is submitted to NetBiosPostMessage, it needs to fix up the NCB by placing the address of the DLL-resident post processing routine into the ncb_post field. Then NetBiosPostMessage should call NetBiosRequest to submit the NCB. The post routine gets called when the NCB command is executed. Within the post processing routine, you should call PostMessage to send a user-defined message to the application window. Your application might not want to receive a message for particular NCB commands. Your interface should give you complete control over whether or not to post the message. Here, the value of wParam controls the posting of messages. The values are as follows:
// Do not post the iMessage.
#defineNOTIFY_OFF0x0000
// Post iMessage only if NCB is synchronous
#defineNOTIFY_IF_SYNC0x0001
// Post iMessage only if NCB is asynchronous
#defineNOTIFY_IF_ASYNC0x0002
// Always send iMessage.
#defineNOTIFY_ALWAYS 0x0003
The following shows how to use this call in a program.
NetBiosPostMessage(hWnd, WM_NETBIOS, NOTIFY_ALWAYS,
(LONG) &ncb).
When the NCB completes, the arguments passed to the window procedure receiving the posted message will have the values shown in Figure 4.
Figure 4 Arguments Passed After the NCB Completes
hWnd | Same hWnd passed to NetBiosPostMessage |
iMessage | Same iMessage passed to NetBiosPostMessage |
wParam | The NetBIOS command that completed |
lParam | Far pointer to NCB that completed |
All of the parameters are the same as those passed to NetBiosPostMesssage, except wParam, which should contain the NCB command and not the notify mask. Figure 5 is a typical window procedure that handles NetBIOS messages.
When an application calls NetBiosPostMessage, the values of hWnd, iMessage, and the NCB’s address have to be saved by NetBiosPostMessage in a table. When the post routine is called, it needs to search this table for the information necessary to do the PostMessage call. The post routine is passed only the completed NCB’s address in ES:BX so the search will have to match this NCB address with the one in the table. For simplicity, we have created a fixed size table but a better method would be to dynamically allocate the table. A table size of twelve was chosen because this is the default number of NCBs available to MS-DOS-based applications under Microsoft LAN Manager 2.x.
#define DEFAULT_MAX_NCBS 12
typedef struct ENTRY
{
PNCB pNcb;
HWND hWnd;
unsigned iMessage;
} ENTRY;
ENTRY NcbTable[DEFAULT_MAX_NCBS];
WORD TableEntriesUsed = 0;
// NCB table pointer used in the post routine.
ENTRY _far *NcbTablePtr = NcbTable;
We recommend that this table be initialized to all zeros in the DLL’s LibMain procedure. During the search process, a value of 0 for the pNcb will mean that that table entry is not in use.
When NetBiosPostMessage is called with wParam equal to zero, the call defaults to NetBiosRequest, and hWnd and iMessage are ignored. The code for NetBiosPostMessage can be found in Figure 6.
Now that you’ve seen the code for NetBiosPostMessage, let’s take a look at the post routine and NetBiosRequest (see Figures 7 and 8). When the post routine gets control, ES:BX contains the NCB’s segment:offset. Since you know this NCB is in the table (because you put it there in NetBiosPostMessage), all you have to do is search the table. Although it is a linear search, the table is small so the search is very fast.
NetBiosRequest calls the function NETBIOSCALL, which is part of the Windows API. This function is the preferred way to submit NCBs rather than calling INT 5CH or INT 2AH directly. NetBiosRequest was declared as far rather than near so that it can be exported by the DLL.
Our sample NetBIOS application, Remote Browser (see Figure 9), illustrates one technique for solving memory (that is, code or data segment) movement problems in Windows.
This program allows any one workstation to be a server and others to be clients. The clients can view the current working directory structure of the server. This remote browsing facility was implemented using versions of the following C run-time functions that were modified to allow for remote access: _getcwd, _dos_findfirst, and dos_findnext.
Remote Browser has two parts, one for clients and one for servers. The client part allows the user to browse the remote server’s directory, while the server part allows the same program to respond to the client requests to browse the server’s directory.
The user interface consists of a simple window with a menu that allows configuring of the local workstation and connecting to a remote server (see Figure 10). The code is in BROWSER.C (see Figure 9). A list of the Connect menu options follows.
Figure 10 Remote Browser
The Configure command generates a WM_COMMAND message with a wParam of IDM_CONFIG. At this time, Remote Browser initializes the client and the server. The Config procedure uses a dialog to prompt for a local name. The client initialization deletes old names, if there are any, and adds new names in the LANA name table using the NCB.ADD.NAME command.
Once the client is initialized correctly, the server part of Remote Browser can be initialized. (Of course, copies of Remote Browser should be loaded on both the client and server workstations.) First the NCB.ADD.NAME command is used to register a new name command and NCB.LISTEN is used to post an asynchronous listen. We put the character A (arbitrary) at the 16th byte of the server (NetBIOS) name to distinguish it from the client.
The server is active as soon as initialization is complete. It can only be deactivated by closing the application.
The Connect command generates a WM_COMMAND with a wParam of IDM_OPEN. If the client has been initialized, the program puts up a dialog box to get the name of a remote server. Then it tries to connect to the remote server via the NCB.CALL command. This is done asynchronously. Upon successful connection, the program draws a small window that displays the current working directory on the server. The remote server’s directory name is obtained through a call to R_getcwd. Remote Browser next creates a list box to display the files present in that directory. The FillUpTheListBox function uses R_dos_findfirst and R_dos_findnext to retrieve the file names in the current working directory of the remote server.
The Close command generates a WM_COMMAND with a wParam of IDM_CLOSE. It disconnects the client from a remote server, if it is connected. An existing session is broken by submitting the NCB.HANGUP command. Once the client is disconnected, it can connect to a new server.
The Exit command generates a WM_COMMAND with a wParam of IDM_EXIT. By closing the application, this command deactivates the client and the server. Deactivation of the client simply means disconnecting it from a remote server by issuing NCB.HANGUP. Next, the client name is deleted from the LANA name table by issuing an NCB.DELETE.NAME command.
To deactivate the server, any existing sessions are canceled by issuing an NCB.HANGUP command. Then any pending NCBs are canceled by issuing NCB.CANCEL. Finally, the server name is removed from the adapter name table by issuing NCB.DELETE.NAME.
The About command generates a WM_COMMAND message with a wParam of IDM_ABOUT, which displays an About box.
The Remote Browser client code is combined with the shell code in BROWSER.C and linked with the network routines in NETIO.C. The client is responsible for initializing itself, connecting to the remote server, and exporting a number of functions (see Figure 11).
Figure 11 Functions Exported by the Remote Browser Client
char * R_getcwd( HWND hwnd, BYTE bLsn, char * pszBuffer, int maxlen);
Gets the current working directory on the server. The function returns a pointer to the current working directory upon successful completion.
hwnd | Handle of the active window. This window will receive asynchronous messages. |
bLsn | Local session number of a valid session between the client and the server. |
pszBuffer | Contains the current working directory string. |
maxlen | Determines the maximum length of the buffer (pszBuffer). |
BYTE R_findfirst( HWND hwnd, BYTE bLsn, char * pszFileName, unsigned attrib, struct find_t *pfindtFileInfo);
Returns details of the first file found during file browsing.
hwnd | Handle of the active window. This window will receive asynchronous messages. |
bLsn | Local session number of a valid session between the client and the server. |
pszFileName | Filename to start search with; can be a wildcard. |
attrib | Attributes associated with files found during search. |
pfindtFileInfo | Upon successful completion, the find_t structure is filled with file details as well as a 21-byte reserved field. This field must be passed as is during subsequent R_dos_findnext calls. |
BYTE R_findnext( HWND hwnd, BYTE bLsn, struct find_t *pfindtFileInfo);
Returns details of the files found during file browsing, based on the current browsing status hidden in the 21-byte reserved field of the find_t structure returned after R_dos_findfirst or subsequent R_dos_findnext calls.
hwnd | Handle of the active window. This window will receive asynchronous messages. |
bLsn | Local session number of a valid session between the client and the server. |
pfindtFileInfo | Upon successful completion, the find_t structure is filled with file details as well as a 21-byte reserved field. This field must be passed as is during subsequent R_dos_findnext calls. |
While these functions appear to work synchronously from the shell’s point of view, they actually work asynchronously in cooperation with the window originating the calls, which in this case is the main window. It is difficult to keep track of the status of the client at all times during an asynchronous operation. This problem is solved by devising a finite state machine (FSM) for the client. Implementing the client as an FSM makes it easier to track the state of the client and to isolate bugs. The following enumerated type lists all the states of the client FSM:
typedef enum _ClientFSM { C_UNINITIALIZED,
C_INITIALIZED, C_CALLING,
C_CONNECTED, C_SENDING,
C_SENT, C_RECEIVING,
C_RECEIVED
} ClientFSM;
Figure 12 shows the possible transitions between these states.
Figure 12 Client State Transitions
Let’s follow the state transitions of a client as the user attempts to connect to a remote server. First, a dialog box prompts the user for the name of the remote server. This is done by the Connect procedure in BROWSER.C. Next, the state of the client is changed to C_CALLING and the client returns from the procedure. An asynchronous call made via the Call function in NETIO.C allows the user to switch to another application while Remote Browser attempts to connect to the remote server.
A successful completion of Call indicates that a valid NetBIOS session has been set up with the remote server. Upon successful return from Call, the client state is set to C_CONNECTED; otherwise it is set back to C_INITIALIZED.
Inside the Call function, an NCB.CALL | ASYNCH command is submitted. Following that, a request is made to the post processing routine to send the main window (identified by hWnd) a message called BW_CALL_BACK upon completion of the NCB.
irc = NetBiosPostMessage(hWnd, // Main window
BW_CALL_BACK,// Message sent to
// main window
NOTIFY_IF_ASYNC,// Notify value.
pncbNCB);// Pointer to the
// NCB.
After submitting the command, the program has two options. One would be to return from the Call function immediately after posting the NCB. In this case, the originating window, the main window, receives notification of completion of the NCB.CALL command and then notifies the user about the establishment of the connection.
The other method, which is the one used here, is to loop inside the Call function until NCB.CALL completes and pass back a return code to the calling window indicating connection success or failure (see Figure 9).
You might think that this looping method prevents any other application from responding to user input. This problem is solved by calling MessageLoop in BROWSER.C. MessageLoop in turn calls PeekMessage to yield control to other applications.
But looping presents another problem. NetBiosPostMessage notifies the main window when the NCB completes. The main window then needs to signal the looping Call function to terminate the loop. We’ve used an enumerated global variable, enumCallStatus, to accomplish this. It is set to CALL_START by the Call function and later set to CALL_CMPLT (call completed successfully) or CALL_ERROR (an error occurred) by the main window.
Most of the server code is in SERVER.C. BROWSER.C contains both client and server notification code. The server FSM states are:
typedef enum _ServerFSM {
S_UNINITIALIZED,
S_INITIALIZED, S_LISTENING,
S_CONNECTED, S_RECEIVING,
S_RECEIVED, S_SENDING,
S_SENT, S_ENDING
} ServerFSM;
Figure 13 shows the transitions between these states.
Figure 13 Server State Transitions
The server is initialized after a successful client initialization. Next, the server posts an asynchronous listen. As each asynchronous listen times out, it posts a new listen. Once a client connects to the server via the NCB.CALL command, the server goes into receive mode (S_RECEIVING). The server posts asynchronous receives using NCB.RECEIVE.ANY. At this time, the client can ask for services by sending request packets via NCB.SEND commands.
In Remote Browser, the server recognizes service requests to perform the following functions:
_getcwd()
_dos_findfirst()
_dos_findnext()
In each case, it invokes these calls locally and sends the result back to the client. Thus, the client must switch back to receive mode after sending a request packet. This occurs in all three of the client functions: R_getcwd, R_dos_findfirst, and R_dos_findnext. In Remote Browser, the client initiates a transaction by first setting itself to the C_SENDING state. Then it sends a request packet to the server. After a successful send, the client sets itself to the C_RECEIVING state and waits for a reply to its earlier request by posting an asynchronous NCB.RECEIVE. Upon successfully receiving a packet, the client sets its state back to C_CONNECTED. The server goes through the exact opposite states. It sets itself to S_RECEIVING prior to receiving a packet. Before sending data back to the client, it briefly changes its state to S_SENDING. After sending the data back to the client, it goes back into receive mode by changing its state to S_RECEIVING.
The server keeps servicing this client until it disconnects. When this happens, the server goes back to listening mode waiting for yet another client to connect.
NetBIOS applications must lock NCBs that are to be used for asynchronous commands. The Remote Browser shows one technique for dynamically allocating and locking memory. ALLOC.C exports three functions for this purpose (see Figure 14).
Figure 14 Functions Exported by ALLOC.C
NCB * NcbAlloc(int iNosNcb) | Returns one or more pointers to zero-initialized NCB structures locked in global memory |
LPVOID NcbAllocBuf (WORD wDataLen) | Returns a pointer to a zero-initialized buffer locked in global memory. |
VOID NcbFree (LPVOID) | Frees the memory allocated using above two functions. |
NcbFree takes an LPVOID pointer to a locked memory block, but needs a handle to that memory to unlock it. NcbAlloc, which you call to allocate a memory block, calls GlobalAlloc on your behalf. It obtains a handle but returns to you a pointer to a memory block (an NCB). However, NcbFree needs to be able to reference the handle to that global memory at a later time. This is taken care of by the following structure:
typedef struct _Block
{
HANDLE hMem;
BYTE bMem;
} BLOCK;
The NcbAllocBuf routine allocates a variable-size memory block. The byte field (bMem), which serves as a placeholder, is always the first byte of this variable-size block. The handle is saved in the hMem field of _Block and the user is returned a pointer to the bMem field of _Block. NcbFree just looks up the handle associated with _Block by subtracting SIZEOF(HANDLE) from the memory pointer passed by the caller. Then NcbFree can easily call GlobalUnlock and GlobalFree.
In this article, we’ve discussed how to solve the programming problems faced by NetBIOS application writers developing in Windows. We’ve demonstrated how to use asynchronous NetBIOS commands to deal with the fact that Windows is nonpreemptive and we’ve presented techniques to circumvent data and code segment movement without requiring all data and code segments to be fixed.
In the Windows NTÔ operating system, NetBIOS programmers will be allowed to associate a Win32Ô event with an NCB when making asynchronous calls. (A Win32 event behaves like a semaphore and can be used as a signaling mechanism between any two processes. In this context, an application could wait on the event after submitting an asynchronous NCB.) The event is set to signaled state when an asynchronous command completes. (This information is based on the December 1991 prerelease version of the Microsoft Windows 32-bit Development Kit—Ed.) NetBIOS programs for Win32 environments can utilize this method to handle asynchronous commands. Meanwhile, you can use the methods discussed here
1For ease of reading, “Windows” refers to the Microsoft Windows operating system. “Windows” is a trademark that refers only to this Microsoft product.