Using REXX as a macro language

From EDM2
Revision as of 18:58, 10 February 2020 by Ak120 (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

By Roger Orr

Introduction

My last technical tip (Pointers Sep/Oct 94) looked at calling C code from REXX; and this time I thought I would look at the reverse problem - that of calling REXX code from C.

This provides a relatively simple way to use REXX as a macro language, or alternatively to write a test harness for your own REXX scripts.

For the purposes of this article imagine we wish to run a macro language inside a program. For simplicity we only want to provide a few simple commands: PAUSE, BEEP and TESTFAIL. However for security we wish to prevent the user passing instructions on to the command processor.

Obviously to implement a complete macro language we would need more than this, possibly also making use of common variables between C and REXX; but this example illustrates the technique.

One example, which may be familiar to some of you already, of a rather fuller implementation of a Rexx-based macro language is given by the Enhanced Editor (EPM) which uses .ERX files to provide extendability for the editor. If nothing else this article may be of interest if you are the sort of person who likes to know how something works!

Overview

The core of the program is the call to RexxStart, which is the call to invoke the interpreter. This call takes a number of input parameters:

a) a count, and array, of arguments to the Rexx program. These arguments are stored as RXSTRINGs - basically an RXSTRING is a structure with a length and a string address. RexxSaa.h contains some macros to manipulate them. These input arguments are then obtained inside the Rexx program using, for example, 'Parse Arg'.
b) the name of the file containing the Rexx program. A more advanced method is to pass the program in memory using the next parameter, which contains the address of some in memory structures.
c) We must pass the interpreter the name of the command environment. This defines the external command handler for the Rexx interpreter - i.e. any command not recognised by Rexx will be handled by this environment. For example, when the OS/2 command shell invokes the Rexx interpreter any commands not recognised by Rexx (e.g. 'del *.*') will be handled by the command shell itself. This command environment must be registered with Rexx before calling the RexxStart API.
d) The Rexx interpreter can run in several modes: as a command, function or subroutine. This example calls using the RXSUBROUTINE method.
e) The last input parameter is a list of the 'exits' for this call. A Rexx 'exit' is a procedure registered with Rexx which allows you further control over the behaviour of the Rexx program. Rexx supplies a set of places where additional functionality can be added - for example when the 'SAY' command is executed, when an external function is called or when an external command is executed. You may specify an 'exit' for as many or as few of these as you wish - and may use the same exit procedure for each or a unique one for each exit. However, like the command environment, each exit has to be registered with Rexx before calling RexxStart.

The RexxStart API also has two output parameters (or three counting the return code). They are an integer return code and a string return value, and are described in a bit more detail in the next section.

So the sample program starts by registering the sub command environment "subcom" with Rexx and then, just for fun, we also register a system exit "SysExit" for the RXCMD exit.

For this example the command handler looks for the commands "PAUSE", "BEEP" or "TESTFAIL". For a real example more commands (probably with parameters) would probably be supported.

The exit we have specified, RXCMD, is called when Rexx passes a command to an external command handler and we simply check that these commands are still being addressed to our own environment, "SubCom". This prevents a Rexx program using the 'ADDRESS' command and then running other programs, and provides a level of security if you need it.

Once the appropriate things are registered we read lines one by one and pass each as an argument to the macro file using the Rexx interpreter via RexxStart(). This allows a simple interactive play with the macro file specified when the program was started, by default "subcom.tst". More generally you might pass a complete macro script to the Rexx interpreter.

Please don't get confused - I am trying to demonstrate both passing parameters and programs to the interpreter. The CallRexx program uses the same Rexx program each time it calls RexxStart and just varies the arguments. I suspect that the arguments will be irrelevant in many cases and the main parameter will be the Rexx program to invoke.

Notes on the code

The function prototypes (e.g. "RexxSubcomHandler SubCommand;") are required to get the correct parameter passing convention. Unfortunately you have to cast the function pointer when registering it to avoid warnings since the API was defined using the 'PFN' typedef which is slightly different. If you miss out the function prototype the compiler will not generate the right code to find the parameters passed to you by Rexx.

RexxStart() returns three things:

