Novell Home

Writing Library NLMs

Articles and Tips: article

ADAM B. JEROME
Developer Support Engineer
Novell Developer Support

01 Nov 1996


Explains several programming tips and techniques for Library NLM engineering.

Introduction

This DevNote explains several programming tips and techniques for Library NLM engineering. XLIB1.EXE, referenced in this article, contains additional information and techniques for writing Library NLMs. This file can be obtained from Novell Developer Support's standard example code distribution points including: ftp://ftp.novell.com/pub/netwire/ndevsup/05

A Library NLM is an NLM application that exports symbols (both functions and values) that can be accessed by library client NLM applications. Client NLM applications are dynamically linked to Library NLM symbols at load time, or self-linking at runtime. Library NLMs are similar to Windows DLL files in many respects.

There is little difference between a Library NLM and a non-library NLM. Both must have a main() function and both must adhere to proper shut down conduct. (See "Graceful NLM Demise," Novell Developer Notes, January 1996, pp. 48 - 50.) The difference is that Library NLMs export addresses, of functions and static storage, to NetWare's public symbol table. This allows other client NLMs to access the Library NLM's exported functions and values.

Let's examine the process of constructing an NLM to better understand Library NLM concepts. Consider the following "Hello world" example.

/*File: HELLO.C */



#include <stdio.h>


void main(void)

      {

      printf("Hello world.\n");

      }

The printf() function is not defined within the file HELLO.C. Therefore we can say that HELLO.C makes a reference to an external function called printf(). STDIO.H prototypes this function to the compiler as an external function. The following line from STDIO.H shows this prototype.

extern int printf(const char*, ...);

During the compilation of HELLO.C, the extern keyword tells the compiler that printf() cannot be resolved at compiling time. The compiler constructs a note (or object record) in its output file (HELLO.OBJ) to inform the linker of the unresolved printf symbol.

A linker is used to produce an executable NLM application (HELLO.NLM) from its object components (HELLO.OBJ). There are specific linkers suitable for building NLM executables. (See "NLM Development Environment," Novell Developer Notes, February 1996, pp. 49-54). NLM linkers, like linkers of other environments, process object records and attempt to resolve symbols which could not be resolved by the compiler. As unresolved symbols, such as printf, are encountered the linker first looks through all specified object files (.OBJ) for a match. If the symbol remains unresolved, the linker will then look through all specified object libraries (.LIB).

In our HELLO.NLM example, the symbol printf() is not resolved at this point because it is not defined in any specified .OBJ file, and the linker has been instructed to not attempt to resolve any symbols using static .LIB files.

Normally, a linker would generate an "Unresolved external" type error at this point. However, an NLM linker allows for IMPORT symbol options that inform the linker that the specified symbol will be available at load-time. The IMPORT linker directive allows the developer to specify symbols individually, or list the symbols in one (or more) .IMP files.

An .IMP file is an ASCII text file which lists a suite of symbols offered by a particular Library NLM. In the case of HELLO.NLM and printf(), one would normally IMPORT the symbols supplied in the NLM SDK file CLIB.IMP. If you bring CLIB.IMP up in your text editor, you will see a line similar to the following.

printf,

When the linker is attempting to resolve the printf() symbol, and upon finding this symbol in CLIB.IMP, an entry is made in HELLO.NLM's import symbol list (a structure internal to the NLM's file format) to indicate to the NLM Loader/Linker that this symbol (printf) must be resolved before the NLM can be executed.

Note: At this point, if a symbol remains unresolved by any of the above three methods, an NLM linker will issue an error message indicating that the specified symbol is unknown. An executable NLM will not be created.

When the NLM is loaded from the console, the NLM Loader/Linker attempts to resolve the loading NLM's unresolved symbols by consulting NetWare's Public Symbol Table. In the case of HELLO.NLM, the loader would find the printf symbol in NetWare's Public Symbol Table (assuming CLIB.NLM has been previously loaded). The table contains an entry-point address for each symbol. The NLM Linker/Loader finishes linking HELLO.NLM's printf statement by "fixing up" printf's entry point.

