Novell is now a part of Micro Focus

Developing C++ NLMs

Articles and Tips: article

DIRK HOWARD
Software Engineer
Developer Support

01 Jan 1996


One area of software engineering and development that is showing promise of helping tame program development is Object Oriented Programming/Object Oriented Design (OOP/OOD). OOP/OOD stresses the inter-relation between data and the way data is acted upon. The goal is to design a system that works ona given data element or data stream in a tested and reliable fashion. C++ is an OOP language that can be used to write NLMs. The DevNote shows what some some of the stumbling blocks are in C++ NLM development and how to get around them.

Introduction

Developing software for any environment is not a trivial task. The NetWare Loadable Module (NLM) environment brings with it additional considerations. With NLMs you must make sure that all resources that you allocate and use are released before program termination. With NLMs you cannot rely on the OS to clean up after you.

In some ways, developers are responsible for writing a portion of the OS that interfaces with their services. Because the developer needs to make sure that all resources are relinquished before termination, the complexity level of an NLM-based application climbs at an alarming rate. Even so, the benefits of writing NLMs are great. You have tight coupling with the NetWare operating system, and typically you also have very good levels of performance.

One area of software engineering and development that is showing promise of helping to tame program development is Object Oriented Programming/Object Oriented Design (OOP/OOD). OOP/OOD tries to stress the inter-relation between data and the way that the data is acted upon. The goal is to design a system that works on a given data element or data stream in a tested and reliable fashion. Typically this is done by combining data with the operations that will work on it. In the object-oriented world this is know as encapsulation. C++ is an OOP language that implements encapsulation in the language and can be used to develop NLMs.

NLMs are typically written in C. Sometimes small portions are written in Assembly. Technically there is no reason that NLMs cannot be written in other high level languages, including C++.

One of the roadblocks is that when you try to use tools other than C and Assembly, it seems that you are on your own. In this article I show what some of the stumbling blocks are in C++ NLM development and how you can get around them. Novell does not currently provide any C++ class libraries for accessing NetWare services.

Example Goal

In choosing an example project for designing a C++ based NLM, I wanted to meet the following criteria:

  1. The class should demonstrate multithreaded programming techniques.

  2. The class must be able to handle error conditions gracefully.

  3. The class should be self-contained, meaning that the programmer can use it in new ways without having to modify the class itself.

  4. The class should clean up all resources allocated to an instance of a class upon deletion of that instance.

Using these requirements, I decided to implement a class that displays a bar graph of some program statistic. I call this class StatsThread. StatsThread creates a new thread of program execution for each instance of the class that is invoked. It either displays a percentage bar graph, or a bar based on a minimum and maximum value.

StatsThread has three constructors used to create a new instance of the StatsThread object. The first constructor creates an instance that displays a percentage graph. Inputs include the line number to display the graph on, the title for the graph, the interval in milliseconds to wait between updates, and the function to call to get the current value of the statistic.

StatsThread(

   int line,

   char *title,

   unsigned interval,

   long (* percentFunc)(void)

);

The second constructor creates an instance that displays a value bar based on static maximum and minimum values.

StatsThread(

   int line,

   char *title,

   unsigned interval,

   long (* valFunc)(void),

   long lowWater,

   long highWater );

The final class constructor uses functions to dynamically determine the maximum and minimum bar values.

StatsThread(

   int line,

   char *title,

   unsigned interval,

   long (* valFunc)(void),

   long (* lowWaterFunc)(void),

   long (* highWaterFunc)(void)

);

One design issue that quickly came to the forefront, was how a class would start its own thread of execution. I had decided to have the constructor call BeginThreadGroup() passing in the address of a class member function that would be the execution routine for the thread.

I quickly found out that I could not do this because that the address needed to be determined dynamically, and that couldn't be done without generating numerous compiler error messages. I changed the SubThread member function to a static function that all instances of the class would use. To do this, I have to pass in a pointer to the instance of the class to the SubThread procedure.

myThreadID = BeginThreadGroup( SubThread, NULL, NULL, (void *)this );

The SubThread function then had to de-reference this pointer.

static void StatsThread::SubThread(void *arg )

{

   StatsThread *myInstance;

   myInstance = (StatsThread*) arg;

   ...

   while ( !myInstance->shutdown)

   {

      ...

      delay( myInstance->myInterval);

   }

}

It may have been possible to design the SubThread member function as a virtual function to solve the run-time binding issue. I did not explore this avenue since I had managed to get the functionality I wanted from the above implementation.

