Rexx: The King is Alive and Well
There was a period of time in the later 1990's when one could question whether Rexx would be around much longer. IBM released a white paper stating that its strategic direction on scripting languages was changing to BASIC. IBM released its own version of Visual BASIC for OS/2 and Windows and bundled Visual Basic with release 5 of DB2. But the pretender to the throne died a sudden and rapid death. IBM's version of Visual Basic quickly disappeared and Rexx lives on.
Rexx not only lives on but it gets stronger and stronger. While IBM was proclaiming the imminent death of Rexx, others were busy porting Rexx to different operating systems and hardware platforms. Today Rexx runs on a wide variety of platforms, from large mainframes to tiny PDAs. On OS/2, Windows, Linux and AIX a whole new object oriented version of Rexx was developed and released by, strangely enough, IBM. Why is Rexx becoming more and more popular? I think it is because Rexx has a number of outstanding features that make it an ideal scripting language.
Rexx was designed to be more than just a programming language, it was designed to be scripting language. A scripting language is a language used by an application to provide a way to automate features or parts of the application, sometimes also called a macro language. The features of the Rexx language (no variable "types", easy to learn and use, etc.), along with the capabilities for extending the language and integrating the language into an application, make Rexx an outstanding language application "macro" language. This article looks at Rexx from the scripting or macro perspective.
How the King is Executed
Before we can understand how Rexx can be used as a macro language we must first look at how Rexx programs are executed.
In OS/2 when you open a Command Prompt window the operating system starts a program called the command processor, or CMD.EXE. The program processes the commands you type in the window. When you type a name or command that is not a Command Processor command (such as DIR, TYPE, etc.), the Command Processor assumes the name is the name of a program. The Command Processor appends one of the program file extensions (.BAT, .CMD, .COM, .EXE) to the name and searches for a file with that name. If the extension of the file ends in *.CMD the Command Processor reads the first line of the program to determine the type of program it is. If the first two characters of the file contain the characters /*, which is the start of a Rexx comment, the file is considered to be a Rexx program. It the first two characters are not /* the file is considered to be a batch file. If the file is a Rexx program the Command Processor loads the Rexx interpreter and passes the name of the file, along with any arguments, to the Rexx interpreter.
The Rexx interpreter is a dynamically linked library, called REXX.DLL. Rexx programs are started by loading REXX.DLL into memory and calling the function RexxStart(). The RexxStart() function executes the Rexx program and returns when the Rexx program completes, or when it terminates because of an error.
When Rexx is used as the macro language for an application it is the application that loads the Rexx interpreter REXX.DLL and calls the RexxStart() function instead of the OS/2 Command Processor. Any program that can call C type functions from an OS/2 DLL can execute Rexx programs. However simply giving an application the ability to start a Rexx does not make Rexx a macro language. The Rexx program needs to have some way of interfacing with the application in order to act as a macro to the application. For example if Rexx were to be used as a macro language in a text editor the Rexx program would need the ability to delete text and insert text into the file being edited, to save the file being edited, to load new files, etc.
Rexx provides the following ways of interfacing a Rexx program with an application.
- External functions
- Subcommand handlers
- System exit handlers
- Variable pool interface
- SOM classes
External functions are functions written in some other language, usually C, that can be called from a Rexx program. Subcommand handlers are routines written in another language that are called by the Rexx interpreter when a non-Rexx command is encountered in a Rexx program. System exit handlers are routines written in another language that are called by the Rexx interpreter when certain Rexx commands are encountered in the program, or during certain program events, such as program start-up, termination, etc. The Rexx variable pool is where Rexx stores its variables; external functions, subcommand handlers and system exit routines can create, read and write Rexx variables through the variable pool interface. SOM classes are classes created by the application which Object Rexx can call through its SOM interface; they pertain only to Object Rexx type programs. We will look at each of these ways of "extending" Rexx in the sections below.
Rexx programs are started with the RexxStart() function in Rexx.DLL. The syntax is:
rc = RexxStart(ArgCount, ArgList, RexxProgramNameOnDisk, RexxProgramNameInMemory, subCommandName, programType, systemExits, ReturnCodeAsInteger, ReturnCodeAsString)
|ArgCount||The number of arguments to be passed to the Rexx program.|
|ArgList||The argument value(s) to be passed to the Rexx program.|
|RexxProgramNameOnDisk||The file name of the file on disk containing the Rexx program to run.|
Rexx programs can be run from memory, rather than read in from a disk file. The Rexx program can be located in the Rexx macrospace, or it can be stored in a "normal" memory buffer. In addition the Rexx program can be run from a tokenized image stored in a memory buffer, thereby avoiding the time it takes the interpreter to tokenize the program. This argument specifies the memory buffer for the literal program or tokenized program.
(The command processor stores the tokenized version of a Rexx program in the extended attribute REXX.TOKENSIMAGE of the file the first time the program is run. If the REXX.TOKENSIMAGE extended attribute exists, and it up to date, the command processor passes the tokenized image to RexxStart.)
|subcommandName||The name of the routine that handles subcommands. The default subcommand handler is CMD: the command processor.|
The type of program being run. Valid values are:
|systemExits||The name of the routine that handles system exits. If no system exits are to be used, then use NULL.|
|ReturnCodeAsInteger||The value returned by the Rexx program with the RETURN or EXIT instruction, converted to an integer value.|
|ReturnCodeAsString||The value returned by the Rexx program with the RETURN or EXIT instruction.|
The RexxStart() function does not return until the Rexx program finishes running.
External functions are functions written in some other language that can be called from Rexx just like "normal" Rexx functions. Usually the other language is C, but it can be Pascal or any language that can follow C function calling conventions. For example Sibyl, the Pascal development tool, has a Pascal interface to the Rexx API. (The Rexx API is a set of functions and header files that provide programs or applications with the ability to start Rexx programs, and build external functions, along with subcommand handlers and system exit routines.)
External functions are the most common method of extending Rexx. On OS/2, and on the Windows version of Object Rexx, Rexx comes with a set of external functions called RexxUtil that provide an interface to the operating system. In addition almost all the OS/2 system services are "exposed" to Rexx using external functions that a Rexx program can call.
External functions are "packaged" into a file, either a *.DLL or an *.EXE file. There can be one function in the file or many functions in the file. Each of the external functions must be "registered" to Rexx before they can be used. For example if there are 50 external functions in a single DLL, all 50 functions must be registered with Rexx before they can be used. Since it would be a burden to the Rexx programmer to have to register each and every function he wanted to use, most function packages include a function that registers all the other functions.
The registration tells Rexx what the name of the function will be called in Rexx, which file contains the function (*.DLL or *.EXE) and what the name of the function is in that file. The registration can take place in the external language using the Rexx API, or in the Rexx program itself using the built-in function RxFuncAdd(). The differences between registering the external function in the external language and registering it in Rexx are:
- The external language can register the external functions before the Rexx interpreter starts running a Rexx program. The RxFuncAdd() built-in Rexx function obviously cannot register any functions until the Rexx program starts, since it is called from Rexx.
- The built-in Rexx function RxFuncAdd() cannot register external functions that exist in *.EXE file; only functions that reside in DLL modules. The external language Rexx API must be used to register functions that reside in *.EXE files. The reason for this is because external functions in an *.EXE file can be unloaded from memory at any time, for example when the program contained in the executable file is terminated. For functions in DLL modules the module is loaded by the Rexx interpreter and managed by the interpreter, thus insuring that the function(s) stay in memory until the Rexx program using them finishes.
The Rexx API used by an external language to register a function looks like this.
RexxRegisterFunctionDll(functionNameinRexx, moduleNameThatContainsFunction, nameOfFunctionInModule);
RexxRegisterFunctionExe(functionNameinRexx, moduleNameThatContainsFunction, nameOfFunctionInModule);
An example is:
rc = RexxRegisterFunctionDll("wdPluginLoad","WDPLUGIN","wdPluginLoad");
which registers a function called wdPluginLoad from the module WDPLUGIN.DLL and names the function in Rexx wdPluginLoad()
The built-in Rexx function RxFuncAdd() looks like this
rxFuncAdd(functionNameinRexx, moduleNameThatContainsFunction, nameOfFunctionInModule);
An example is:
call rxFuncAdd "wdPluginLoad","WDPLUGIN","wdPluginLoad"
which does the same thing: it registers a function called wdPluginLoad from the module WDPLUGIN.DLL and calls the function in Rexx wdPluginLoad()
Normally the name the function is called in Rexx and the name of the function in the external function file (the *.DLL or *.EXE) are the same. But there are times when the name in Rexx will need to be different than actual name of the function in the external function file. For example DB2 and Oracle both supply external Rexx functions for interfacing with their databases. Unfortunately both companies use the same names for some of their functions, for example both have a function named SQLEXEC(). If the ability did not exist adjust the name the function is called in Rexx it would be impossible for a single Rexx program to interface to DB2 and Oracle at the same time. Since the names of the functions as known in Rexx can be changed, the DB2 function could be registered to Rexx as SQLEXEC_DB2() and the Oracle function could be registered as SQLEXEC_ORA() and both could be used in the same Rexx program.
The example below registers the function wdPluginLoad from the module WDPLUGIN.DLL as the function heyYou in Rexx.
call rxFuncAdd "heyYou","WDPLUGIN","wdPluginLoad"
According to the Rexx API documentation, external functions registered with RexxRegisterFunctionDll are available to any Rexx program after they are registered, while functions registered with RexxRegisterFunctionExe are only available to Rexx programs invoked from the same process that registered the function. Applications using Rexx as a macro language, and that use external functions as an interface between the application and Rexx, should code the functions in the application module (either in the EXE or in a DLL loaded by the application) and should use RexxRegisterFunctionExe to register those functions before the application starts the Rexx program/macro.
The example below shows Rexx functions being registered, the Rexx program being executed, and the functions being deregistered.
// register the Rexx functions we are providing to the Rexx program RexxRegisterFunctionExe("WDPARSEARGS", (PFN) &wdParseArgs ); RexxRegisterFunctionExe("WDEXPOSEARGS", (PFN) &wdExposeArgs ); RexxRegisterFunctionExe("WDCOMMAND", (PFN) &wdCommand ); RexxRegisterFunctionExe("WDOPENPIPE", (PFN) &wdOpenPipe ); RexxRegisterFunctionExe("WDREADPIPE", (PFN) &wdReadPipe ); RexxRegisterFunctionExe("WDCLOSEPIPE", (PFN) &wdClosePipe ); // start the Rexx program rc = RexxStart(1,rxarg, // number and array of arguments rexxProcName, // name of rexx file NULL, // instore stuff - not used here "CMD", // subcommand handler - default CMD RXCOMMAND, // run as command file exitArray, // exit arrray - Exits for this macro &rexxrc, &rexxretval); // deregister the functions RexxDeregisterFunction("WDPARSEARGS"); RexxDeregisterFunction("WDEXPOSEARGS"); RexxDeregisterFunction("WDCOMMAND"); RexxDeregisterFunction("WDOPENPIPE"); RexxDeregisterFunction("WDREADPIPE"); RexxDeregisterFunction("WDCLOSEPIPE");
Roger Orr's most excellent articles on Rexx (and other stuff) have example programs that show how to write external functions, and subcommand handlers and system exit routines. They can be found at http://www.howzatt.demon.co.uk.
A Subcommand handler is a routine written in another language that is called by the Rexx interpreter when Rexx encounters a command that is not a normal Rexx command. The language is designed so that any command not recognized by the Rexx interpreter is passed to a (sub)command handler. The default subcommand handler is CMD, which passes the commands to the OS/2 Command Processor. This is why you can write lines like
/* Rexx program */ say 'Directory listing' 'dir *.*'
and have them work. The command dir *.* is not a valid Rexx command so the Rexx interpreter passes off dir *.* to CMD.EXE, which is the default subcommand handler. CMD.EXE executes the dir *.* command on behalf of Rexx and returns the results to Rexx.
Subcommand handlers, like external functions, can reside in their own DLL, or can be coded into an application, either in the EXE or a DLL loaded by the application. A subcommand handler must also be registered before it can be used, just like an external function. And just like external functions, the subcommand handler can be registered from inside the Rexx program, using the RxSubCom Rexx instruction, or from an external program using the RexxRegisterSubcomExe() or RexxRegisterSubcomDll() functions.
The syntax for registering a subcommand handler from inside of Rexx is
RXSUBCOM REGISTER nameOfHandlerInRexx nameOfDll nameOfRoutineInDLL
The syntax for registering a subcommand handler from an external language is
RexxRegisterSubcomDll(nameOfHandlerInRexx, nameOfDll, nameOfRoutineInDLL, userArea, whoCanDropHandler)
The userArea is 8 bytes of unformatted memory that can used to store information the subcommand handler can use to determine which Rexx program is calling it. The whoCanDropHandler specifies if only the thread the registered the subcommand handler can deregister it, or if just anyone can deregister it.
Once a subcommand handler has been registered it can be used by a Rexx program by specifying the subcommand handler's name in the Rexx ADDRESS instruction. Multiple subcommand handlers can be registered at the same time, and the Rexx program can switch between then while running with the ADDRESS instruction.
Every Rexx program has a subcommand handler assigned, either the default handler or one that has been specified with the ADDRESS instruction. When an application starts a Rexx program with the RexxStart() function it can specify a subcommand handler to be the default subcommand handler, in the [#subcom subcommandName] argument of RexxStart(). The subcommand handler must be registered before it can be specified as a RexxStart() argument.
Subcommand handlers are the most common way of providing an interface between a Rexx program running as a macro and the application. But external functions can also be used to provide that interface. Often times the choice between which of the two to use comes down a preference for the programming interface that will be used in Rexx. The differences between a subcommand handler and external functions are:
- A single subcommand handler handles all non-Rexx commands from the Rexx program. The subcommand handler uses a switch statement or IF - THEN statements to handle each command passed to it. With external functions each function is a different routine; each function must be registered and called by name.
- Subcommands always return a value in the special Rexx variable RC, if they return a variable. External functions, if they return a value, return it as part of the return code from the function. The return code can be assigned to any Rexx variable by the Rexx programmer when he calls the function. For example, for a subcommand
'dothis' if rc = 1 then ... for an external function xx = doThis() if xx =1 then ...
- If a Rexx program wants to use more than one subcommand handler it can switch between handlers using the Rexx ADDRESS instruction. With external functions there is no "switching"; once the function is registered it can be used at any time in the Rexx program.
System Exit Handlers
System exit handlers are routines written in an external language that are called when certain events happen in the Rexx program. These events include:
- When the Rexx interpreter starts.
- When Rexx terminates.
- When a Rexx function is called.
- When a subcommand handler is called.
- When any action happens on a queue.
- When any I/O (input/output) happens: SAY, PULL, Trace.
- When Rexx is halted with a HALT condition.
The exit handler can handle the event itself, ignore the event and let the Rexx interpreter take its normal action, or terminate Rexx.
Exit handlers must be registered before they can be used. They must be registered by the application that will invoke the Rexx program before starting the Rexx program. The exit handler(s) are specified in the [#systemExits systemExits] argument of the RexxStart() function.
System exit handlers can be used to modify the way Rexx programs execute, or to control what a Rexx program is allowed to do. For example:
- A start-up handler can initialize variables in the Rexx variable pool, which provides an alternate way of passing information into a Rexx program.
- A termination handler can query the Rexx variable pool after the Rexx program terminates to determine values in the variables. This provides an alternate way of returning values from the Rexx program to the calling application.
- The subcommand exit handler can prevent any subcommand handler other than an "approved" one from being used (i.e. prevent the ADDRESS instruction from changing subcommand handlers), or else it can just route all subcommands to the appropriate subcommand handler regardless of the ADDRESS setting.
- Rexx I/O can be rerouted, or prevented. For instance the PULL command can be intercepted by an application and the application can display its own dialog box for input, returning the value entered in the dialog box to the Rexx program as if it came from the "normal" PULL instruction. Likewise output from a Rexx SAY instruction could be rerouted to a window or control in the application.
- The application can get notification of when the Rexx program is halted, or it can cause a HALT condition, with the HALT exit handler.
Variable Pool Interface
All three methods of extending Rexx - external functions, subcommand handlers, and system exit handlers - have an additional way of interfacing with Rexx. They can modify the Rexx variable pool. This means that the external functions can add new Rexx variables, delete or DROP existing Rexx variables, and change the value of existing Rexx variables. This gives applications using Rexx as a macro language a way of putting data into variables that Rexx can access, or reading Rexx variables that are set by the Rexx program.
The application interfacing with the Rexx variable pool can address variables one of two ways:
- With the literal name of the variable.
- With a variable name (compound variable) that is resolved by the Rexx interpreter to the actual variable name, just like in Rexx itself.
Creating Rexx variables and putting values into Rexx variables is a powerful way of interfacing between an application and Rexx.
All the method described above for integrating Rexx with an application - external functions, subcommand and system exit handlers, and the Rexx variable pool - are applicable to both classic Rexx and Object Rexx. However using Object Rexx as the macro language opens up possibilities for scripting applications with Rexx that area not available with classic Rexx.
Object Rexx is an IBM created object oriented version of Rexx. It first appeared in Warp v 4, and was later ported to work with Warp v 3. Object Rexx adds a very complete object orientation to the familiar procedural Rexx that we all know, which is now called "classic Rexx". The beauty of Object Rexx is that the way it is implemented allows you to program either in the old, procedural way of classic Rexx, or in the new, object oriented way provided by Object Rexx. Even without using any objects at all, Object Rexx is a better Rexx than classic Rexx. It provides additional built-in functions that are available when writing procedural type Rexx programs, and it checks the syntax of the entire Rexx program before it starts running, so you know that all the branches of the program will not fail because of a syntax error.
While Object Rexx is available for OS/2, Windows, Linux, and AIX, the OS/2 version has something the other versions don't: the ability to use SOM objects. With the OS/2 version of Object Rexx SOM object can be imported into a Rexx program so that they become full Object Rexx objects, just like ones created in Object Rexx. This opens up a whole new way of designing and structuring an application for scripting.
Rexx programs with a way of using SOM objects. SOM objects can be imported into an Object Rexx program so that they appear to the Rexx program as "native" objects. This opens up a whole new realm of scripting possibilities, called object modelling.
With object modelling, you analyze your application in terms of objects. Suppose your application is a server that uses TCP/IP sockets. One way to look at the communications is that each socket is an instance of a socket class. You could create a SOM class for sockets and SOM methods for the functions related to sockets, and then use those methods to interact with the socket objects.
One advantage is that SOM can keep state data for a particular instance of an object. With external functions, the function code typically wouldn't keep state data. The person writing the REXX procedure would have to maintain any state data across calls. Because the state data is maintained with the object itself, the complexity of scripting procedures is reduced.
Once the objects and methods are stored in SOM, they can be used from Object REXX. The beauty of this setup is that people who are scripting your application work with it in the same way that you do: by sending messages to objects. You don't have to provide a translation layer, i.e. write the interface between Rexx and the application. Moreover, the new direct-to-SOM compilers (IBM VAC C++ v 3) greatly improve the usability of SOM. If your code is written in C++, you no longer need to build the classes separately with SOM's Interface Definition language (IDL).
This quote comes from http://www-3.ibm.com/software/ad/obj-rexx/baryla7.html.
With object modelling you are still providing an interface between Object Rexx and the application, but that interface is through SOM objects rather than the more traditional methods described in the section above. The advantage of providing an interface through SOM objects is that any language that can interface to SOM can be used as the scripting language, which on OS/2 means C/C++, Java, and of course Object Rexx. The next step up from object modeling is to actually factor the application into objects, meaning that the SOM objects aren't the interface between the application and the Rexx, but the application itself. In other words the application is built from SOM objects. The user interface part of the application is separated from the functional part. The function part is built using SOM, and the both the user interface and the scripting languages interface to the functional part of the application the same way: by sending messages to the SOM objects, i.e. by calling methods of the objects.
Rexx is not just as easy to use, easy to learn programming language that comes with OS/2. It is also a very powerful scripting language designed to be incorporated into applications. The interface, the Rexx API, which makes that interfacing possible, is a very well documented and very well supported API. Almost all Rexx interpreters on all platforms recognize the same Rexx API that is supported on OS/2 - known as the SAA Rexx API. Meaning that an application using Rexx as its scripting language can be developed on OS/2 and ported to other platforms and still keep the Rexx scripting feature.
The King is certainly not dead; in fact he becomes stronger and better looking all the time.