If the NLM Linker/Loader finds that an IMPORT symbol does not exist in NetWare's Public Symbol Table, an appropriate error message is displayed on the NetWare Console screen, and NLM fails to load.

The EXPORT Directive

The section above gave a quick overview of the NLM linker IMPORT directive. There is a similar NLM linker directive called EXPORT. It is used to tell the NLM linker which symbols (functions and variable values) are to be exported from the NLM being linked to NetWare's Public Symbol Table. When an NLM is loaded, the NLM linker/loader examines the NLM's export symbol list and attempts to place each entry found in NetWare's Public Symbol Table along with the symbol's address. A Library NLM is nothing more than an NLM which exports one or more symbols. The EXPORT linker directive allows the developer to specify symbols individually, or list the symbols in one (or more) .EXP files.

If the NLM Linker/Loader discovers that an EXPORT symbol already exists in NetWare's Public Symbol Table, an appropriate error message is displayed on the NetWare Console, and the NLM fails to load.

Note: An .EXP file can be renamed and used by Library NLM clients as an .IMP file.

The ImportSymbol() Function

NLMs can also import symbols at run time, rather than at load/link time. A modified version of HELLO.C, below, shows how this might be done.

/*File: HELLO.C */



#include <advanced.h> /* Defines ImportSymbol() */
#include <process.h>  /* Defines GetNLMHandle() */


void main(void)

   {

   int (*fp_printf)(const char*, ...);



   fp_printf=ImportSymbol(

      /* I- NLMHandle */ GetNLMHandle(),

      /* I- symbolName */ "printf"

      );

   if(fp_printf == NULL)

      goto SYMBOL_NOT_FOUND;



   (*fp_printf)("Hello world.\n");





   UnimportSymbol(

      /* I- NLMHandle */ GetNLMHandle(),

      /* I- symbolName */ "printf"

      );



SYMBOL_NOT_FOUND:



   return;

   }

This is obviously the hard way to write a simple "Hello world" NLM application. However, there are applications that implement these functions for some very good reasons. For example, assume that there is a new function available in NetWare 4.x which was not available in NetWare 3.x. An existing 3.x application may have to execute an elaborate code path as a work-around for such a function. The developer would like to take advantage of the new 4.x function. However, if this function is introduced into an application's code base, the application will fail to load on a 3.x server. The NLM fails to load because the NetWare 3.x NLM Linker/Loader will not find the 4.x specific symbol.

If the application uses ImportSymbol() at runtime, it can take advantage of the new NetWare 4.x function and fall back on the elaborate work-around in the NetWare 3.x environment. This allows a single NLM to take advantage of the newest NetWare technology while maintaining compatibility with older NetWare platforms.

Note that a successful ImportSymbol() call must always be paired with a call to UnimportSymbol(). NetWare keeps track of the number of times a symbol (exported by a library) has been imported to client NLMs. It does this to keep the Library NLM from being unloaded from the console while client NLMs are still using the Library NLM's exported resources.

The IMPORT.NLM Tool

While writing and testing NLMs, and specifically Library NLMs, it is sometimes helpful to verify that a particular symbol exists in NetWare's Public Symbol Table. The code below, IMPORT.C, shows a tool that can be used to do so.

/****************************************************************************

** Program start. 

*/ 

