Extending OS/2 batch files - Part 2

From EDM2
Jump to: navigation, search
Part 1 Part 2

by Roger Orr

Introduction

In the first article on extending batch files I scratched the surface of the Rexx programming language. Although as I hope I showed Rexx is a very powerful language there are some limitations. These fall into two main classes: some things that Rexx cannot do and some things Rexx does too slowly.

In both cases one way to keep the overall benefits of the Rexx environment is to make use of a library written in another language - typically in C. IBM themselves ship a useful library of routines called RexxUtil which I will use as an example before moving on to describing how you might write a library yourself.

RexxUtil is shipped as a Dynamic Link Library (DLL) and it provides the programmer with some generally useful functions which help issue some system commands and with user input/output.

Using Rexx DLLs

In order to make use of a Rexx callable DLL you must first tell Rexx about the functions you wish to use. This is usually done by calling RxFuncAdd to define the command you want to call and the DLL and entry point it will use.

Many DLLs (and RexxUtil is no exception) provide one such function which automatically registers the other functions; which does make life simpler.

So for example to use the SysSleep function in RexxUtil you could issue the following call:

 call RxFuncAdd 'sleep', 'RexxUtil', 'SysSleep'

You can now go 'call sleep 60' to wait for 60 seconds. The RexxUtil library provides "SysLoadFuncs" to automatically load all RexxUtil functions. For example:

 call RxFuncAdd 'SysLoadFuncs', 'RexxUtil', 'SysLoadFuncs'
 call SysLoadFuncs

Note that in this case the name Rexx knows the function by will be the same as the internal module name of the function. Mind you, this is no bad a rule to follow anyway to avoid confusion.

I would encourage you to have a look at the online documentation for RexxUtil and see the range of functions offered. This greatly enhances the number of things you can do with Rexx under OS/2 just using the standard supplied library.

Example of simple use of RexxUtil

I would like a program which lists all files with "today's" date in or below a specified subdirectory. I decided for simplicity of coding to use the RexxUtil 'SysFileTree' function to return a list of all files and then to display only the files with the correct date. If no directory name is specified I will display all files on the C: drive.

newfile1.cmd
/* Use of RexxUtil to show today's files */ 

today = date('o')

call RxFuncAdd 'SysFileTree', 'RexxUtil', 'SysFileTree'

parse arg dir scrap
if dir =  then
    dir = 'C:'

call SysFileTree dir || '\*.*', 'file', 'SBT'

do i = 1 to file.0
   if substr( file.i, 1, 8 ) = today then do
       parse value file.i with date size attr filename
       say filename
   end
end

If you create newfile1.cmd and then run it you may see something like this:

C:>newfile1 c:\os2
 c:\OS2\OS2.INI
 c:\OS2\OS2.!!!
 c:\OS2\OS2SYS.INI
 c:\OS2\OS2SYS.!!!

C:>

I hope the logic of the program is fairly obvious. I use the 'date()' function to get today's date, and then the 'S', 'B' and 'T' flags on SysFileTree to get a list of all files at or below the chosen subdirectory with the date in the format YY/MM/DD/HH/MM.

This will fill in the stem variable 'file' with a list of all files found, and set file.0 to the number of files found. I can then iterate around the stem variable checking for a match on the first few characters of the date.

Problems with RexxUtil

Even such a simple example shows a few problems or potential problems.

The first is performance. Running newfile1 on a not very full disk took 8.5 seconds, a C equivalent program took 3 seconds. You have to decide how important performance is compared to other benefits such as flexibility and development time.

Another possible worry is error checking. Change the newfile1.cmd call to RxFuncAdd to:

call RxFuncAdd 'SysFileTree', 'RexxUtl', 'SysFileTree'

and run the program again.

   11 +++   Call SysFileTree dir || '\*.*', 'file', 'SBT';
REX0043: Error 43 running C:\rogero\articles\NEWFILE1.CMD, line 11: Routine
not found

You can see that the RxFuncAdd call doesn't error, but Rexx errors when the function added is called. This delayed binding can cause serious problems if the function being called is used in a rarely executed piece of code, or if the DLL is on a network drive you only have access to sometimes! This is one place where the use of SysLoadFuncs prevents these silly typing errors from creeping in.

The third problem is flexibility. Suppose I want to do something with SysFileTree which isn't in the list of available options? If the 'T' option to return file time wasn't compatible with the 'o' option in the date function I might have had to write some date conversion code in Rexx.

And finally I may want to do something which isn't in RexxUtil at all - as an example, I want to change newfile1.cmd so that it defaults to the root of the BOOT drive if no arguments are supplied.

In these last two cases you will want to write your own Rexx callable library.

Rolling your own library

Remember the KISS rule (Keep It Simple, Stupid!) - it is a good idea when writing in a mixture of languages to keep as much as possible to one or the other. So when writing a Rexx library, your goal is to either do most of the work in Rexx and the bare essentials in C, or vice versa.

