Writing REXX Functions

The REXX Language can be extended with new commands/functions which are written in some lower level language such as C. For example, you can develop some function written in C that takes a text string and displays it in a "pop-up window" with a graphical "OK" button. The REXX script can then call this function which is written in C (just as if the script were to call another REXX built-in function such as STRIP()), passing the string to be displayed.

If you've read the tutorial "Using REXX for scripting", then you're already familiar with almost the exact same thing that we're going to do here. That tutorial discussed Adding your own functions to REXX. We are going to use almost the same techniques here, but with two important differences -- we're going to put our code entirely into a dynamic link library (DLL) instead of inside of some executable program we write, and also, we're not going to use RexxRegisterFunctionExe() to register the functions (nor RexxDeregisterFunction() to deregister them). By putting our functions into a DLL, this means that any and all REXX scripts can call our functions at any time, simultaneously, regardless of which executable launches the REXX script. (When we put our functions into our own executable, then only REXX scripts that we ourselves launch with a call to RexxStart() can use our custom, REXX-callable functions).

If you've already read the tutorial "Using REXX for scripting", you can skip down to the discussion about Making our Dynamic Link Library. Otherwise, what follows will mostly be a review of what you already learned in the preceding tutorial.


Table of Contents

Setting up for development
A REXX string -- RXSTRING
Writing a REXX-callable function
Args passed to your function (by the REXX script)
Returning a REXX string to the REXX script
Return values (and errors)
Making our Dynamic Link Library
How the REXX script registers your functions
Helping the REXX script register your functions
Supporting Reginald's auto-load feature
User interface guidelines
Setting/Querying REXX variables in the script
Halting execution of a script/Raising conditions
Supporting various interpreters

Setting up for development

Before you start writing your own REXX-callable functions, you need a couple things for development:

  1. You need a REXX interpreter installed upon your system. This runs any REXX script. A good choice is the Reginald REXX interpreter available from the REXX Users Page. It's absolutely free, and comes in a package that is easily installed/uninstalled (and will even install your DLL for you). A user of your software will also have to install such a REXX interpreter upon his system, and Reginald is an easy one to use. (Plus, you can freely distribute Reginald with your DLL, so that he doesn't have to hunt around for one himself). Most of the examples in this tutorial will use Reginald for reference.

  2. You need to download a "header file" that declares the functions in the REXX interpreter that your C code may call. Your source code references this file so that your compiler knows the names of those functions, and how to call them. Also, any particular data structures you use with the interpreter are defined in this file. So far, such a header file exists only for C or C++ and it is named REXXSAA.H. You should find it in the developer materials for your REXX interpreter. You copy this file to where your C compiler keeps its "include" files. (If you can translate this file for the compiler of another language, then you're all set to go with that other language. I'd welcome hearing from developers who do create such a file for other languages). Then in your C source, put the following line toward the top of your source:
    #include <rexxsaa.h>
    

  3. If you're statically linking to the interpreter (discussed later), you may need the interpreter's LIB file (depending upon which operating system you're using). This is fed to your linker (just like any object file) so that the linker also knows the names of those functions, and how to call them. Windows C development systems need this. Other languages/operating systems may or may not need such a file. You should find it in the developer materials for your REXX interpreter.

Reginald has a "Developer's Kit" that includes the header and LIB files for C programmers. It also contains the example C source code in this tutorial. The Windows version even includes Visual C++ 4.0 project files, so that you can quickly get up and running with the examples. You can download it from the Reginald Developer's Page. The examples in this tutorial will be in C (but that doesn't mean that the REXX interpreter is restricted to use by C programs), and some will be for MS Windows specifically.

Remarks that are operating system specific will be prefaced with the operating system name in red. For example, a note about support under MS Windows operating system will begin with Windows.


A REXX string -- RXSTRING

Before we proceed to writing our function, you should understand one thing about REXX. Everything in a REXX script is stored as a string -- even numeric values. For example, the number -100.4 would be stored in a data buffer that looked like this in C:

char buffer[] = {'-', '1', '0', '0', '.', '4'};

So any arguments that a REXX script passes to you, and any data you return to the REXX script must be in the form of a string. But, REXX does not (normally) use C-style strings. It doesn't nul-terminate its strings. In fact, a REXX string can contain characters of any value (not just ASCII characters, like in our example of a numeric string above). This includes bytes equal to 0. (ie, A REXX string can have embedded nul bytes).