void main(int argC, char *argV[])

   {

   void (*fp)(void);



   /*------------------------------------------------------------------------

   ** Parse command line parameters.

   */

   if(argC != 2)

      {

      ConsolePrintf("*** IMPORT.NLM

      USAGE:  LOAD IMPORT {symbolName}\n\r");

      goto END_ERR;

      }



   /*------------------------------------------------------------------------

   ** Import the symbol.

   */

   fp=ImportSymbol(

      /* NLMHandle */ (int)GetNLMHandle(),

      /* symbolName */ argV[1]

      );



   /*------------------------------------------------------------------------

   ** What happened?

   */

   if(fp == NULL)

      ConsolePrintf("*** Symbol (%s) does not exist.\n\r", argV[1]);

   else                 

      {

      ConsolePrintf("*** Symbol (%s) exists.\n\r", argV[1]);

      UnimportSymbol(

         /* NLMHandle */ (int)GetNLMHandle(),

         /* symbolName */ argV[1]

         );

      }



END_ERR:



   return;

   }

The resulting executable NLM would be used similar to the following:

FS1:load import printf

Loading module IMPORT.NLM

IMPORT

Version 1.00 August 6, 1996

Debug symbol information for

IMPORT.NLM loaded

   *** Symbol (printf) exists.





FS1:

As you construct a Library NLM, you might find this utility useful to verify that your Library NLM symbols have been properly exported.

Example: DEMOLIB1

The following demonstrates a very simple Library NLM which exports its DEMOLIB1_Add function. Let's examine each function in DEMOLIB1.C.

DEMOLIB1: main()

/****************************************************************************

** Program start.

*/

void main(void)

   {

   ++NLM_threadCnt;



   signal(SIGTERM, NLM_SignalHandler);



   --NLM_threadCnt;



   ExitThread(TSR_THREAD, 0);

   };

DEMOLIB1 has a global variable called NLM_threadCnt. It is used to keep track of the number of NetWare threads currently executing DEMOLIB1 code. The start-up thread which executes main() falls into this category, so this value is incremented. We will examine the need for NLM_threadCnt further below in the SIGTERM signal handler function.

The main() function also registers a SIGTERM signal handler so that when the NLM is unloaded (via the NetWare console unload command) the Library NLM can clean up gracefully. Just before the main() function terminates, it decrements the NLM_threadCnt value indicating that the startup-thread will no longer be executing DEMOLIB1 code.

Finally, ExitThread() is called with a TSR_THREAD option. This causes DEMOLIB1's code segment and data segment to remain resident, but terminates the start-up thread along with its stack and associated structures. The symbol DEMOLIB1_Add remains exported to NetWare's Public Symbol Table. Note that ExitThread()'s second '0' parameter (status) is currently ignored by NetWare.

DEMOLIB1: NLM_SignalHandler()



/****************************************************************************

** Program signal handler.

*/

void NLM_SignalHandler(int sig)

   {

   switch(sig)

      {

      case SIGTERM:



         while(NLM_threadCnt != 0)

            ThreadSwitchWithDelay();



         break;

      }



   return;

   }

This function will only be executed when the SIGTERM signal event occurs. It occurs when a NetWare console operator issues an UNLOAD command against our NLM. The code simply waits for all threads to vacate DEMOLIB1's code before it allows the NLM to be unloaded. This is accomplished by polling the NLM_threadCnt value until it becomes zero.

The thread which executes this code is the same thread that maintains the NetWare console screen (ie: the colon prompt).

DEMOLIB1: DEMOLIB1_Add()



/****************************************************************************

** Exported function.

*/

int DEMOLIB1_Add(int value1, int value2)

   {

   int total;



   ++NLM_threadCnt;



   total=value1+value2;



   --NLM_threadCnt;





   return(total);

   }

The DEMOLIB1_Add function is the one we want other NLMs to call, and the one that is specified using the EXPORT linker directive. As you can see, it simply adds the values of the two parameters and returns this value to the caller. In order to keep track of the number of threads currently executing DEMOLIB1 code, the NLM_threadCnt value is incremented as the caller's thread enters this function, and decrements as the thread exits this function.

All exported Library NLM functions should be coded so that they are reentrant. This is required to allow multiple client threads to execute the exported function at the same time. DEMOLIB1_Add is coded so that it is reentrant. For example, the total value is an automatic storage type (ie: stored on the stack), and since each client thread has its own stack, this value is manipulated in each client thread's context. If the total value was declared as a static type (ie: stored in DEMOLIB1's data segment), the return value of DEMOLIB1_Add would be unpredictable.

Testing DEMOLIB1.NLM

Before loading DEMOLIB1.NLM, the symbol DEMOLIB1_Add is not listed in NetWare's Public Symbol Table. You can verify this using the IMPORT.NLM utility. The IMPORT.NLM utility can also be used to verify that the symbol DEMOLIB1_Add has been properly exported by DEMOLIB1.NLM to NetWare's Public Symbol Table. Finally, if you unload DEMOLIB1.NLM, you can again use the IMPORT.NLM utility to verify that the symbol DEMOLIB1_Add has been removed from NetWare's Public Symbol Table.