(a) a return code from the function - 0 if the interpreter was happy
(b) a return number from the interpreter - from the 'return' statement if it is numeric, otherwise 0.
(c) a RXSTRING containing the string returned from the interpreted program. You must free the returned memory using DosFreeMem() (unless you allocated memory initially - this example doesn't).

Once an external command is found in the macro program the subcommand handler will be called.

In our example we registered an exit for 'RXCMD' so we will first get a call to 'SysExit'. If we return RXEXIT_NOT_HANDLED then the interpreter carries on normally and passes the command string to the subcommand handler 'SubCommand'.

It is up to you how you process the input command string. This simple example merely uses stricmp()! Note that it is a Rexx string and not a C string so the length is technically given by the strlength of the input not the position of the first NUL character. Usually these are the same for simple cases, but you shouldn't actually rely on it.

A little like RexxStart() the subcommand handler can return up to three things:

(a) a numeric return code for the Rexx 'RC' variable
(b) a failure or error value - which will raise the appropriate Rexx condition
(c) a string which will be put into the Rexx 'RC' variable if the return code is zero. Rexx is quite decent and gives you enough string space for about 256 characters - you will need to allocate more memory if you wish to return really long answers.

So for example, the PAUSE command sleeps about 5 seconds, and then sets the return string to NULL and return value to 0 which sets the Rexx return value 'RC' to '0'.

The 'TESTFAIL' command demonstrates an external command failure by setting the return code to 1 and the error flag to RXSUBCOM_FAILURE, which raises the 'failure' condition in the Rexx interpreter.

Any unknown command sets the return string to 'UNKNOWN COMMAND' and the error flag to RXSUBCOM_ERROR, which raises the Rexx 'error' condition.

Examples

1 >callrexx
2 Enter commands, Ctrl-Z to exit
3 say hello
4 HELLO
5 Contains 2 words
6 TestFail
7     13 *-*     TestFail;
8        +++       RC(1)
9  FAILURE: occurred in TESTFAIL
10 Contains 1 words
11 Ctrl+Z

1. We invoke CallRexx using the default program subcom.tst

2. CallRexx prompts for input to be passed as arguments to the program

3. Enter the line 'say hello'

4. The input argument is interpreted in subcom.tst and results in 'HELLO'

5. The subcom.tst program counts the words, and returns this string which is printed by CallRexx.

6. We enter the line 'TESTFAIL', which is passed to our sub command handler, which returns 1 and sets RXSUBCOM_FAILURE

7 & 8. The interpreter prints the line number of the "interpret Data" line in subcom.tst (where failure was raised) and the value of RC.

9. In subcom.tst we have made use of a handler for the failure condition which prints out the condition (FAILURE) and the description ("TESTFAIL")

10. Once again subcom.tst returns a count of words in the input string.

11. Enter Ctrl+Z to terminate CallRexx.

1 callrexx palin.tst <palin.dat
2 Enter commands, Ctrl-Z to exit
3 Input Data is palidromic
4 *** able was i ere i saw elba ***
5 Input Data is palidromic
6 *** bob ***
7 this is not

1. We invoke CallRexx using the palin.tst program with input from palin.dat

2. CallRexx prompts for input, reads lines from palin.dat and passes to the macro program.

3. palin.tst prints out 'Input data is palindromic', and then calls the external commands "beep" and "pause". It then returns the string enclosed in "***".

4. CallRexx prints the return string.

5. & 6. like lines 3. & 4.

7. palin.tst merely returns the parsed input string.

Conclusion

The Rexx interpreter is a powerful method for implementing a macro language which allows programs to be written in a standard language (i.e. Rexx) but make use of the specific features of your application. I hope the trivial nature of the commands supported doesn't mask the power of this approach!

It is relatively simple to add a basic macro facility to your application, but you, as you wish, can add more and more control over the environment in which the macro program executes.

CALLREXX.C
/*
   RexxDemo     - demonstrate using Rexx as a macro language
   Compile as: icc /wall+ppt- CallRexx.c REXX.LIB
*/

#define INCL_DOSPROCESS
#include <os2.h>
#define INCL_REXXSAA
#include <rexxsaa.h>
#include <stdio.h>
#include <string.h>

/*****************************************************************************/
/* Local function prototypes                                                 */
/*****************************************************************************/

RexxSubcomHandler SubCommand;     /* Sub command handler for macro           */
RexxExitHandler SysExit;          /* Exit handler for macro                  */

/*****************************************************************************/
/* M A I N   P R O G R A M                                                   */
/*****************************************************************************/

int main( int argc, char **argv )
    {
   RXSTRING arg = {0};                 /* argument string for REXX          */
   RXSTRING rexxretval = {0};          /* return value from REXX            */
   APIRET   rc = 0;                    /* return code from API calls */
   LONG     lrc = 0;                   /* return code from REXX     */
   SHORT    rexxrc = 0;                /* return code from function */
   RXSYSEXIT ExitArray[] = {
      { "SysExit", RXCMD },
      { NULL, RXENDLST } };
   char buff[ 256 ] = "";
   PSZ pszRexx = "Subcom.tst";

   if ( argc > 1 )
      pszRexx = argv[ 1 ];

   /* Register sub command environment (from this .EXE) */
   rc = RexxRegisterSubcomExe( "SubCom", (PFN)SubCommand, NULL );
   if ( rc != 0 )
      printf( "RexxRegisterSubcomExe error - rc: %u\n", rc );

   /* Register exit environment (from this .EXE) */
   rc = RexxRegisterExitExe( "SysExit", (PFN)SysExit, NULL );
   if ( rc != 0 )
      printf( "RexxRegisterExitExe error - rc: %u\n", rc );

   printf( "Enter commands, Ctrl-Z to exit\n" );
   while ( gets( buff ) != NULL )
      {
      /* create input argument and call the interpreter */
      MAKERXSTRING( arg, buff, strlen( buff ) );

      /* By setting the strlength of the output RXSTRING to zero, we   */
      /* force the interpreter to allocate memory and return it to us. */
      rexxretval.strlength = 0;

      lrc = RexxStart( 1, &arg,          /* number, and array, of args   */
                       pszRexx,                 /* name of REXX file     */
                       NULL,                    /* No INSTORE used       */
                       "SubCom",                /* Command env. name     */
                       RXSUBROUTINE,            /* Code for how invoked  */
                       ExitArray,               /* EXITs for this call   */
                       &rexxrc, &rexxretval );  /* Rexx program output   */

      if ( lrc != 0 )
         printf( "Interpreter Return Code: %d\n", (int) lrc );
      if ( rexxrc != 0 )
         printf( "Function Return Code:    %d\n", (int) rexxrc );
      if ( rexxretval.strlength )
         printf( "%s\n", rexxretval.strptr );
      DosFreeMem( rexxretval.strptr );      /* Free memory returned by REXX */
      }

   /* Tidy up before returning */
   RexxDeregisterExit( "SysExit", NULL );
   RexxDeregisterSubcom( "SubCom", NULL );

   return 0;
   }
/*****************************************************************************/
/* Subcommand handler.  This simple example handles only:                    */
/*    PAUSE - sleep for a short time                                         */
/*    BEEP  - make a single beep                                             */
/*    TESTFAIL  - raise a failure                                            */
/*****************************************************************************/

ULONG SubCommand(
   PRXSTRING pCmdstr,                  /* command string (input)  */
   PUSHORT pErrFlag,                   /* ERROR/FAILURE  (output) */
   PRXSTRING pRetstr )                 /* string retcode (output) */
   {
  if ( stricmp( pCmdstr->strptr, "PAUSE" ) == 0 )
     DosSleep( 5000 );
  else if ( stricmp( pCmdstr->strptr, "BEEP" ) == 0 )
     DosBeep( 250, 250 );
  else if ( stricmp( pCmdstr->strptr, "TESTFAIL" ) == 0 )
     {
     *pErrFlag = RXSUBCOM_FAILURE;/* Raise FAILURE in REXX */
     /* use integer 'rc' as REXX RC  */
     return 1;
     }
  else
     {
     *pErrFlag = RXSUBCOM_ERROR;   /* Raise ERROR in REXX program */
     strcpy( pRetstr->strptr, "UNKNOWN COMMAND" );
     pRetstr->strlength = strlen( pRetstr->strptr );
     return 0;                /* let the string be the RC */
     }

  /* Return with the RXSTRING set to all zeros, and the        */
  /* return value set to 0; which makes the REXX RC value "0". */
  *pErrFlag = RXSUBCOM_OK;
  MAKERXSTRING( *pRetstr, NULL, 0 );
  return 0;
  }
/*****************************************************************************/
/* SysExit: Exit handler.  Only checks for RXCMD, subfunc RXCMDHST to ensure */
/* commands are being addressed to the right environment.                    */
/*****************************************************************************/ 

LONG SysExit(
  LONG Func,
  LONG SubFunc,
  PEXIT pExit )
  {
  RXCMDHST_PARM *pHost = (RXCMDHST_PARM *)pExit;
  LONG rc = RXEXIT_NOT_HANDLED;  /* default: usual processing */

  if ( ( Func = RXCMD ) && ( SubFunc == RXCMDHST ) &&
       ( stricmp( pHost->rxcmd_address, "SubCom" ) != 0 ) )
     {
     /* Attempt to address commands to another subcommand interface */
     printf( "*** %s not available ***\n", pHost->rxcmd_address );
     pHost->rxcmd_flags.rxfcerr = 1;
     rc = RXEXIT_HANDLED;
     }

  return rc;
  }
SUBCOM.TST
/* SUBCOM.TST - Test out the Rexx subcommand environment             */

Parse Arg Data                         /* get argument string        */

call on error                           /* For diagnostic info */
call on failure name error

interpret Data

count = 0               /* Count words in 'Data' */
do while Data \= 
  count = count + 1
  parse value Data with symbol Data
end

return "Contains" count "words"

error:
  say condition( Condition )': occurred in' condition( Description )
  return
PALIN.TST
/* simple REXX macro script to check for palindromic lines */

parse arg Forward Rest

Backward = reverse( Forward )
do while Rest \=               /* Read each word into Forward and Backward */
  parse value Rest with word Rest
  Forward = Forward word
  Backward = reverse(word) Backward
end

if Forward \=  & Backward = Forward then do
  say "Input Data is palidromic"
  beep
  pause
  Forward = "***" Forward "***"
end

return Forward
PALIN.DAT
able was i ere i saw elba
bob
this is not

Roger Orr 29 Dec 94