So, to allow the REXX script to pass some REXX string to your function, the interpreter uses a data structure called an RXSTRING. This has two fields. It has a 'strptr' field which contains the address of the data buffer in which the data actually resides. (ie, It's a pointer to the REXX string). And it has a 'strlength' field which tells you how many characters (ie, 8-bit bytes) are in the REXX string. In C, it looks like this:

typedef struct RXSTRING_type 
{
   unsigned long strlength;
   char          *strptr;
} RXSTRING;

When you return a REXX string to the REXX script, you'll also use an RXSTRING. You'll copy your data into some buffer. Then, you'll put the address of that buffer into the RXSTRING's strptr field. You'll set the RXSTRING's strlength field to how many characters are in that buffer.


Writing a REXX-callable function

In this tutorial, we'll assume that you're writing a REXX-callable function in C, although other languages can be used as long as they support being passed arguments on the stack in C order.

When creating your C function, you do so just like you would do with any other C software. You write the source code in C, and then compile it into a dynamic link library (DLL) using a C compiler/linker. The REXX script can then "directly" call your C function within the DLL. REXX documentation refers to such a REXX-callable function (written in some language other than REXX) as an "external function".

But in order to allow the REXX script to be able to call your C function, you have to do more than just write any C function and compile it into a DLL. First of all, your C function must be written to accept a particular set of arguments (to be discussed below), and return a particular value. So there are some requirements as to how you must design your C function.

Your function must be able to accept 5 particular args, passed to it (by the REXX script) on the stack per C language calling convention. The 5 arguments are as follows:

name

A C-style (nul-terminated) string which is the name of your function. Mostly this is just for reference. It lets you know what name the REXX script gave to your function when it used RXFUNCADD() to "register" your function.

numargs

The number of args that the script is passing to your function.

args

An array of RXSTRING structs containing those args passed by the script.

queuename

A C-style (nul-terminated) string which is the name of the current queue. (We'll talk more about that later).

retstr

An RXSTRING struct which contains a 256 byte buffer into which you may return data to the script.

REXXSAA.H contains the declaration for your function. It must be of type APIRET APIENTRY. It must return the number RXFUNC_OK (ie, 0) to the REXX interpreter if everything goes well. (Otherwise, it may return some non-zero error number, usually RXERR_INCORRECT_CALL, ie, 40, which is a generic error. Other possible errors are listed in REXXSAA.H).

Here then is the skeleton of a C function in your program that a REXX script may call. We'll call it "TestFunction".

APIRET APIENTRY TestFunction(CONST CHAR *name, ULONG numargs, RXSTRING args[], CONST UCHAR *queuename, RXSTRING *retstr)
{
   return(RXFUNC_OK);
}

That value RXFUNC_OK is being returned to the interpreter, not the REXX script. (That's why RXFUNC_OK is not a REXX string). It's used to let the interpreter know that everything went well. To return data (ie, a REXX string) to the script, you're going to use the 'retstr' RXSTRING structure that was passed to you. We'll examine that a little later.


Args passed to your function (by the REXX script)

How does the REXX script call your function? It looks remarkably like a C call. Here's an example of a REXX script calling TestFunction() with 4 args, and assigning TestFunction()'s returned REXX string to a variable called 'MyReturn'.

MyReturn = TestFunction('Arg One', 12, MyVariable, "Arg 4")

I mentioned that each argument that the REXX script passes you is contained in an RXSTRING, and that usually the data buffer of an RXSTRING is not nul-terminated. Well, one nice thing that the interpreter does is make sure that each arg passed to your function is nul-terminated. (But that doesn't mean the data may not have some embedded nul bytes in it, so you have to be careful in your assumptions about data passed to your function). So you can treat it like a C-style nul-terminated string (assuming that it doesn't contain embedded nul bytes). But note that the RXSTRING's strlength field does not reflect this extra, terminating nul byte in its count.

Remember that REXX treats numbers as numeric strings. So, you see that second argument of 12 passed to TestFunction()? It is actually passed in a nul-terminated data buffer that looks like this in C:

char buffer[] = {'1', '2', 0};

Here then is a version of TestFunction() that prints out the args passed to it:

APIRET APIENTRY TestFunction(CONST CHAR *name, ULONG numargs, RXSTRING args[], CONST UCHAR *queuename, RXSTRING *retstr)
{
   ULONG i;

   for (i = 0; i < numargs; i++)
   {
      printf("Arg #%ld = %s\n", i + 1, args[i].strptr);
   }

   return(RXFUNC_OK);
}

But there is one more thing to consider. A REXX script could "omit" or "skip" an arg passed to your function. What this means is that, where the arg would appear, the REXX script simply leaves blank space. Below, the REXX script omits that second arg of 12. Notice that there is just blank space before the comma where the 12 used to be:

MyReturn = TestFunction('Arg One', , MyVariable, "Arg 4")

How is this reflected in the args passed to your function? Well, 'numargs' will still be set to 4. But the RXSTRING for that second arg will have its strptr field set to 0. This alerts you to the fact that the REXX script omitted this arg.

Here then is a version of TestFunction() that properly checks if an arg has been omitted before printing out each arg:

APIRET APIENTRY TestFunction(CONST CHAR *name, ULONG numargs, RXSTRING args[], CONST UCHAR *queuename, RXSTRING *retstr)
{
   ULONG i;

   for (i = 0; i < numargs; i++)
   {
      if (args[i].strptr)
         printf("Arg #%ld = %s\n", i + 1, args[i].strptr);
      else
         printf("Arg #%ld is omitted\n", i + 1);
   }

   return(RXFUNC_OK);
}

With most interpreters, you're allowed to modify the contents of the args passed to you. (For example, you can overwrite the contents of an arg's data buffer, so long as you don't exceed the RXSTRING's strlength). But you shouldn't change the RXSTRING's strptr or strlength fields. And modifying the contents of those args is not reflected back in the script. (So, you can't use this as a way of returning new data to the script). But beware that this was never standardized, so some interpreters may behave strangely if you modify the contents of an arg passed to you. Reginald does not care if you modify the contents of args.

Some interpreters limit how many args a script can pass to your function. (The minimal acceptable limit is 16 args, so to be compatible with all interpreters, don't design a function that expects more than that). But Reginald does not limit how many args can be passed to a function.


Returning a REXX string to the REXX script

If you want to return data to the script, you must use the supplied 'retstr' RXSTRING. This points to a data buffer (provided by the interpreter) into which you can place any data you desire. The size of the provided buffer is RXAUTOBUFLEN (ie, typically 256 bytes). You must then set the RXSTRING's strlength to reflect how many bytes you copied into the buffer. Here then is how TestFunction() would return a string "Hello World" to the script:

APIRET APIENTRY TestFunction(CONST CHAR *name, ULONG numargs, RXSTRING args[], CONST UCHAR *queuename, PRXSTRING retstr)
{
   strcpy(retstr->strptr, "Hello World");
   retstr->strlength = strlen(retstr->strptr);

   return(RXFUNC_OK);
}

Note that the data you return to the script does not need to be nul-terminated (although we did so in the example above because it was easier that way). And the strlength should not reflect any extra nul byte if you do nul-terminate the data.

How does the script get this value from TestFunction()? It does so just like a C function would. Here's how the REXX script would assign this returned value from TestFunction() to a variable 'MyReturn' and then display it:

MyReturn = TestFunction()
SAY MyReturn

If a REXX script does not need to use the value returned from your function, it must use the CALL keyword to call your function as if it were a subroutine. All this really entails is putting the CALL keyword in front of the name of the function, and omitting the parentheses around the arguments, as so:

CALL TestFunction 'Arg One', , MyVariable, "Arg 4";

When a script calls your function as if it were a subroutine, any REXX string you returned to the script will (temporarily) be assigned to the special RC REXX variable in the script.

NOTE: Reginald supports keeping the parentheses around the args passed to your function, while using the CALL keyword. Other interpreters may not support this.

Alternately, the REXX statement ADDRESS NULL may be added to the top of the script, and then the script can toss your return value away much like in C -- without needing the CALL keyword, and keeping the parentheses, as so:

ADDRESS NULL
TestFunction('Arg One', , MyVariable, "Arg 4");

NOTE: Calling a function without the CALL keyword or without having an ADDRESS NULL before it, will result in the string that your function returns being passed to some Subcom Handler, by default, the operating system's command shell. Strange/unexpected behavior/errors may be seen if that happens. (Subcom Handlers are discussed in Using REXX for scripting).

If you don't want to return any value to the script, then you may set the retstr's strptr field to 0. Here is an example of a function that returns no value to the script:

APIRET APIENTRY TestFunction(CONST CHAR *name, ULONG numargs, RXSTRING args[], CONST UCHAR *queuename, RXSTRING *retstr)
{
   retstr->strptr = 0;
   retstr->strlength = 0;  /* Some interpreters require the strlength field 0 too */

   return(RXFUNC_OK);
}

If your function doesn't return a value, the REXX script must call your function like a subroutine. Yes, this is inconsistent, but that's the way that REXX was designed.

For this reason, you may prefer to return an empty string. Simply set the retstr's strlength field to 0, but don't clear the strptr field. Here is an example of a function that returns an empty string to the script:

APIRET APIENTRY TestFunction(CONST CHAR *name, ULONG numargs, RXSTRING args[], CONST UCHAR *queuename, RXSTRING *retstr)
{
   /* Return an empty string */
   retstr->strlength = 0;

   return(RXFUNC_OK);
}

NOTE: Reginald does away with this inconsistent behavior of REXX. Even if your function returns no data, a REXX script may still call it like a function (ie, without the CALL keyword, and using parentheses around the arguments), and does not need the ADDRESS NULL statement.

You may be wondering "What if I need to return more than RXAUTOBUFLEN bytes of data?". Well, then you have to allocate a new buffer (using an interpreter function called RexxAllocateMemory()), and stuff that pointer into retstr's strptr field. The interpreter will take care of freeing that buffer when it's done with it. Here's an example of TestFunction() returning a 1,000 byte buffer filled with zeroes:

APIRET APIENTRY TestFunction(CONST CHAR *name, ULONG numargs, RXSTRING args[], CONST UCHAR *queuename, RXSTRING *retstr)
{
   /* Allocate a new data buffer and stuff the
      pointer into the retstr RXSTRING */
   if (!(retstr->strptr = RexxAllocateMemory(1000)))
   {
      retstr->strlength = 0;

      /* An error. Let the interpreter know */
      return(RXERR_STORAGE_EXHAUSTED);
   }

   /* Clear the buffer */
   memset(retstr->strptr, 0, 1000);

   /* Set the strlength */
   retstr->strlength = 1000;

   /* Return successfully */
   return(RXFUNC_OK);
}

Return values (and errors)

Let's talk a little about the return value (to the interpreter) from your function. As mentioned, it should be 0 if everything went well. It should be non-zero if there is a error. The standard is to return the value 40 (which is defined as RXERR_INCORRECT_CALL in Reginald's REXXSAA.H). So what does this do? Well, if the script is trapping the SYNTAX condition, then the interpreter triggers that condition. The error message that the script retrieves via CONDITION('D') is a rather ambiguous "Incorrect call to function". If the script isn't trapping SYNTAX, then the interpreter aborts the script, displaying that same error message to the user.

Well, that isn't very informative, is it? Newer interpreters substitute the error message "External function <name> failed" where <name> is replaced with the name of your function. That's a little bit more informative, but it still falls far short of giving you the ability to report what specifically went wrong. This is an oversight of the design of REXX.

But Reginald comes to the rescue here. Reginald allows you to return a wide array of error values (listed under the heading "EXTERNAL FUNCTION HANDLERS" in REXXSAA.H). For example, by returning the value RXERR_INV_SUBKEYWORD, Reginald will report an error message of:

EXE function <name> reported "Invalid sub-keyword found"

This gives a bit more information about why your function has failed. But, there's more. There are also some "pre-fabricated" error messages that, combined with the MAKEFUNCARG() macro, allow you to construct a message with your own information inserted into the message. For example, returning a value of RXERR_ARGZEROONE allows you to invoke the prefabricated message:

EXE function <name> reported "argument <argnumber> is not 0 or 1; found "<value>"

<name> will be the name of your function. <argnumber> tells which argument was in error, where 1 is the first argument passed to you. You need to tell Reginald which argument number was in error. <value> will be the value of the erroneous argument that the script passed to you. In this way, the user gets very specific information about why your function failed.

How do you tell Reginald which arg number was the erroneous one? You OR the argument number with your return value of RXERR_ARGZEROONE, and use the MAKEFUNCARG() macro. For example, to specify that you want Reginald to report that the second arg passed to your function was in error because it was supposed to be 0 or 1, but was some other value, you simply return the following:

   return(RXERR_ARGZEROONE | MAKEFUNCARG(2));

It's that easy. And the truly nice thing about is, if Reginald's MSGBOX OPTION is enabled, the error message pops in a box with a "Help" button which the user can click upon, and Reginald will bring up a help page specifically about that error message with helpful hints as to what the error is about and how to fix it.

And if these pre-fabricated error messages are not enough, Reginald even has a specific error return that allows you to return your own error message. You simply copy your error message into the return RXSTRING -- just like you were returning some data to the script -- and then return the value RXERR_EXTFUNC_ERR. For example, here's how you return the error message "You're an idiot!":

APIRET APIENTRY TestFunction(CONST CHAR *name, ULONG numargs, RXSTRING args[], CONST UCHAR *queuename, RXSTRING *retstr)
{
   strcpy(retstr->strptr, "You're an idiot!");
   retstr->strlength = strlen(retstr->strptr);
   return(RXERR_EXTFUNC_ERR);
}

Reginald will then post an error message of:

EXE function "TestFunction" reported "You're an idiot!"

If the user clicks on the Help button, this will bring up a page that explains to him that this is a error message from an external function, but it can't give him helpful hints about what likely caused it and how to fix it. So, you should use the pre-fabricated messages wherever possible.

These extra return values are all proprietary to Reginald, but it should not bother any other properly written interpreter. If you return RXERR_ARGZEROONE | MAKEFUNCARG(2) to some other interpreter, for example, the other interpreter should simply post the error message "External function <name> failed". In other words, the worst that the interpreter will do is ignore your return, assume that you returned RXERR_INCORRECT_CALL, and yield that generic, uninformative error message. So, it should be safe to utilize these Reginald features in your external functions so that, when run under Reginald, your external functions will sport robust, informative error reporting.


Making our Dynamic Link Library

Now that you know how to write a REXX-callable function that accepts args (ie, REXX strings) from the script, and can return one REXX string to the script, the final step is to turn your C code into a DLL.

If you've installed the Reginald Developer's Kit, the Dll1 directory contains an example of a C DLL that registers two functions. One function is called TestAdd(). The script passes two numeric args, and TestAdd() adds the args together and returns the sum to the script. The other function is called TestDoCanadian() and it simply appends the phrase "eh?" to one arg string passed to it. (It's a joke, eh?) And there is a REXX script called test.rex which makes calls to those two functions. Spend some time perusing this relatively small, simple example to see how you can add your own functions to REXX.

Creating a DLL can involve some operating system specific stuff. For example, on Windows, you need to "export" your REXX-callable functions by creating a linker definition (.DEF) file. You usually also have to instruct your compiler/linker that you wish to create a DLL. So, you may need to read the materials with your development system for instructions on how to create a DLL. If you're using Microsoft Visual C++ 6.0, I've created a walkthough for How to create a REXX function library.


How the REXX script registers your functions

In order to be able to call a specific function in your DLL, a REXX script must first register that function with the interpreter. This lets the interpreter know the name of the DLL file (containing your function), and the name of your function within your DLL (ie, the name that you gave your function when you compiled your DLL). The REXX script can assign any "REXX name" it wants to this function, for the purpose of calling it. But to register it, the script must know the name of your DLL and the real name of your function.

The REXX script registers your function using the standard REXX built-in function RXFUNCADD. RXFUNCADD takes the name of the library (DLL file) which contains the function (minus the .DLL extension), the name of your function that it wishes to call within the library, and the REXX name that the script would like to assign to your function (ie, so that the script can call it using a name different than its real name).

For example, let's say that you created a DLL named "mylib.dll", and you put a REXX-callable function in it called "TestFunc". Here's how the REXX script would register that function, and give it the name "MyNewName" for the purposes of calling it:

err = RXFUNCADD('MYNEWNAME', 'mylib', 'TestFunc')

After this successfully completes (ie, err = 0, otherwise an error occurred), he can call your function using his assigned REXX name, passing you arguments. Here, the script calls your function, passing the literal string "Hello", and stuffing your returned REXX string into a variable called "MyVar":

MyVar = MyNewName('Hello')

Of course, a script could make his REXX name the same as the real name when he registers and calls your function:

err = RXFUNCADD('TESTFUNC', 'mylib', 'TestFunc')
MyVar = TestFunc('Hello')

You can put as many REXX-callable functions in your DLL as you would like. But the REXX script must individually register each function that the script wishes to call. (ie, The script must do an individual RXFUNCADD() for each of your REXX-callable functions, before the script can call that function).

After a script is done using your function, and does not wish to ever call it again, the script may use the RXFUNCDROP() built-in to deregister your function. (But this isn't usually necessary. A well-written interpreter will automatically deregister functions on behalf of the script when that script terminates. Reginald does).


Helping the REXX script register your functions

If you have a lot of REXX-callable functions in your DLL, then a REXX script may need to make numerous calls to RXFUNCADD(). That's a lot of REXX code and a lot of error checking for the script to do.

Many DLL authors try to make it easy for a REXX script to register all of the library's functions. The DLL may have a special, REXX-callable function inside of it, which when called, registers all of the other functions in the DLL on behalf of the REXX script. This means that the script doesn't have to do a separate RXFUNCADD for every function that it wishes to call inside of your DLL. Of course, the REXX script does have to initially call RXFUNCADD for that one special function, and then call that special function once.

When you register functions that are inside of your DLL, you do not use the interpreter API RexxRegisterFunctionExe(). Rather, you use RexxRegisterFunctionDll(). The latter takes 3 args -- the REXX name you're giving to your function, the name of your DLL, and the real name of the function.

For example, let's assume that you have two REXX-callable functions inside of your "mylib.dll". These two functions are called TestFunc1 and TestFunc2, respectively. And your DLL contains a third, REXX-callable function named TestLoadFuncs which registers the other two functions on behalf of the script. Here's how you would write TestLoadFuncs:

APIRET APIENTRY TestLoadFuncs(CONST CHAR *name, ULONG numargs, RXSTRING args[], CONST UCHAR *queuename, RXSTRING *retstr)
{
   APIRET   err;

   /* Register TestFunc1. The REXX script will have to call it as so */
   if ((err = RexxRegisterFunctionDll("TESTFUNC1", "mylib.dll", "TestFunc1")))
   {
      /* An error. Let the interpreter know */

      strcpy(retstr->strptr, "TestFunc1 failed to register");
bad:  retstr->strlength = strlen(retstr->strptr);
      return(RXERR_EXTFUNC_ERR);
   }

   /* Register TestFunc2. The REXX script will have to call it as so */
   if ((err = RexxRegisterFunctionDll("TESTFUNC2", "mylib.dll", "TestFunc2")))
   {
      /* An error. Let the interpreter know */

      /* First, we need to deregister TestFunc1 */
      RexxDeregisterFunction("TESTFUNC1")

      strcpy(retstr->strptr, "TestFunc2 failed to register");
	  goto bad;
   }

   /* For success, we return nothing to the script */
   retstr->strptr = 0;
   retstr->strlength = 0;

   /* Return successfully */
   return(RXFUNC_OK);
}

Now here's how the REXX script uses RXFUNCADD() on TestLoadFuncs and then calls TestLoadFuncs:

err = RXFUNCADD('TESTLOADFUNCS', 'mylib', 'TestLoadFuncs')
CALL TestLoadFuncs()

After the above, the REXX script may call TestFunc1 and TestFunc2 in your DLL. Of course, since you were the one who registered the functions, the script needs to use the "REXX name" that you provided as the first arg to RexxRegisterFunctionDll(). So, the script could call TestFunc1 as so:

MyVar = TestFunc1()

RexxRegisterFunctionDll() for some interpreters is case-sensitive with regard to names. For example, if you register a function with the REXX name of "testfunc" and the REXX script tries to call it as TestFunc(), then the interpreter will not find such a registered function. For this reason, it is best to upper case the REXX name you pass to RexxRegisterFunctionDll(). You don't need to actually make the real names of your functions all upper case -- just the first arg passed to RexxRegisterFunctionDll() -- ie, the "REXX name" that you give to the function. That way, when the REXX script calls your function and doesn't put quotes around the name, the REXX interpreter will upper case it as well. Reginald upper cases any REXX name you pass to RexxRegisterFunctionDll(), so you need not worry about doing that yourself. I recommend that you preface all of your REXX names with a certain string of characters. For example, here I chose to begin all function names with the 4 characters "Test". This helps you to identify calls to your DLL when you write a REXX script. Also, if you pick a unique preface, then it helps to avoid name collisions with other REXX scripts. (ie, You don't want to have identical REXX names with another DLL function library, otherwise your call to RexxRegisterFunctionDll() will fail with RXFUNC_DEFINED if the other guy registered his first).

As far as the real name of your function, you must specify the case exactly as you used it in your source code. As far as your library name is concerned, you must specify the case exactly as the filename itself, only on operating systems that have case-sensitive filenames. (yuck. Windows does not).

Typically, you'll add another special function to your DLL that is the complementary function to TestLoadFuncs(). It will deregister all of your functions (using RexxDeregisterFunction()) so that the script doesn't have to use RXFUNCDROP() upon each function name. The script is expected to call that complementary function after it is done using the functions in the library. Needless to say, TestLoadFuncs() should also register this complementary function. Here is the complementary function, which we'll call TestDropFuncs:

APIRET APIENTRY TestDropFuncs(CONST CHAR *name, ULONG numargs, RXSTRING args[], CONST UCHAR *queuename, RXSTRING *retstr)
{
   /* Deregister TestFunc1 */
   RexxDeregisterFunctionDll("TESTFUNC1");

   /* Deregister TestFunc2 */
   RexxDeregisterFunctionDll("TESTFUNC2");

   /* For success, we return nothing to the script */
   retstr->strptr = 0;
   retstr->strlength = 0;

   /* Return successfully */
   return(RXFUNC_OK);
}

The name you supply to RexxDeregisterFunctionDll() is the "REXX name" that you gave when you called RexxRegisterFunctionDll(). Again, it is best to upper case this name. And again, Reginald does this for you automatically.

If you've installed the Reginald Developer's Kit, the Dll2 directory contains a version of the Dll1 example that implements the above TestLoadFuncs and TestDropFuncs. (But notice that we use a lookup table of function names to reduce the number of strings in our code, and also to more easily loop through the table to make calls to RexxRegisterFunctionDll() and RexxDeregisterFunction(). Also, for interpreters other than Reginald, we deal with that upper case problem).


Supporting Reginald's auto-load feature

The Reginald REXX Interpreter takes this ease-of-use one step further. It allows your DLL to auto-load its functions. When a DLL is auto-loaded, it automatically registers all of its functions on behalf of the REXX script as above, but the REXX script doesn't even need to register and call even one special function. The script simply calls any functions it wishes in your DLL as soon as the script runs. Nor does the script need to call any complementary function when it is done using the functions in your DLL.

In order for your DLL to support Reginald's auto-load feature, you must register all of the functions in your DLL (as shown above). But you must supply a special function called RexxRegisterFuncs(). This will return 0 if all went well.

If your DLL supports Reginald's auto-load feature, then a user can add it to Reginald's Auto-loaded function libraries list. He uses Reginald's REXX Administration Tool to do this. Once he does this, all of the REXX-callable functions in your DLL will be automatically made available to every REXX script that runs on his computer. No script will need to do any RXFUNCADD() or RXFUNCDROP() at all.

If you've installed the Reginald Developer's Kit, the Dll3 directory contains a version of the Dll2 example that implements support for Reginald's auto-loading.


Windows: If you would like to write an installer for your function library that detects whether Reginald is installed, and if so, installs your DLL as an auto-loading function library, you need to do the following from your install program:

To detect whether Reginald is installed, try to open the "HKEY_CURRENT_USER\Software\Reginald" key in the Windows registry. If that key exists, then Reginald is installed. To set your DLL for auto-load, you need to add a value to this Reginald key. The name for the value will be FuncXXX where XXX is the first free "slot number". The actual data you set for that value is the full path name of your DLL ("C:\Program files\MyDir\MyDll.dll" for example). Here is an example below:

HKEY  hKey;

/* Open Reginald key */
if (!RegOpenKeyEx(HKEY_CURRENT_USER, "Software\\Reginald", 0, KEY_ALL_ACCESS, &hKey))
{
   /* Reginald is installed. Here you would ask the user if
    * he wishes to add your DLL to Reginald's auto-loaded Function
    * Libraries. If so, continue with the code below.
    */

   if (UserWantsUsAutoLoaded)
   {
      unsigned long  dllnum;
      unsigned long  type;
      unsigned long  len;
      char  buf[10];
      char  dll[MAX_PATH];

      /* Assume that the full path name of your DLL is in the buffer MyDllName[] */

      /* Start with the first slot number (ie, 0) */
      dllnum = 0;

      do
      {
         /* Create the next registry value name. It
          * will be FuncXXX where XXX is the slot number.
          */
         sprintf(buf, "Func%u", dllnum++);

         /* See if there is already an auto-loaded DLL with this value
          * name, and if so, fetch the DLL pathname into dll[].
          */
         type = REG_EXPAND_SZ;
         if ((type = RegQueryValueEx(hKey, buf, 0, &type, dll, &len)))
         {
            /* Were there no more auto-loaded DLLs? */
            if (type == 2)

               /* There were no more auto-loaded DLLs, so add our DLL */
               RegSetValueEx(hKey, buf, 0, REG_EXPAND_SZ, MyDllName, strlen(MyDllName) + 1);

            /* Done. (Either successful, or we had an error that we can't handle) */
            break;
         }

         /* See if this value key is for our DLL. If so, it's already set to auto-load,
          * so we need not add another value name for it.
          */
      } while (strcmpi(dll, MyDllName));
   }

   /* Close Reginald key */
   RegCloseKey(hKey);
}

User interface guidelines

One thing that you should never do in your REXX-callable function is to call printf() or to otherwise call any function that displays text to a console window. (For example, don't do a C++ cout). With the advent of graphical interfaces for REXX, a console window may not even be open. Don't make an assumption otherwise.

Likewise, never do a scanf() or otherwise call any function that attempts to get user input from a console window. Your input should be from the REXX script via its args passed to you (or perhaps querying variables in the REXX script, or pulling lines off of the queue).

It's the script's job to get user input and output. The script should be free to choose how to do that. For example, maybe the script will use some other function library that has special functions to open windows with menus and graphical controls such as buttons and sliders. Don't circumvent the script's user interface by trying to go directly to the user for input, or trying to directly display information to the user, unless the purpose of your functions are to provide the script with a new type of user interface.

So if your function has an error, don't print/display any error message. Instead, return an error number to the interpreter. The interpreter will inform the script that an error has occurred (usually by triggering the SYNTAX condition). Leave it to the REXX script to decide when and how to display any messages to the user. After all, the script may want to "silently" handle the error first. Don't make the assumption that you should be the one to report error messages to the user. Let the script manage the user interface.

When you use Reginald's "pre-fabricated error messages" (ie, you return any of the listed error numbers in REXXSAA.H such as RXERR_STORAGE_EXHAUSTED, RXERR_BAD_ARITHMETIC, RXERR_ARGMISSING, RXERR_VARNAME, etc), you benefit from Reginald's Error Message Box feature. (ie, If the script isn't trapping SYNTAX, then a message box pops up presenting an error message, the name of the script that called your function, the line number where the error occurred, and the actual line in the REXX script. Plus, there's a Help button that the user can click to bring up an online help page about that specific error. So, Reginald already is capable of presenting more informative error feedback than your function could ever do)

If you need to report errors above and beyond what Reginald supports in its "pre-fabricated error messages", then you can return your own error message as shown earlier (ie, return the special error number RXERR_EXTFUNC_ERR, and copy your error message into the buffer that you normally use to return your REXX string to the script). But if you want to supply your own online help in conjunction with the Help button on Reginald's Error Message box, a better option is to use Reginald's proprietary RexxRaiseCondition().


Setting/Querying REXX variables in the script

Your function can set or query the value of any variable in the REXX script using the REXXSAA API RexxVariablePool(). This is described in the tutorial Using REXX for scripting.


Halting execution of a script

The REXXSAA API RexxSetHalt(), and the proprietary API RexxRaiseCondition() can be called from your function to trigger the HALT condition in the script. (The script will not begin to service this until after your function returns). It is advisable to return RXFUNC_OK after you've called RexxSetHalt() or RexxRaiseCondition(), so that you don't get a superfluous error message.

RexxSetHalt() and RexxRaiseCondition are discussed in Using REXX for scripting.


Supporting various interpreters

If there are various interpreters available for your platform, and you wish to support them all, you should not link with the .LIB file for any given interpreter. Doing otherwise is "static linking", and ties your REXXSAA API calls to that one interpreter for which you have a .LIB file. For example, if you've looked at the Microsoft Visual C++ Projects that accompanied this tutorial, you'll see that we linked with REGINALD.LIB. When you link with REGINALD.LIB, it inserts a small amount of "stub code" that allows Windows to automatically load REGINALD.DLL (ie, Reginald's REXX interpreter) and go through your DLL, resolving your calls to Reginald's REXXSAA API.

But if you want to support more than one interpreter, you should instead use dynamic linking. You do not use the .LIB file at all. This will usually require you to do some operating specific stuff. Consult the developer documentation with your system about dynamic linking to libraries.

Windows: If you're writing a DLL for Windows, then you can use the following techniques to create a function library that works with various interpreters for windows.

First, you need to know the names of the DLLs for all the various interpreters you wish to support. For example, Reginald's interpreter is contained in a DLL called "reginald.dll". Regina's interpreter is contained in a DLL called "regina.dll". Object REXX's interpreter is in a DLL called "rexx.dll". And there are a few others.

You need to call the Windows API GetModuleHandle() passing it the name of each one of your supported interpreter DLLs. When GetModuleHandle() returns a handle for a given interpreter name, that is the interpreter that is using your function library. (GetModuleHandle() will otherwise return 0 if a given interpreter name is not the one who is using you).

Assuming that GetModuleHandle() returns a handle for one of your supported interpreter DLLs, you can then get the addresses of each one of its REXXSAA APIs you're interested in, by using the Windows API GetProcAddress(). You pass the handle to the interpreter DLL, and a string containing the nul-terminated name of the API (for example "RexxRegisterFunctionDll"). If this REXXSAA API is found inside of the interpreter, its address is returned. You should save this address in a global variable in your program. Then whenever you wish to call this REXXSAA API, you use that variable to fetch its address. In other words, you can't directly hardcode a call to a REXXSAA API such as RexxRegisterFunctionDll() in your DLL. Rather, you call the function indirectly, using a pointer to it.

The best place to get pointers to the REXSAA API is in your DllMain function (ie, your DLL's load code) -- the first thing that you do.

If you installed the Reginald Developer's Kit, the Dll4 directory shows a version of Dll3 with dynamic linking to support several Windows interpreters. Note the code in DllMain() when the DLL is first loaded. Also note that we don't link with the REGINALD.LIB file.