To close down the thread of execution, the destructor sets a variable that the SubThread checks to see if execution should terminate. The destructor will then spin, checking to see if the thread is still alive. Once the thread of execution has terminated, it is safe for the instance to be destroyed and the memory allocated to it to be returned to the OS.

StatsThread::~StatsThread() 

{

   char   name[40];



   shutdown = TRUE;

   /* While the sub thread for this instance is 

      still running, delay and loop                             */

   while ( GetThreadName(myThreadID, name ) != EBADHNDL )

      delay( 10 );

}

Actual use of the class looks something like this:

void main()

{

   StatsThread *cpuUtil;



   HideInputCursor();

   cpuUtil = new StatsThread(1, "CPU Utilization", 1000, ServerCPU );



   while ( !kbhit() )

      delay( 500 );



   delete cpuUtil;

}

Deleting an instance of the StatsThread class cleans up and releases all resources. There is no fear of some unforgotten resource being left open as long as for each creation there is a deletion.

Compiler and Linker Issues

I used two different tool sets to compile and link my C++ code. The first tool set consisted of Watcom C/C++ Version 10.0 with the linker WLINK, which is provided with the compiler. The second tool set consisted of Borland C/C++ 4.51 with NlinkPro 3.0 from Base Technologies. Watcom C/C++ is the most commonly used compiler for NLM developers and is supported by Novell with their NLMLINK tool.

Watcom C/C++

The first problem I had was determining how to compile my C++ source files into object modules. I first tried changing my compiler from WCC386 to WPP386 and using the same switches that I used with standard NLM compilation. (I used QMK386 to build my make file.) This did not resolve the include file, so I then added a switch for the path to the Novell include headers. I also found that I needed to include the switch /bt=netware to build a C++ NLM. Once I had made these changes to the compiler, I was able to produce object modules.

Once I was successful in compiling my source (.CPP) in to object modules (.OBJ), the next step was to link them together into a NLM. I first tried to use NLMLINK; however it could not handle the needed symbols for the C++ features. NLMLINK left a number of symbols as unknown and could not handle the C++ mangled names. It turns out that the symbols I needed reside in the CLIB3S.LIB file provided by Watcom.

Since NLMLINK does not support the inclusion of libraries, I then turned to WLINK from Watcom to link my NLM. This turned out to work just fine as long as I didn't try to use the Novell supplied PRELUDE.OBJ. If I used the Novell supplied prelude, I got unfreed resource messages at exit.

When testing the resulting NLM, I found that when the NLM was unloaded from the Console that the Console locked up and that the NLM was not unloaded. I traced this down to problems with the Watcom static CLIB and the Watcom implementation of PRELUDE.OBJ.

To solve this problem I needed to use the Novell supplied PRELUDE.OBJ and then strip out all CLIB.NLM functions and symbols from the Watcom CLIB3S.LIB. I built a command file that I then used against CLIB3S.LIB to do this. The command file is called CLIB3S.CMD. The syntax used with WLIB to strip down the static CLIB is as follows:

WLIB CLIB3S @CLIB3S.CMD

This left me with a Watcom CLIB3S.LIB devoid of all Novell provided symbols. I then relinked my NLM with the Novell PRELUDE.OBJ and the striped down version of CLIB3S.LIB. This gave me the result that I was looking for. I now have an NLM that is C++ and behaves consistently.

Borland C/C++ 4.51 and NLINKPro 3.0

Using the Borland C/C++ compiler with NLINKPro was a straightforward task. First I opened the Borland Windows IDE and selected the Project Menu and chose the NLM Expert option. I then entered the name of my project (see Figure 1).

Figure 1: Entering the name of the project.

Once the AppExpert screen is open, I entered information in the following four sections.

Under the Libraries topic, I selected the clibsup.lib support option (see Figure 2).

Figure 2: Selecting the CLIBSUP.LIB option.

Under the Options topic, I entered the information for Screen Name and entered the information for Thread Name (see Figure 3).

Figure 3: Entering Screen Name and Thread name information.

Under the Misc topic, I entered data for the Version number (see Figure 4).

Figure 4: Entering version number information.

Under the Import topic, I entered the symbol GetServerUtilization into the Import Symbol List (see Figure 5).

Figure 5: Entering symbol information.

I then selected OK. Next I went to the menu bar and selected Options | Project and made the following changes.

Under the Compiler topic, I turned off the Generate Underscores option (see Figure 6).

Figure 6: Turning off the Generate Underscores option.

Under the C++ Options topic, I turned off Enable exceptions (see Figure 7).

Figure 7: Turning off Enable exceptions.

At this point I was able to compile and link the NLM, with the result shown in Figure 8.