To further test DEMOLIB1.NLM, you could construct the following NLM which calls DEMOLIB1_Add() and prints the result.

#include <stdlib.h>
#include <stdio.h>
#include <conio.h>


extern int DEMOLIB1_Add(int, int);



/****************************************************************************

** Program start.

*/

void main(void)

   {

   int a = 23;

   int b = 91;

   int c;



   c=DEMOLIB1_Add(a, b);



   printf("%d + %d = %d\n", a, b, c);

   printf("Waiting for a keystroke before unloading...");

   getch();



   return;

   }

When linking the above, remember to define the symbol DEMOLIB1_Addusing the linker IMPORT directive.

Some other test points are:

  • Load TESTLIB1.NLM when DEMOLIB1.NLM is not loaded. You will discover that NetWare prints a message similar to thefollowing:

    Loader cannot find public symbol: DEMOLIB1_Add
  • Unload DEMOLIB1.NLM while TESTLIB1.NLM is still running. You will discover that NetWare 4.x prints a message similar to the following:

    You must unload TESTLIB1.NLM before you can unload DEMOLIB1.NLM

NLM Module Dependence

TESTLIB1.NLM could be improved by informing the NetWare Linker/Loader that it is dependent on DEMOLIB1.NLM. By doing so NetWare's Linker/Loader would attempt to load DEMOLIB1.NLM automatically, if it is not already loaded, when TESTLIB1.NLM is loaded.

NLM linkers can be notified of an NLM's dependancy upon other Library NLMs via the MODULE linker directive. The MODULE directive is used to specify one or more NLMs to be pre-loaded before the requested NLM is loaded. NLM linkers place module dependence information into the header structure of linked NLM executables. When the NLM is loaded (via console operator command or some other method) the NetWare Linker/Loader attempts to load each of the listed NLMs before the actual NLM is loaded. If any of the "dependent" module NLMs cannot be loaded, the initial load request fails.

Each NLM can therefore have its own list of support modules (NLMs) that must be loaded first. This can have a cascading effect causing many NLMs to be loaded from a single LOAD command.

Exported Symbol Naming Convention

You might have noticed that the name of the exported function in DEMOLIB1 carried the name of the Library NLM that exported it as an uppercase prefix followed by an underscore character. Novell strongly encourages developers to adhere to this convention. This convention helps avoid duplicate name conflicts between Library NLMs and also helps NetWare system support professionals know which Library NLM is exporting a particular symbol.

DEMOLIB2

The second Library NLM example that I will discuss here is DEMOLIB2 which exports two functions; DEMOLIB2_Malloc() and DEMOLIB2_Free(). It is very similar to DEMOLIB1 in most respects.

/****************************************************************************

** Exported function.

*/

void *DEMOLIB2_Malloc(LONG size)

   {

   void *vp;

   LONG *msignature;

   LONG *msize;

   void *muser;



   ++NLM_threadCnt;



   /*------------------------------------------------------------------------

   ** Allocate memory as requested by the size parameter, plus a LONG for

   ** a memory signature and another LONG to store the size of the

   ** allocation.

   */

   vp=malloc(size+(sizeof(LONG) * 2));

   if(vp == NULL)

      goto END_ERR;



   /*------------------------------------------------------------------------

   ** Initialize data pointers.

   */

   msignature = (LONG *)vp;

   msize = (LONG *)((BYTE *)vp + 4);

   muser = (void *)((BYTE *)vp + 8);



   /*------------------------------------------------------------------------

   ** Place the DEMOLIB2_Alloc signature and size into the first four and

   ** eight bytes of the allocated memory.

   */

   *msignature=0xBEEFF00D;

   *msize=size;



   /*------------------------------------------------------------------------

   ** Copy the "Allocated" signature into every byte of the "user" portion

   ** of the allocated memory.

   */

   memset(muser, 0xAC, size);





END_ERR:



   --NLM_threadCnt;





   return(muser);

   }



