In our discussion of extending Rexx, I am going to present the Personal Rexx for Windows NT API. The OS/2 API and the Personal Rexx API are virtually identical. The major difference between the two is with respect to the fundamental datatypes used by the respective APIs. For instance, under OS/2, PSZ is typically used for pointers to character strings, while under Win32 the Personal Rexx API usually uses LPCSTR.
Before we go any further, I need to introduce the fundamental Rexx API datatype, the RXSTRING. Remember that Rexx treats everything as a string. This is represented internally by the RXSTRING datatype. Figure 3 shows the RXSTRING typedefs for both OS/2 and Personal Rexx. The RXSTRING datatype is a structure containing two elements, the string length and a pointer to the string itself. This string can be of any size, up to the limits of the machine.
Internally, the interpreter does not use NULL terminated strings. However, when the interpreter passes RXSTRINGs off as arguments to external functions, a NULL is added to the end of the string. It is, however, important to remember that the string is allowed to contain NULLs. Consequently, never use any of the standard C string functions to manipulate an RXSTRING string pointer unless you're reasonably certain that the only NULLs will be the ones that terminate the string. To be safe, use the standard C memory functions along with the length field in the RXSTRING struct.
In order to save hair and make life with RXSTRINGs a little easier, both APIs provide several macros devoted to dealing with RXSTRINGs. Figure 4 shows the RXSTRING macros. From the macros we can see the three basic ways that an RXSTRING can be presented to us. An RXSTRING is said to be NULL when the string pointer doesn't point to anything. An RXSTRING is said to have zero length when the string pointer points to something, but that something is NULL. We have a valid RXSTRING whenever the string pointer points to something and that something is not NULL. The RXSTRLEN and RXSTRPTR macros retrieve the string length and string pointer respectively, while the MAKERXSTRING macro lets us build an RXSTRING without having to manipulate both components of the structure separately.
The Rexx interpreter expects external functions to be defined with a specific argument list. When the interpreter calls the external function, five arguments are passed, the function name, the number of arguments, a pointer to the arguments themselves, the name of the current external data queue and a pointer to a return string. For function prototyping, a typedef is provided by both APIs to make that task quite a bit simpler. Figure 5 shows the external function prototype typedefs for both systems.
The arguments passed by the Rexx interpreter from the Rexx program to the external function are, of course, RXSTRINGs. What the Rexx interpreter gives us is actually a pointer to those RXSTRINGs. In addition, the return string provided by the interpreter to contain any results that we may want to return, points by default to a 256 byte block of storage. It's important to remember that our return string is limited, by default, to 256 bytes. I will discuss how to get around that limit below. The value that our function places in this RXSTRING is what is returned to the user in the Rexx program. The user can call our function in one of two ways:
In the first case, the value our function places in the return RXSTRING is assigned by the interpreter to the variable coded by the user, in this case "ret_val". With the second method of calling our function there is no variable specified into which the return value can be placed. To handle situations like this, the Rexx interpreter assigns a special variable called "RESULT" the return value from the function call. Figure 6 shows two code fragments illustrating the use of both techniques.
So, now we know what the Rexx interpreter is going to give us and we know that the interpreter has provided us with an RXSTRING in which we can place a value to be returned to the Rexx program. The last piece is the return code. Both APIs define external functions as returning a 32-bit value. This value is examined by the interpreter upon return from our function. A zero return code tells the Rexx interpreter that our function performed correcly. A non-zero return code indicates that we encountered an error somewhere within our function. If we return a non-zero code, then the interpreter reports an "error 40: Invalid call to routine" to the user.
If we write a function and that function does not return a value, then the RXSTRING provided must be set to an NULL string. The easiest way to do this is to use the MAKERXSTRING macro:
Ok, enough of the preliminaries. Let's take a look at an example of an external function. We'll use our make-believe function from the Rexx program in Figure 2, the "f2c" function to convert degrees Fahrenheit to degrees Celsius. Except now, we'll make it into a real function that our Rexx program can call. Figure 7 shows the function.
We start by declaring a variable to hold the value that the user has passed to us. The purpose of our function is to convert a value from Fahrenheit to Celsius. So, it's pretty much essential that the user provides us with a value to convert. That's the next step, to get the value the user has specified. The first thing we do is check the argument count to make sure that one and only one argument has been specified by the user. Strictly speaking, we need only check that the user has entered at least one value, but we want to make sure that our function is used exactly as intended. If we don't find exactly one argument, then we're going to return to the interpreter, indicating an error.
Finally, the guts of our function. We've checked that the user has specified one and only one argument. Now we need to get that argument and perform our calculation. Remember that we're being passed an RXSTRING which contains the user specified value. Also remember that Rexx treats everything as a string, so this value is also a string. So, that means we need to convert the string to a number so that we can do some math with it. The simplest way to do this is to use the sscanf C function, which is what we do, asking that the converted value be placed in the variable "n".
Ok, the user has entered one and only one argument and we've converted that string value into a number. We're ready to return an answer, using the simple conversion formula. Now we're ready to return the value to the user.
Returning our answer to the user is done in two parts. First, we need to fill in the RXSTRING values. We'll do this in a slightly clever manner. We know that the RXSTRING holds both a string pointer, as well as a string length. Remembering that Rexx treats everything as strings, we know that we need to convert our number answer into a string. The simplest way to do this is to use the sprintf C function and that's what we're going to do. But wait, doesn't the sprintf function return the number of bytes "printed"? Yes, so we use that fact to assign the entire sprintf function call to the RXSTRING string length variable and fill in our RXSTRING return string with a single statement.
Finally, we're ready to return control to the Rexx interpreter. Since everything worked as advertised, we use a zero return code.
Return to Contents
Read next section Building the DLL
Bill Potvin
bill.potvin@network.com