Figure 8: Compiling and linking results.

Performance

The performance seemed to be pretty good with the NLM from either environment. There are some differences in executable size and performance, but I don't believe those differences will be significant on larger systems written in C++.

Listed below are the time slice statistics, executable size, and memory usage of each NLM.

Watcom C/C++

NLM Size (In Bytes) 28188 Memory Used (In Bytes) 154656

Sentry Time Slice statistics:

Sentry Version 1.12 Time Utilization Report



The maximum time slice allowed on this machine is 155 ms.



List sorted by NLM name-->Process name

                                                                           Max            Avg

NLM-->Process Name                              Time (ms)   Time (ms)  Count    Load

TESTSTAT-->TestStat__P                0          22.441      0.150              1           01.50%

TESTSTAT-->TestStat__P     1           1.914       1.195                0           00.00%

TESTSTAT-->TestStat__P     2           2.283       1.241                0           00.00%

TESTSTAT-->TestStat__P     3           1.604       0.817                0           00.00%

TESTSTAT-->TestStat__P     4          13.392      5.095             0           00.00%

TESTSTAT-->TestStat__P     5          14.269      3.838             0           00.00%

TESTSTAT-->TestStat__P     6          18.467      5.112             0           00.00%

TESTSTAT-->TestStat__P     7          23.110      5.193             0           00.00%

TESTSTAT-->TestStat__P     8          13.487      3.807             0           00.00%

NLM Size (In Bytes) 5246 Memory Used (In Bytes) 154640

Sentry Time Slice statistics:

Sentry Time Slice statistics:



Sentry Version 1.12 Time Utilization Report



The maximum time slice allowed on this machine is 155 ms.



List sorted by NLM name-->Process name

                                            Max           Avg

NLM-->Process Name                          Time (ms)  Time (ms)    Count     Load

TESTSTAT-->TestStat~           0            22.878          0.147      1      01.47%

TESTSTAT-->TestStat~           1             1.711          1.206      3      04.02%

TESTSTAT-->TestStat~           2             1.635          1.222      6      02.03%

TESTSTAT-->TestStat~           3             1.550          0.754      0      00.00%

TESTSTAT-->TestStat~           4             4.608          2.379      3      07.93%

TESTSTAT-->TestStat~           5             4.654          1.421      5      02.84%

TESTSTAT-->TestStat~           6             4.662          1.764      4      04.41%

TESTSTAT-->TestStat~           7             4.775          1.753      5      03.50%

TESTSTAT-->TestStat~           8             4.679          2.388      4      05.97%

Gains and Losses

Even though there is some additional overhead in each instance of the StatsThread class, there are gains made in future development projects that use this class. Once StatsThread has been throughly tested and debugged, we can use it with confidence in future projects.

One way that object oriented programming can help is by allowing inheritance. This means that we reuse code by inheriting features from a previously designed class.

One way that this can be illustrated is by adding functionality to support logging of statistics to the StatsThread class. I will call this new class LoggingStats-Thread; it looks something like this:

class LoggingStatsThread :public StatsThread

{

private:

   LoggingStatsThread(){};



public:

   LoggingStatsThread(int line, char *title, unsigned interval,

      long (* percentFunc)(void) )

      : StatsThread( line,title, interval, percentFunc ){};



   LoggingStatsThread(int line, char *title, unsigned interval,  

      long (* valFunc)(void),long lowWater,long highWater )

      : StatsThread( line, title, interval, valFunc, lowWater, highWater ){};



   LoggingStatsThread(int line, char *title, unsigned interval,

      long (* valFunc)(void), long (* lowWaterFunc)(void),

      long (* highWaterFunc)(void) )



      : StatsThread( line, title, interval, valFunc, lowWaterFunc,

         highWaterFunc ){};



   ~LoggingStatsThread(){};

   int LogStat( char *filename );

};

As you can see from the class definition, only the LogStat function needs to be expanded on. The other member functions are relegated to the base class.

Even though there is an increase in both the amount of source code and the size of the executable, the future reuse of the code can more than make up for those increases. Also the self-contained nature of the classes makes it easier to track the allocated resources so that they can be deallocated.

Source Files

The source files to the two NLMs covered in this article are available via CompuServe or the Internet.

CompuServe To download the source files from CompuServe, go to the Novell Developer Support forum (GO NDEVSUP). Look for XCPP1.EXE in the Publications library (17).

Internet The source files are available at the following location:

ftp://ftp.novell.com/pub/netwire/ndevsup/17/xcpp1.exe

* 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.

© Copyright Micro Focus or one of its affiliates