/****************************************************************************

** Exported function.

*/

int DEMOLIB2_Free(void *vp)

   {

   int cCode = 0;

   LONG *msignature;

   LONG *msize;

   void *muser;

   LONG  size;



   ++NLM_threadCnt;



   msignature=(LONG *)((BYTE *)vp - 8);

   msize = (LONG *)((BYTE *)vp - 4);

   muser = vp;



   /*------------------------------------------------------------------------

   ** Verify DEMOLIB2_Alloc signature.

   */

   if(*msignature != 0xBEEFF00D)

      {

      cCode = -1;

      goto END_ERR;

      }



   /*------------------------------------------------------------------------

   ** Store the size.

   */

   size=*msize;



   /*------------------------------------------------------------------------

   ** Copy the "Freed" signature into every byte of the structure.

   */

   memset(msignature, 0xFE, size + (sizeof(LONG) * 2));



   /*------------------------------------------------------------------------

   ** Free the specified memory.

   */

   free(msignature);



END_ERR:



   --NLM_threadCnt;





   return(cCode);

   }

Ownership of System Resources

As you can see, these functions allocate and free a system resource, namely memory. You can use the TESTLIB2 utility to experiment with DEMOLIB2.

void main(void)

   {

   void *vp;

   int cCode;



   /*------------------------------------------------------------------------

   ** Allocate memory using function exported by DEMOLIB2.

   */

   vp=DEMOLIB2_Malloc(100);

   if(vp == NULL)

      {

      printf("DEMOLIB2_Malloc(100) failed.\n");

      goto END_ERR;

      }

   printf("Memory has been allocated.  vp=%08X\n", vp);



END_ERR:



   /*------------------------------------------------------------------------

   ** Free (as needed) memory using function exported by DEMOLIB2.

   */

   if(vp != NULL)

      {

      cCode=DEMOLIB2_Free(vp);

      if(cCode != 0)

         printf("DEMOLIB2_Free(vp) failed: %d\n", cCode);

      else

         printf("Memory has been freed.\n");

      }



   printf("Waiting for a keystroke before unloading....");

   getch();



   return;

   }

For our purposes, the resource allocated by DEMOLIB2 could be a file, server screen, memory, or any resource that is allocated to a specific NLM. Here is the question: Which NLM owns the allocated memory; DEMOLIB2 or TESTLIB2? The answer is TESTLIB2. You can verify this by changing TESTLIB2 so that it does not call DEMOLIB2_Free(). By doing so, when TESTLIB2 terminates, NetWare will print out one of those nasty messages indicating that TESTLIB2 terminated without cleaning up all its memory resources. Even though the actual malloc() function is called in the DEMOLIB2 code, the NetWare thread executing this code is owned by TESTLIB2. Therefore malloc() tags the memory as owned by TESTLIB2. In other words, an attribute of the executing thread's context is that any resources it allocates will belong to its owning NLM (TESTLIB2.NLM in this case).

This attribute of a thread's context can be modified so that the resources allocated by the thread will be tagged to another owner. This is generally done using the GetThreadGroupID() and SetThreadGroupID() functions. Using these functions, the resource ownership attribute of one thread's context can be applied to a second thread's context.

For example, the initialization code of a Library NLM can call GetThreadGroupID() to get main()'s thread context. Then exported Library NLM functions can use SetThreadGroupID() to impose main()'s thread context on the thread. This will cause all subsequent system resource allocations to be tagged to the Library NLM rather than the client NLM. Of course, before the exported Library NLM code returns control to the client NLM, it should generally reset the context back to the client NLM's context. If the context is not reset, system resources allocated by this client thread will continue to be tagged to the Library NLM rather than the client NLM.

Calling GetThreadGroupID in DEMOLIB2's main() function would not be productive since main() terminates by calling ExitThread(TSR_THREAD, 0). This call effectively destroys the thread's context. Any references to a destroyed thread context, as obtained by GetThreadGroupID(), are invalid and unsuitable as a parameter to SetThreadGroupID().

DEMOLIB3

