A few months ago, I discussed a technique describing how to declare arrays in RPG IV that had a variable number of array elements. Arrays of this nature are referred to as dynamic arrays. This week, I am presenting a new technique for declaring dynamic arrays. This one does not have the complexities of the previous technique.
A major shortcoming in the previous technique that I illustrated was the need to allocate and reallocate memory dynamically based on a mathematical equation (the number of desired elements multiplied by the length of a single element). In addition, the requirement that the number of array elements currently allocated had to be tracked by the program is undesirable. The technique was useable but not fun.
This time, none of those shortcomings occur. About the only oddity is the use of a pointer, and the use of that pointer isn't complex at all. Here's the outline of this new technique:
- Declare the array with the BASED keyword.
- Get a pointer to a user space.
- Assign that pointer to the pointer in the BASED keyword.
Other than that, you can use the array as if it were dynamic, because it is now automatically growing as you access elements in the array. So, if one time you access five elements and another time you access 5,000 elements, your program will work, and none of the allocate/deallocate issues exist.
First things first. To create a dynamic array, you need to declare the array with the BASED keyword. Within the BASED keyword, specify the name of a field. The field name does not need to exist, and probably should not exit. If it does not exist, the RPG IV compiler automatically generates the correct declaration for it. If it does exist, it must be declared as a data type of pointer (*).
The following Definition statement declares an array named DYNARR and specifies the BASED keyword. The BASED(pArr) keyword identifies the based-on pointer field. Since there is no explicit declaration for that field, RPG IV automatically declares one for you.
D dynArr S 200A Dim(32766) BASED(pArr)
The following two Definition statements have the same effect as the previous one; however, the pArr variable is explicitly declared on the first line. Therefore, the compiler does not need to declare one for you. This style is useful for more advanced programming in which, perhaps, you would leverage the pArr variable for more than one use, or you might use this style simply for completeness.
D pArr S *
D dynArr S 200A Dim(32766) BASED(pArr)
Whenever a variable is declared and that declaration contains the BASED keyword (as in the examples above), the compiler does not allocate storage for the variable. That means that if you try to move something into DYNARR, you'll get a runtime error, because no storage has been allocated for the variable. When the BASED keyword is involved, you are telling the compiler that you will allocate the storage for the variable yourself. This could mean using the ALLOC/REALLOC opcodes or simply assigning the address of another variable to the pointer. See the example below.
0001 D pData S *
0002 D Data S 32A BASED(pData)
0003 D Real S 128A
.....C..n01..............OpCode(ex)Extended-factor2++++++++++++++++
0004 C eval pData = %addr(Real)
In this example, the field DATA is declared as a 32-byte character field with the BASED keyword. The pointer field pDATA is explicitly declared on the prior statement. Initially, no storage is assigned to the pDATA pointer; therefore, the DATA has no storage associated with it.
To assign a value to the pDATA pointer, and consequently to provide storage for the DATA variable, an assignment statement is used (line 4). The %ADDR built-in function returns the memory location (i.e., the address) of the field identified by its first parameter. An address is the only type of data that may be stored in pointers.
Once this assignment is made, the data that has been allocated (automatically by the compiler) for the REAL variable is now also being used for the DATA variable. Overlapping fields? Yes.
Notice the variance in the field lengths. The DATA field is 32 bytes long, whereas the REAL field is 128 bytes long. This is perfectly fine as long as the REAL field is at least as long as the DATA field. If the situation were reversed, however, you'd run into a problem if you attempt to access byte 33 of the 32-byte field.
User Spaces as Dynamic Arrays
The safest way that I've found to dynamically allocate storage for a dynamic array is to not do it at all. That is, come up with a way to make the system safely and automatically allocate the storage for you. After all, isn't that the way a true dynamic array scheme would work if IBM did it for us? The big question is, however, what is there that would do such a thing? It occurred to me that a user space object (*USRSPC) could be just the right solution to this question.
User space objects are what data areas are based on. Space objects have been on this system for over 25 years, and user spaces have been around for as long as the AS/400's been around and then some. So they are a pretty reliable object to use.
By default, user space objects are fixed-size objects, just like a data area. However there are two interesting aspects of user space objects that help solve the dynamic memory problem.
1) User space objects have an attribute that controls whether the user space is fixed-length or variable-length. Changing that attribute to 1 causes the user space to become auto-extending. This means that if you create the user space with a length of 12 bytes and you attempt to read or write to byte 750, the underlying interface automatically extends the user space to at least 750 bytes. You do nothing special; it just happens!
2) Using the QUSPTRUS API, you can retrieve a pointer to a user space object that works and acts just like a pointer from the %ADDR built-in function.
Given these two facts, it occurred to me that I could just do something like this:
0001 D dynArr S 200A Dim(32766) BASED(pArray)
.....C..n01..............OpCode(ex)Extended-factor2++++++++++++++++
0002 C CallP QusPtrUs(szUS:pArray:apiError)
The field named szUS contains the name of the user space. The field name pArray is the return value that receives the pointer to the user space object's data, and apiError is the standard IBM-supplied API error data structure.
With just two lines of code, you can declare and assign the storage for a dynamically sized array. The best part is that you don't have to worry about deallocating or freeing up the storage for the dynamic array when you finish. Create the user space in QTEMP and forget about it!
The bad news is that if you now use something like the SORTA opcode, the entire array will be sorted and hence extend the user space up to the full size of the array. That may be OK if you're expecting that to happen, but you may get unwanted results if you expected it to only sort the elements with data in them. Obviously, a full IBM-provided solution is needed, such as the rumored %SUBARR built-in function that may allow you to segment an array and work with the dynamically specified from and to elements.
Odds and Ends
The QUSPTRUS API is used to retrieve a pointer to the user space object's data. The APi can be easily called with the traditional CALL/PARM opcodes. But after all this is 2003, not 1983, so why not call it using a prototype? The source for the prototype follows:
D QusPTRUS PR ExtPgm('QUSPTRUS')
D szUserspace 20A Const
D pRtnPtr *
D apierror 16A OPTIONS(*VARSIZE)
Remember, the parameter names on a prototype are just placeholders or comments. They are not field declarations. So it doesn't matter what you call them, but you should take advantage of the fact that they are not declarations and use them in lieu of comments. For example, "szUserSpace" helps to signify that the field is character and is supposed to contain the name of a user space.
The apiError parameter is the standard API exception/error data structure. Unfortunately, the APIs lack consistency with respect to this data structure. Some of them require it to be passed as a parameter; on some, it is optional; and on others, there is an alternate format. For our purposes, the apiError data structure's format is declared as follows:
D apiError DS Inz
D apiLen 10I 0 Inz(%size(apiError))
D apiRLen 10I 0
D apiMsgID 7A
D apiResv1 1A Inz(X'00')
D apiErrData 10A
A Full Example
The example that follows can be compiled on your system and should provide you with an example of how this technique works. But first, you must create a user space with a relatively small size (larger size is OK, but we're only testing at this point). To create a user space, call the CrtUsrSpace procedure (included in the RPG ToolKit) from within your RPG IV program and specify a size of something like 32 bytes or so, as follows:
C Callp CrtUsrSpace(szUS : 32)
If you don't have the RPG ToolKit, you can key in and run the CL command and the RPG IV CPP listed in Figures 2 and 3 at the end of this article. That command performs the same function as the CrtUsrSpace procedure, but does it from within CL. Alternatively, you can call the QUSCRTUS API followed by the QUSCUSAT API to accomplish the same thing.
Once the user space has been created, and the source member listed in Figure 1 is compiled, use the STRDBG command to set a break point on the last RETURN opcode (last line of code). Once the breakpoint is set, exit the debugger using F12 and then call the program. When the line containing the breakpoint is about to be run, the debugger will stop and display the source on the screen. At this point, place the cursor on the "dynArr(1700)" variable displayed in the source window and press F11. You should see "Hello World" in that element of the array.
If you do the math, you'll see that the size of an array element (200 in the example in Figure 1) multiplied by the element number (1,700) comes to 340,000. This means that 340,000 bytes of storage would have been required in order for the array to successfully provide 1,700 elements. But since we are using a user space, we avoided any hand-coded allocation schemes and are indifferent about the number of elements we use.
When I created a 32-byte user space and ran this program, the size of the user space automatically grew to 344,064 on my machine.
|
Figure 1: DYNOARR is a test program to prove dynamic array size theory.
Create User Space Made Easy
In order to create an extendable user space, two APIs must be called: QUSCRTUS (Create User Space) and QUSCUSAT (Change User Space Attributes).
The QUSCRTUS API creates a fixed-length user space at the size specified and allows things like the object attribute and text to be applied. The QUSCUSAT API allows you to change some of the attributes of the user space, including the current size, the initial value (a single character repeated in each byte of the user space), and the extendability option. For some reason QUSCRTUS does not include a parameter that allows the extendability option to be specified, so QUSCUSAT must also be called.
In the RPG ToolKit for OS/400, there are procedures that allow you to easily create, change, and delete user spaces from within RPG. In addition, there are extra commands included, such as CRTUSRSPC, DLTUSRSPC, and CHGUSRSPCA. To provide this capability, I have reproduced the CRTUSRSPC command here, along with the CPP. Essentially, I have expanded the code by removing the calls to the ToolKit procedures and replacing them with calls to the OS/400 APIs mentioned above. So the ToolKit is not required to create user spaces on your system.
Listed in Figure 2 is the command definition source for the CRTUSRSPC CL command. The only required parameter is the first one, USRSPC (user space name). To test the dynamic array size theory, however, you want to make sure you specify the size at something like 32 bytes, rather than the 32k default value. For example, the following CRTUSRSPC command creates a user space named DynoRama in QTEMP with a size of 32 bytes and makes it auto-extendable.
|
Figure 2: This is the command definition source for the CRTUSRSPC command.
To compiler the command definition source listed in Figure 2, specify the following CRTCMD command:
CRTCMD CMD(CRTUSRSPC) PGM(mylib/RTKUSRSPC)
Be sure to replace MYLIB with the name of the library where you've compiled the RTKCRTUS program.
The source code listed in Figure 3 is the CPP for the CRTUSRSPC command. The first few dozen lines are declarations, prototypes for the APIs that are called, and the procedure interface for the program itself. Note that I avoid using the outdated *ENTRY/PLIST opcodes and instead use a procedure interface.
The RTKCRTUS program is fairly straight forward; it calls just two APIs: QUSCRTUS to create the user space and then QUSCUSAT to set the auto-extendability attribute for the user space.
Before running the DYNARR program from Figure 1, be sure to compile and run the CRTUSRSPC command to create the user space.
|
Figure 3: Here's the RPG IV source for the RTKCRTUS program of the CRTUSRSPC command.
LATEST COMMENTS
MC Press Online