These are two extremes in writing Rexx callable libraries could be called the "bite size" and the "component".

A "bite size" library provides a low level function which does something quite small - just enough to let the Rexx program carry on.

A "component" library is where you have a large and complicated set of functions and provide a Rexx interface to let you use the flexibility of Rexx as a 'glue' to build your application up. An example of this is the Rexx callable API to the IBM System Performance Monitoring tool SPM/2 - the underlying code does some pretty complicated stuff to return the information which can then be processed and displayed easily and flexibly using a relatively small amount of Rexx code.

Many real life examples will of course fall somewhere in between these two extremes.

I'll illustrate a "bite size" library by refining newfile1.cmd as described above to use the boot drive by default.

If you want to write a component library you may want to provide both C and Rexx APIs both to make it easier to test and to make the components callable from more languages. In this case the Rexx part is basically providing a Rexx to C translation layer. Make sure you understand the "bite size" library and then apply the same concepts to larger chunks of functionality.

Where possible I would recommend writing and testing the C API first and then add a Rexx API once the underlying functionality is solid. It is generally easier to debug C code when called from a C program than when called from a Rexx program, since the C debugger understand the C code in the DLL.

The example function

The OS/2 API provides a way to get the index of the OS/2 boot drive: DosQuerySysInfo() with the QSV_BOOT_DRIVE parameter. I decided the most useful Rexx callable function would wrap up a call to DosQuerySysInfo() with a numeric or string based interface.

So I want to provide a function to provide the following Rexx interface:

value = RxQuerySysInfo(parameter)

where the 'parameter' can be a number (e.g. 5 to match the QSV_BOOT_DRIVE value) or a string (e.g. "BOOT DRIVE").

This does involve a little more work than a function which only gets the boot drive, but results in a function which may be of use in a much wider range of Rexx scripts.

So I want to write a DLL "RxSysInf.dll" with an entry point RxQuerySysInfo. When the Rexx code issues the call to RxQuerySysInfo my C function will be called by the Rexx interpreter. The interpreter will pass in a number of parameters, the most important being the input arguments and the return string.

The input arguments will be provided as an array of RXSTRING items - an RXSTRING is a structure which contains a Rexx string (length plus data). There are two parameters, used in a very similar manner to the standard C main() parameters of argc and argv.

In this case I will expect one parameter, the string value of the function argument. In general Rexx functions can be called with a variable number of arguments and you may wish to provide for different behaviour based on how many arguments are specified - see for example the RexxUtil function "SysIni".

The return string is provided by the Rexx interpreter and provides room for a string of up to 256 bytes to be passed back to the caller. If the result string exceeds 256 characters then you must use DosAllocMem to allocate enough memory to store the output string.

Finally your function has a return code, which must be zero if the function was successful. If the function returns a non-zero value then Rexx will raise a "syntax" error and you will see output like:

 REX0040: Error 40 running C:\rogero\articles\NEWFILE2.CMD, line 9: Incorrect
 call to routine

As I discussed in the previous article you could write a syntax handler in Rexx to perform some other processing if this occurs.

Notes on the code

The OS/2 toolkit provides a header file RexxSAA.h which you include to define necessary things. One key point is that the function called by Rexx MUST have a prototype of "RexxFunctionHandler" - the safest way to ensure this is to put an explicit function prototype into your code. Warning - this header file includes OS2.H so make sure you define symbols like INCL_DOS before you include rexxsaa.h!

The makefile creates a DLL using the multithreaded flags since in general you have no control over how many threads the main program may have.

The function works very simply by first trying to treat the input argument as a number, and if it is not a valid numeric value then checks the string against names in a table one by one looking for a case independent match. You may prefer to implement a 'fuzzier' match algorithm to cater for abbreviations.

makefile
RxSysInf.dll : RxSysInf.obj RxSysInf.def
        icc /Gmd /Ge- $**

RxSysInf.obj : RxSysInf.c
        icc /c /Gmd /Ge- /Ss $*.c
RxSysInf.def
LIBRARY
DESCRIPTION 'Rexx callable SysInfo interface'
EXPORTS
RxQuerySysInfo
RxSysInf.c
#define INCL_DOS
#include <os2.h>

#define INCL_RXFUNC        /* Defines for Rexx external functions */
#include <rexxsaa.h>

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

// Declare the Rexx interface
RexxFunctionHandler RxQuerySysInfo;