The next Library NLM example, DEMOLIB3, suspends the library's main()thread rather than destroying it.

/****************************************************************************

** Program start.

*/

void main(void)

   {

   ++NLM_threadCnt;



   /*------------------------------------------------------------------------

   ** Initialize storage.

   */

   NLM_mainThreadID=GetThreadID();

   NLM_mainThreadGroupID=GetThreadGroupID();





   /*------------------------------------------------------------------------

   ** Register a SIGTERM signal handler for the UNLOAD event.

   */

   signal(SIGTERM, NLM_SignalHandler);





   /*------------------------------------------------------------------------

   ** Put main()'s thread to sleep until the UNLOAD event.

   */

   SuspendThread(NLM_mainThreadID);





   /*------------------------------------------------------------------------

   ** Do not allow main()'s thread to terminate until its context is no

   ** longer needed by client NLMs running in DEMOLIB3.NLM's code space.

   */

   while(NLM_threadCnt != 1) 

      ThreadSwitchWithDelay();



   --NLM_threadCnt;





   return;

   }

This is done so that DEMOLIB3's main() thread context can be applied to client NLM threads allowing system resources, allocated by the client NLM threads, to be tagged to the Library NLM rather than to the client NLM.

/****************************************************************************

** Exported function.

*/

void *DEMOLIB3_Malloc(LONG size)

   {

   int nativeThreadGroupID;

   void *vp;

   LONG *msignature;

   LONG *msize;

   void *muser;

   

   ++NLM_threadCnt;



   /*------------------------------------------------------------------------

   ** Set this thread's context to DEMOLIB3 main()'s context, preserving the

   ** thread's native context.

   */

   nativeThreadGroupID=SetThreadGroupID(NLM_mainThreadGroupID);





   /*------------------------------------------------------------------------

   ** Allocate memory as requested by the size parameter, plus a LONG for

   ** a memory signature and another LONG to store the size of the

   ** allocation.

   */

   vp=malloc(size+(sizeof(LONG) * 2));

   if(vp == NULL)

      goto END_ERR;



   /*------------------------------------------------------------------------

   ** Initialize data pointers.

   */

   msignature = (LONG *)vp;

   msize = (LONG *)((BYTE *)vp + 4);

   muser = (void *)((BYTE *)vp + 8);



   /*------------------------------------------------------------------------

   ** Place the DEMOLIB2_Alloc signature and size into the first four and

   ** eight bytes of the allocated memory.

   */

   *msignature=0xBEEFF00D;

   *msize=size;



   /*------------------------------------------------------------------------

   ** Copy the "Allocated" signature into every byte of the "user" portion

   ** of the allocated memory.

   */

   memset(muser, 0xAC, size);





END_ERR:



   /*------------------------------------------------------------------------

   ** Reset this thread's context back to its native context.

   */

   SetThreadGroupID(nativeThreadGroupID);





   --NLM_threadCnt;





   return(muser);

   }



/****************************************************************************

** Exported function.

*/

int DEMOLIB3_Free(void *vp)

   {

   int nativeThreadGroupID;

   int cCode = 0;

   LONG *msignature;

   LONG *msize;

   void *muser;

   LONG  size;



   ++NLM_threadCnt;



   /*------------------------------------------------------------------------

   ** Set this thread's context to DEMOLIB3 main()'s context, preserving the

   ** thread's native context.

   */

   nativeThreadGroupID=SetThreadGroupID(NLM_mainThreadGroupID);





   /*------------------------------------------------------------------------

   ** Initialize storage.

   */

   msignature=(LONG *)((BYTE *)vp - 8);

   msize = (LONG *)((BYTE *)vp - 4);

   muser = vp;



   /*------------------------------------------------------------------------

   ** Verify DEMOLIB2_Alloc signature.

   */

   if(*msignature != 0xBEEFF00D)

      {

      cCode = -1;

      goto END_ERR;

      }



   /*------------------------------------------------------------------------

   ** Store the size.

   */

   size=*msize;



   /*------------------------------------------------------------------------

   ** Copy the "Freed" signature into every byte of the structure.

   */

   memset(msignature, 0xFE, size + (sizeof(LONG) * 2));



   /*------------------------------------------------------------------------

   ** Free the specified memory.

   */

   free(msignature);



END_ERR:



   /*------------------------------------------------------------------------

   ** Reset this thread's context back to its native context.

   */

   SetThreadGroupID(nativeThreadGroupID);





   --NLM_threadCnt;





   return(cCode);

   }

