The Basics Of Memory and Addresses
Every computer contains "storage" that is used to store programs, files,
variables, etc. This storage (which we will also be referring to as "memory",
even though it could actually be disk space on any modern operating system) is
usually numbered, so that each byte within it has a unique address.
When you run a program, it needs to use space in memory to put things like
procedures, variables, etc. Each job running the program (or any other
program) needs its own unique space in memory that won't conflict with
every other job running every other program. It therefore is up to the
operating system to assign valid areas of memory when requested.
The process of requesting that memory be made available to the program is
called "allocating memory". A typical scenario works something like this:
- A program is called.
- When the program needs a place to store a variable, it asks the operating system, saying something like "I need 10 bytes of memory"
- The operating system replies with something like "You can use 10 bytes of
memory beginning at location 71533!"
- The program continues running, and when its done, says to the operating
system "I'm done with the memory at location 71533"
A Simple Example
Here is a very simple example of pointers in RPG IV:
D Var1 S 12A
D Ptr S *
D Var2 S 12A based(Ptr)
c eval Ptr = %addr(Var1)
** This displays "Hello World!"
c eval Var1 = 'Hello World!'
c dsply Var2
** This displays "World Hello!"
c eval Var2 = 'World Hello!'
c dsply Var1
c eval *inlr = *on
c return
First, look at the D-Specs. "Var1" is declared as an ordinary, 12-char long
variable. The compiler will automatically generate code to allocate memory
to this location when the program runs. Var2 is also a 12-char long variable,
but this time we've told it that Var2 is based on a pointer called "Ptr".
Because of this, no memory is allcated to Var2 when the program starts, and
Var2 will always occupy the area of memory thats pointed to by the pointer
called Ptr.
First first line in the C-specs puts the address of the Var1 variable
into the pointer called Ptr. Since Var2 is based on Ptr, it effectively
moves the location in memory of Var2 to be the same as Var1!
To demonstrate this, we assign a value of "Hello World!" to Var1. Note that
when we display Var2, it also says "Hello World!". We further demonstrate it
by changing the value of Var2 to be "World Hello!", once again, we see that
this change also affected Var1.
Pointer Arithmetic
As demonstrated in the previous example, its easy to put the address of
another variable into a pointer. But what if there isn't already a variable
in the area of memory that we want to point to? What if we want to move
through a list of data? Thats where pointer arithmetic comes in.
As I write this, I'm still using V3R2 of OS/400. In V3R2, you can't do
direct pointer math, you have to set your pointer equal to an address of
another variable! I created a subprocedure that will allow me to add an
arbitrary value to my pointer using an array. Here it is:
Member: QRPGLESRC,OFFSET_H:
D OffsetPtr PR *
D pePointer * Value
D peOffset 10I 0 Value
Member: QRPGLESRC,OFFSETR4
H NOMAIN
D/COPY QRPGLESRC,OFFSET_H
P*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
P* Return a pointer at a specified offset value from another ptr
P*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
P OffsetPtr B EXPORT
D OffsetPtr PI *
D pePointer * Value
D peOffset 10I 0 Value
D p_NewPtr S *
D wkMove S 1A DIM(4097) BASED(p_NewPtr)
c eval p_NewPtr = pePointer
c if peOffset > 0
C dow peOffset > 4096
C eval p_NewPtr = %addr(wkMove(4097))
c eval peOffset = peOffset - 4096
c enddo
C eval p_NewPtr = %addr(wkMove(peOffset+1))
c endif
c return p_NewPtr
P E
In this example, parameters begin with the prefix "pe", pointers with the
prefix "p_" and internal work variables with the prefix "wk".
Here's what it does:
- Takes a source pointer, and bases a 4097 byte array on it.
- If the amount to move the pointer in memory is more than 4096, it moves
the array 4096 bytes forward in memory. It does this until theres less than
4096 bytes left to move the pointer.
- Finally, it uses the array to position the new pointer to point to a
the desired area of memory.
If you're using a newer release of OS/400 that can do direct pointer math,
you don't need to use the OffsetPtr subprocedure. Instead, you can just
do something like this:
c eval p_NewPtr = pePointer + peOffset
In my examples, however, I'll be using the OffsetPtr subprocedure because
I'm using V3R2 and can't test it, otherwise :)
To Compile the OffsetPtr service program:
- CRTRPGMOD MODULE(OFFSETR4) SRCFILE(*LIBL/QRPGLESRC) DBGVIEW(*LIST)
- CRTSRVPGM SRVPGM(OFFSETR4) EXPORT(*ALL)
A More Practical Example: A trigger with an external record format
In this example, I'm going to load an externally defined record format into
a data structure that's based on a pointer. I'll then use my pointer to put
that data structure into the area of memory thats required to use it in a
trigger program. Finally, I'll use this record image to update the
"record maintenance date" and "maintenance user" fields in the file.
This example uses the OffsetPtr service program that we just created. It also
uses a file from my own system called "CUSTMAS" (our Customer Master file) to
make this work on your system, you'll need to change the references to
CUSTMAS to some externally described file that you use in your shop, and
change "cuMDat" and "cuMUsr" to the fields for date & userid respectively.
Source Member: QRPGLESRC,SAMPTRIGR4
D/COPY QRPGLESRC,OFFSET_H
D AFTER C CONST('1')
D BEFORE C CONST('2')
D INSERT C CONST('1')
D DELETE C CONST('2')
D UPDATE C CONST('3')
*** "Before" image of the record buffer
D dsBefore e ds EXTNAME(CUSTMAS)
D BASED(p_Before)
D PREFIX(B_)
D p_Before S *
*** "After" image of the record buffer
D dsAfter e ds EXTNAME(CUSTMAS)
D BASED(p_After)
D PREFIX(A_)
D p_After S *
*** Trigger information passed by the operating system
*** as a parameter to this program.
D dsTrg ds
D dsTrgFile 1 10A
D dsTrgLib 11 20A
D dsTrgMbr 21 30A
D dsTrgEvent 31 31A
D dsTrgTime 32 32A
D dsTrgCmtLk 33 33A
D dsTrgFill1 34 36A
D dsTrgCSID 37 40I 0
D dsTrgFill2 41 48A
D dsTrgBOff 49 52I 0
D dsTrgBLen 53 56I 0
D dsTrgBNOff 57 60I 0
D dsTrgBNLen 61 64I 0
D dsTrgAOff 65 68I 0
D dsTrgALen 69 72I 0
D dsTrgANOff 73 76I 0
D dsTrgANLen 77 80I 0
D dsTrgFill3 81 96A
D peLength S 10I 0
D wkDateFld S D
D psds SDS
D UserID 254 263A
C *ENTRY PLIST
C PARM dsTrg
c parm peLength
**************************************************************
** Point the "before buffer" and the "after buffer" to the
** external record layouts, as appropriate.
**************************************************************
c if dsTrgEvent = UPDATE
c or dsTrgEvent = DELETE
c eval p_Before = OffsetPtr(%addr(dsTrg):
c dsTrgBOff)
c endif
c if dsTrgEvent = UPDATE
c or dsTrgEvent = INSERT
c eval p_After = OffsetPtr(%addr(dsTrg):
c dsTrgAOff)
c endif
**************************************************************
** Change the after buffer to contain the current date and
** the user who made the change.
**************************************************************
c if dsTrgEvent = UPDATE
c or dsTrgEvent = INSERT
c eval A_cuMDat = *DATE
c eval A_cuMUsr = UserID
c endif
c return
To Compile & Attach to file:
- CRTRPGMOD MODULE(SAMPTRIGR4) SRCFILE(*LIBL/QRPGLESRC) DBGVIEW(*LIST)
- CRTPGM PGM(SAMPTRIGR4) BNDSRVPGM(OFFSETR4)
- ADDPFTRG FILE(CUSTMAS) TRGTIME(*BEFORE) TRGEVENT(*INSERT) PGM(SAMPTRIGR4) ALWREPCHG(*YES)
- ADDPFTRG FILE(CUSTMAS) TRGTIME(*BEFORE) TRGEVENT(*UPDATE) PGM(SAMPTRIGR4) ALWREPCHG(*YES)
How it works:
If you're not already familiar with trigger programs, you may want to peruse
the IBM manuals and other books that will explain that part to you. My goal
here is only to explain how the pointer aspect of this program works.
In the D-specs we pull the external definition of the CUSTMAS file into
a data structure. To accomodate the (future) possibility of wanting to
compare data in the "before" and "after" record images, I've also prefixed
the data strucutres with "B_" and "A_" respectively.
The dsTrg data structure contains the various pieces of information that we'll
get passed as a parameter from the operating system when the trigger is called. This includes an "offset" from the start of the data structure to the "before record buffer" and also includes an "offset" from the start of the data structure to the "after record buffer".
This program then changes the pointer that the "before" record image is based on to the address in memory that is "dsTrgBOff" bytes later in memory than the start of the dsTrg data structure, and does a similar thing with the "after" record image.
Finally, we're able to make changes to the after record image, and exit.
Processing a list API
Arguably the most common use for pointers in RPG IV is when reading the output
of an API. The "List API's" output their data to a user space. The output is
somewhat standardized, so that it will contain: "A user area", "A generic header", "an input parm section", "a header section" and a "list data section."
The first time that I looked at a list API, I remember thinking "you've got to
be kidding me! Thats way too complicated!", but as it turns out, its not so bad! The "user area" is only 64 bytes long, and I have not yet found a need to
reference the data stored in it. The "generic header" contains little more
than offsets to the "input section", "header section" and "list section",
along with sizes. Allowing us to skip the "input section" and "header section" if we don't need them. (which we don't for this example!)
In this example, we will list all of the members in a physical (or source
physical) file using the QUSLMBRL API. The "sections" that we wish to deal
with in this example will be the "generic header" to get our offsets from,
and the "list section" to get the details from. We'll also code a "user area",
and just put it into our "generic header" structure, since this simplifies the
program significantly.
This example will also use a few other API's. The QUSCRTUS API will be used
to create a user space to output to, the QUSPTRUS API will be used to get
a pointer to the user space, so we can manipulate the data.
You'll note that I'm glossing over a lot of information about the API's
themselves. Please bear in mind that explaining API's is not part of the
scope of this document. This is, after all, intended to explain how pointers
work!
Source Member: QRPGLESRC,LISTMBRR4
******************************************************************
* API Error Code Structure -- used with most non-bindable API's
******************************************************************
D dsEC DS
D* Bytes Provided (size of struct)
D dsECBytesP 1 4B 0 INZ(256)
D* Bytes Available (returned by API)
D dsECBytesA 5 8B 0 INZ(0)
D* Msg ID of Error Msg Returned
D dsECMsgID 9 15
D* Reserved
D dsECReserv 16 16
D* Msg Data of Error Msg Returned
D dsECMsgDta 17 256
******************************************************************
** List Header (Generic Header for List API's)
******************************************************************
D p_GenHdr S *
D dsLH DS BASED(p_GenHdr)
D* User Area
D dsLHUsrAra 64A
D* Size of Generic Header
D dsLHSize 10I 0
D* Data structure release & level
D dsLHDsLvl 4A
D* Format Name
D dsLHFormat 8A
D* API that generated this
D dsLHAPI 10A
D* Date & Time generated
D dsLHCrtDT 13A
D* Status (I=Incomplete,C=Complete
D* F=Partially Complete)
D dsLHStatus 1A
D* Size Of User Space Used
D dsLHSpcSiz 10I 0
D* Offset to input parms section
D dsLHInpOff 10I 0
D* Size of input parms section
D dsLHInpSiz 10I 0
D* Header section offset
D dsLHHdrOff 10I 0
D* Header section size
D dsLHHdrSiz 10I 0
D* List section offset
D dsLHLstOff 10I 0
D* List section size
D dsLHLstSiz 10I 0
D* Count of Entries in List
D dsLHEntCnt 10I 0
D* Size of a single entry
D dsLHEntSiz 10I 0
D* CCSID of data in list section
D dsLHCCSID 10I 0
D* Country ID
D dsLHCntry 2A
D* Language ID
D dsLHLang 3A
D* Subsetted List Ind
D dsLHSubSet 1A
D* Reserved
D dsLHReserv 42A
******************************************************************
* Prototypes:
******************************************************************
* Create User Space API
*
D CrtUsrSpc PR ExtPgm('QUSCRTUS')
D UsrSpc 20A CONST
D ExtAttr 10A CONST
D InitSiz 10I 0 CONST
D InitVal 1A CONST
D PubAuth 10A CONST
D Text 50A CONST
D Replace 10A CONST
D ErrorCode 256A
* Retrieve Pointer to User Space API
*
D RtvPtrUS PR ExtPgm('QUSPTRUS')
D UsrSpc 20A CONST
D PtrUsrSpc *
* List Members (QUSLMBR) API
*
D ListMbrs PR ExtPgm('QUSLMBR')
D UsrSpc 20A CONST
D Format 8A CONST
D DataBase 20A CONST
D Member 10A CONST
D Override 1A CONST
D ErrorCode 256A
* OffsetPtr service program:
*
D/COPY QRPGLESRC,OFFSET_H
******************************************************************
* Local variables:
******************************************************************
D p_MbrName S *
D MbrName S 10A BASED(p_MbrName)
D Offset S 10I 0
D Msg S 50A
*** Create the User Space QTEMP/MBRDATA
C callp CrtUsrSpc('MBRDATA QTEMP':'USRSPC':
C 1:x'00':'*ALL':'List of Members in File':
c '*YES': dsEC)
c if dsECBytesA > 0
c eval Msg = 'Error ' + dsECMsgID + ' calling -
c QUSCRTUS API!'
c dsply Msg
c eval *inlr = *on
c return
c endif
*** Ask the List Members API to put a list of members in the
*** QRPGLESRC file into the desired user space:
C callp ListMbrs('MBRDATA QTEMP':'MBRL0100':
C 'QRPGLESRC *LIBL':'*ALL':'0':dsEC)
c if dsECBytesA > 0
c eval Msg = 'Error ' + dsECMsgID + ' calling -
c QUSLMBR API'
c dsply Msg
c eval *inlr = *on
c return
c endif
*** Get a pointer to the start of the user space.
C callp RtvPtrUS('MBRDATA QTEMP':p_GenHdr)
*** Loop through the list of members, each time changing the
*** p_MbrName pointer to point to the next member in the list
C do dsLHEntCnt Entry 4 0
c eval Offset = ((Entry-1) * dsLHEntSiz)
c + dsLHLstOff
c eval p_MbrName = OffsetPtr(p_GenHdr: Offset)
c MbrName dsply
c enddo
C eval Msg = 'Press ENTER to end program'
c dsply Msg
*** End Program
c eval *INLR = *On
c Return
To Compile:
- CRTRPGMOD MODULE(LISTMBRR4) DBGVIEW(*LIST)
- CRTPGM PGM(LISTMBRR4) BNDSRVPGM(OFFSETR4)
How It Works:
The line with "callp CrtUsrSpc" on it calls the program identified by the
CrtUsrSpc prototype, which is the QUSCRTUS API. This API will create a user
space thats suitable to receive the output of the "List Members (QUSLMBR)" API.
Then, we check to make sure the API was successful by checking to see if
any bytes are now in the error code data structure.
Our next step is calling the QUSLMBR API itself. We tell it the name of the
user space that we just created, so it knows where to put the data. We also
tell it that we want a list of "*ALL" members in the file called
"*LIBL/QRPGLESRC". If any error occurs here, there will be bytes in the
error code data structure. So, next we check for that, and complain if we
get an error!
Finally, we get a pointer to the user space by calling the QUSPTRUS API. This
API will be assigning a memory address to the p_GenHdr pointer. Since the
List Header Data Structure (dsLH) is based on the p_GenHdr pointer, it will
actually occupy the same area of memory as the start of the user space does.
This is very useful to us, because now we can read the values of the generic
header for the user space directly from the dsLH data structure!
To get an actual list of members in the user space, we set up a loop that
goes through each entry of the list. For each entry, we offset the pointer
called "p_MbrName" to the position within the user space that contains the
member name that we want. Since the "MbrName" variable is based on the
p_MbrName pointer, we'll be able to display the member name directly from
that variable.