// local structure to map QSV names to numbers
static const struct {
   PSZ pszName;
   long lValue;
   } parblk[] = {
   { "max path length", QSV_MAX_PATH_LENGTH },
   { "max text sessions", QSV_MAX_TEXT_SESSIONS },
   { "max pm sessions", QSV_MAX_PM_SESSIONS },
   { "max vdm sessions", QSV_MAX_VDM_SESSIONS },
   { "boot drive", QSV_BOOT_DRIVE },
   { "dyn pri variation", QSV_DYN_PRI_VARIATION },
   { "max wait", QSV_MAX_WAIT },
   { "min slice", QSV_MIN_SLICE },
   { "max slice", QSV_MAX_SLICE },
   { "page size", QSV_PAGE_SIZE },
   { "version major", QSV_VERSION_MAJOR },
   { "version minor", QSV_VERSION_MINOR },
   { "version revision", QSV_VERSION_REVISION },
   { "ms count", QSV_MS_COUNT },
   { "totphymem", QSV_TOTPHYSMEM },
   { "totresmem", QSV_TOTRESMEM },
   { "totavailmem", QSV_TOTAVAILMEM },
   { "maxprmem", QSV_MAXPRMEM },
   { "maxshmem", QSV_MAXSHMEM },
   { "timer interval", QSV_TIMER_INTERVAL },
   { "max comp length", QSV_MAX_COMP_LENGTH },
   { "foreground fs session", QSV_FOREGROUND_FS_SESSION },
   { "foreground process", QSV_FOREGROUND_PROCESS },
   { 0, 0 } };

ULONG RxQuerySysInfo(
   PUCHAR Name,                   // Name of function being called
   ULONG argc,                    // Count of arguments
   PRXSTRING argv,                // Array of arguments
   PSZ QueueName,                 // RXQUEUE to be used
   PRXSTRING Retstr )             // Return string
{
   PSZ pszItem;
   long lValue;
   long lReturn = 0;

   /* Check one, valid, argument */
   if ( ( argc != 1 ) || !RXVALIDSTRING( argv[0] ) )
      return 1;

   pszItem = RXSTRPTR(argv[0]);
   lValue = atol( pszItem );
   if ( lValue == 0 )
   {
       // not a number so find by name
       int i;
       for ( i = 0; parblk[i].pszName != NULL; i++ )
       {
           if ( stricmp( parblk[i].pszName, pszItem ) == 0 )
           {
               lValue = parblk[i].lValue;
               break;
           }
       }
   }

   if ( lValue == 0 )
   {
       // bad input - return "no data"
       Retstr->strptr = 0;
       return 0;
   }

   if ( DosQuerySysInfo( lValue, lValue, &lReturn, sizeof lReturn ) != 0 )
   {
       // an error occurred...
       return 1;
   }

   // Fill in the return string
   Retstr->strlength = sprintf( Retstr->strptr, "%li", lReturn );

   return 0;
}

Using the RxSysInf library

Once I've written this DLL (and placed it somewhere on the LIBPATH) I can change newfile1.cmd to use it:

newfile2.cmd
/* Use of RexxUtil and RxSysInf to show today's files */

today = date('o')

call RxFuncAdd 'SysFileTree', 'RexxUtil', 'SysFileTree'

parse arg dir scrap
if dir =  then do
   call RxFuncAdd 'RxQuerySysInfo', 'RxSysInf', 'RxQuerySysInfo'
   bootdrive = RxQuerySysInfo( "Boot drive" )
   dir = d2c( bootdrive + c2d('A') - 1 )
   say dir
   end

call SysFileTree dir || '\*.*', 'file', 'SBT'

do i = 1 to file.0
   if substr( file.i, 1, 8 ) = today then do
       parse value file.i with date size attr filename
       say filename
   end
end

and I now can make use of the same DLL to display the amount of memory in the computer:

showmem.cmd
/* Use of RxSysInf to show installed memory  */

call RxFuncAdd 'RxQuerySysInfo', 'RxSysInf', 'RxQuerySysInfo'
say "Installed memory: " ,
   format( RxQuerySysInfo( "totphymem" ) / 1024 / 1024 ,, 1 ),
   "Mb"

Extensions

This example function is very simple, it only accepts one argument and returns one value. If we wanted to return something more like the list of files that SysFileTree returns, for example, then our function would have to make use of the Rexx variable interface. This is a mechanism which allows C functions to access the Rexx variables, and to both set and query their values.

We could treat one input argument as the name of a stem variable, which we could then initialise and fill in with the list of items to be returned.

In this case the return string could either be ignored (roughly equivalent to a 'void' C function) or used to return a copy of the count of items or some other relevant information.

The mechanism can also be used in the reverse direction too - you can write Rexx code and call it from C by invoking the interpreter. This can be used to implement a macro language for your program without needing to do the whole job of implementing a macro interpreter!

Conclusion

Rexx is a powerful and practical language for addressing a fairly wide range of programming tasks. The way with which it can be integrated with DLLs written in C makes it very easy to extend the Rexx language to include features not supported by the language itself.

With a little bit of thought at design time you can quickly build up a library of generic Rexx callable functions which can be used to enrich your OS/2 Rexx programming and enable programs to do a wide range of actions.

The IBM Devcon program, for example, comes with a Rexx interface to NetBios to allow a Rexx program to handle network requests.

I hope this article helps you to see where a problem which is beyond Rexx's own native capabilities can be solved by the combination of Rexx and a simple piece of C code.

Roger Orr 12 Dec 1996