If you comment out TESTLIB2's DEMOLIB2_Free() function call NetWare reported that TESTLIB2.NLM did not free all its memory resources. However if you comment out TESTLIB3's DEMOLIB3_Free() function call NetWare reports no error when TESTLIB2.NLM unloads. However, if you now unload DEMOLIB3.NLM, you will see that NetWare reports that DEMOLIB3.NLM did not free all its memory resources. This is because the memory allocated was tagged to the Library NLM, DEMOLIB3.NLM.

Tracking Library NLM Resources

DEMOLIB3 presents an interesting issue. If TESTLIB3 does not properly call the DEMOLIB3_Free() function before it terminates, system resources (memory in this case) remain unfreed.

There may be a case where this effect is implemented as useful. For example, a client NLM could call into a library NLM with data that could be stored by the library NLM and accessed globally by other client NLMs. In such a case, it may be reasonable for the data to outlive the NLM which submitted the data. If this is the case, the Library NLM must keep track of all such resource allocations and properly free each of them when the Library NLM is unloaded.

However, in the case of DEMOLIB3, the allocated memory resource would not be used by other client NLMs. The technology in DEMOLIB3 simply assumes that the client NLM will always pair a DEMOLIB3_Malloc() call with a DEMOLIB3_Free() call. This is not a good assumption. To make DEMOLIB3 a bit more bulletproof, it must be informed when one of its client NLMs have been unloaded. If a Library NLM tracks all resources issued to each client NLM, and it can be made aware that a particular client NLM has unloaded, the Library NLM can properly free the resources of the ill-behaved client NLM.

DEMOLIB4

DEMOLIB4 demonstrates how a Library NLM can keep track of its client NLMs resources and also know when one of its clients unloads. In such an event, DEMOLIB4 cleans up resources allocated to the now terminated client NLM. Details of how this is done are outlined below.

Library APIs

NetWare can inform your Library NLM when a client NLM terminates. This is done by the Library NLM registering a call-back client cleanup function with NetWare using the RegisterLibrary() function. DEMOLIB4 does this in the main() function as follows:

NLM_libraryHandle=RegisterLibrary(NLM_ClientCleanUp);

if(NLM_libraryHandle == 0xFFFFFFFF)

   {

   ConsolePrintf("DEMOLIB4 Error: RegisterLibrary() failed.\n\r");

   goto END_ERR;

   }

As you can see, DEMOLIB4's client cleanup function is called NLM_ClientCleanUp(). It is important to always pair the RegisterLibrary() function with a DeregisterLIbrary() function call. DEMOLIB4 does this also in main().

Besides registering a client cleanup function, the Library NLM must uniquely label each of its client NLMs. The unique label is used by the client cleanup function to identify which client NLM terminated. This is generally done by having the client NLM call a Library NLM client initialization function. In DEMOLIB4, this function equates to DEMOLIB4_InitClient().

NLM clients are uniquely labeled using the SaveDataAreaPtr() function. The dataAreaPtr value is any arbitrary, but unique to each client, 4-byte value. Generally, it's the address of a memory structure used by the Library NLM to track resources of the particular client NLM.

DEMOLIB4 keeps a doubly linked list structure of all its registered client NLMs. As new client NLMs call the DEMOLIB4_InitClient() functions, DEMOLIB4 allocates a new client node and links it into its client list. DEMOLIB4 uses the memory address of the client's node structure as the dataAreaPtr value for the SaveDataAreaPtr() function. The following excerpt from the DEMOLIB4_InitClient() function shows how SaveDataAreaPtr()is called.

/*------------------------------------------------------------------------

** Allocate a new client node (in memory which is owned by the library).

*/

cNode=NLM_ClientNodeCreate(name);

if(cNode == NULL)

   {

   cCode = EFAILURE;

   goto END_ERR;

   }



/*------------------------------------------------------------------------

** Set the data area pointer of this client as the address of the client's

** linked list node.

*/

cCode = SaveDataAreaPtr(NLM_libraryHandle, (void *)cNode);

if(cCode != ESUCCESS)

   {

   free(cNode);

   ConsolePrintf("DEMOLIB4 Error: SaveDataAreaPtr() failed: [0x%08X]\n\r", 

      cCode);

   goto END_ERR;

   }



/*------------------------------------------------------------------------

** Link the client node into the library's client node list.

*/

NLM_ClientNodeLink(cNode);

After a client NLM has called DEMOLIB4_InitClient(), it may now call the other utility functions exported by DEMOLIB4; namely DEMOLIB4_Malloc() and DEMOLIB4_Free(). As a client NLM calls a Library NLM utility function, the utility function calls the GetDataAreaPtr() function to obtain the previously saved dataAreaPointer information. If the client has not had its dataAreaPtr info set by the Library NLM, subsequent calls to GetDataAreaPtr will fail. DEMOLIB4 utility functions each call NLM_ClientValidate() to verify that the calling client has made a previous call to DEMOLIB4_InitClient(). NLM_ClientValidate() also verifies that the dataAreaPtr is a valid address for a node in its client node list.

C_NODE *NLM_ClientValidate(void)

   {

   C_NODE *cNode;

   C_NODE *curNode;



   /*------------------------------------------------------------------------

   ** Get the client's data area pointer value.

   */

   cNode=(C_NODE *)GetDataAreaPtr(NLM_libraryHandle);

   if(cNode == EFAILURE)

      return(NULL);



   /*------------------------------------------------------------------------

   ** Verify that this Data Area Pointer value is a valid node on the client

   ** NLM list.

   */

   curNode=NLM_clientHead;

   while(curNode != NULL)

      {

      if(cNode == curNode)

         return(cNode);  /* Node found. */



      curNode = curNode->next;

      }



   return(NULL);

   }

When a client NLM terminates, and if the library had previously registered the client using the SaveDataAreaPtr() function, NetWare will call the Library NLM's client clean-up function. In the case of DEMOLIB4 the cleanup function is fairly simple.

/****************************************************************************

** Client resource cleanup (OS call-back function).

**

** Context: NetWare OS.

**

** Called by: External NetWare OS Thread.

*/

int NLM_ClientCleanUp(void *vp)

   {

   int nativeThreadGroupID = EFAILURE; /* EFAILURE=invalid thread group ID */



   ++NLM_threadCnt;



   /*------------------------------------------------------------------------

   ** Set this thread's context to DEMOLIB4 main()'s context, preserving the

   ** thread's native context.

   */

   nativeThreadGroupID=SetThreadGroupID(NLM_libraryContext);

   if(nativeThreadGroupID == EFAILURE)

      goto END_ERR;



   NLM_ClientNodeUnlinkAndDestroy((C_NODE *)vp);



END_ERR:



   if(nativeThreadGroupID != EFAILURE)

      SetThreadGroupID(nativeThreadGroupID);





   --NLM_threadCnt;





   return(ESUCCESS);

   }

NLM_ClientCleanUP() switches to the Library NLM's context, and frees all the outstanding resources of the specific client (if any exists). It also removes the client's node from the client list. You might notice that DEMOLIB4 prints messages to the console screen indicating that it had to clean up after client NLMs; similar to the messages NetWare produces.

Summary

Hopefully you have made it this far. If so, you are a serious programmer and you want the real scoop on writing Library NLMs. Obviously, Library NLM development requires a developer who is familiar with NLM programming philosophies such as threads and contexts. The information and example code supplied with this article should help you on your way to writing your own Library NLM. Feel free to contact us here at Developer Support with any further questions about writing Library NLMs.

* Originally published in Novell AppNotes


Disclaimer

The origin of this information may be internal or external to Novell. While Novell makes all reasonable efforts to verify this information, Novell does not make explicit or implied claims to its validity.

© 2014 Novell