|
RPG Prototyping
by Ted Holt
Would you rather find an error in a program at compile time or at
runtime? If you said
compile time, read on. If you said runtime, either find yourself
another profession or
invest in a good set of ear plugs to block out all the complaints
you'll be getting from
your users!
I hate to get a call from a user because of a runtime error. It
ruins my day because I've got
to drop whatever I'm working on, figure out what happened, and then
figure out how to
recover from the error. I've got better things to do. I'd prefer
to find the error at
compile time.
One of the ways I convert some runtime errors into compile-time
errors is through prototyping. Prototyping allows the compiler to
verify that the parameters that will be passed to a called program
or procedure are acceptable--that is, that they are defined appropriately.
Prototyping has several advantages, but in this article I only
discuss how prototyping can convert runtime errors into compile-time
errors.
Giving the Compiler More Information
A prototype tells the compiler how the parameters of a called
program or procedure are defined. The program may be an OPM program
or an ILE program. It doesn't matter which language it was written
in; you can prototype any *PGM object.
For example, suppose I have a CL program that defines two parameters--an
option and a five-digit packed-decimal return code--like this:
PGM PARM(&OPTION &RETURNCODE)
DCL VAR(&OPTION) TYPE(*CHAR) LEN(1)
DCL VAR(&RETURNCODE) TYPE(*DEC) LEN(5)
IF (&OPTION *EQ A) +
(CHGVAR VAR(&RETURNCODE) VALUE(-25))
ELSE IF (&OPTION *EQ B) +
(CHGVAR VAR(&RETURNCODE) VALUE(5))
ELSE +
(CHGVAR VAR(&RETURNCODE) VALUE(25))
RETURN
ENDPGM
To invoke this CL program from an RPG IV program, I use the CALL op code. What
happens if I don't properly define the return code? What if, for example, I define it as a
three-digit number rather than a five-digit number?
D OPTION S 1
D RTNCODE S 3 0 C EVAL OPTION = 'C'
C CALL 'YPGM01CL'
C PARM OPTION
C PARM RTNCODE
C IF RTNCODE < *ZERO
You guessed it: My RPG IV program blows up with a decimal data error (RNQ0907)
when I use the return code field in the IF operation.
I can avoid this problem by creating a prototype. In the prototype, I indicate how the
parameters are defined.
D YPGM01CL pr extpgm('YPGM01CL')
D Option 1a
D ReturnCode 5p 0
I store this prototype in its own source member. In this case,
assume I store the prototype in member YPGM01CL, in source physical
file PROTOTYPES, somewhere in my library list. You don't have
to store the prototypes in a copy book. But by doing so, you have
an easy way to reuse the prototype in other programs without retyping
it each time.
Compare the prototype to the CL code above, and you will see
that the two parameters are correctly defined--a one-byte character
value and a five-digit packed-decimal value.
Notice the EXTPGM (external program) keyword. This tells the
name of the program that will be called.
Here is the same RPG caller, changed to use the prototype.
/copy prototypes,ypgm01cl
D OPTION S 1
D RTNCODE S 3P 0
C EVAL OPTION = 'C'
C CALLP YPGM01CL (OPTION : RTNCODE)
C IF RTNCODE < *ZERO
The /copy directive copies the prototype into the member so that
it can be compiled as part of the program.
Notice that the CALL op code has been replaced by CALLP. The
parameters are listed in the parameter list of CALLP. Both option
and return code fields are defined as they were in the previous
example.
When I try to compile this program, I am stopped by error RNF7535
(The type and attributes of the parameter do not match those of
the prototype). The compiler generates this error because the
second parameter is defined as a 3-digit packed-decimal field,
but the prototype I copied into the program, via the copy book,
specifies that the called program is expecting a 5 position packed-decimal
field. So, instead of a runtime error, I now have a compile-time
error. And if you're like me, you'd rather catch the errors yourself
at compile time rather than have the user catch them at runtime.
Now, I hear some of you objecting. You're saying, "I would catch
an error like this when testing." I agree fully. With short, simple,
little programs like these, you would. But how many programs do
you have that are as simple as this example? What about a caller
of several hundred or maybe even thousands of lines of code, with
plenty of calls, each one with a list of parameters? What if you
decide to enlarge a field that is used as one of those parameters?
Is there a chance you might overlook that the field whose size
you're changing is used as a parameter for a CALL, and that the
called program is not going to be changed?
How can you be sure that your testing will catch the problem?
In my little example, the return code field is used immediately
upon return to the caller. What if the return code is buried in
conditional logic that gets executed only once in a blue moon?
What's likely to happen is that, days or weeks or months later,
you'll get a call from a user with a runtime error. Good luck
trying to find the bug then!
Before you get the impression that this method is foolproof,
let me give you a warning. You, the programmer, have to be sure
that you code the prototype correctly. If I had defined the second
parameter in the prototype as a three-digit field, the modified
RPG program would have compiled, because the second parameters'
attributes would have matched; however, I still would have gotten
a runtime error, because neither of the defined second parameters
matched the CL program's second parameter. This is why I placed
the prototype in a source member and /copy'd it into the calling
program. Every RPG IV caller uses /copy and gets the same prototype.
This reduces the chance that one or more callers will have an
error in the prototype.
Calling a Module
If you've built modules with commands like CRTRPGMOD, CRTCLMOD,
CRTCBLMOD, and CRTCMOD, you're accustomed to calling them with
the CALLB op code. Just as you can with CALL, you can replace
CALLB with CALLP.
Here is the source code for RPG module YMOD01RG, which is entrusted
with the important task of doubling a number and adding 1.23 to
it.
D number S 11p 2
C *entry plist
C parm number C*
C Eval Number = (Number * 2) + 1.23
C*
C return
To call this module the old way, you use CALLB.
D SomeNumber s 11 2
C eval SomeNumber = 265.21
C callb 'YMOD02RG'
C parm SomeNumber
But you can prototype it instead.
D WeirdCalc pr extproc('YMOD02RG')
D ANumber 11 2
Notice I've used the EXTPROC (external procedure) keyword instead of EXTPGM. I
used EXTPROC because YMOD02RG is a module object, not a program object.
Notice also that I did not use the module name in column 7 of the first D-spec. I could
have, but I don't have to. A prototype does not have to have the same name as the
external program or procedure to which it refers. So, if you have an RPG program called
CA1255 for file maintenance over the customer master file, you can call it
ChangeCustomers or CustomerMaint, or whatever you like.
Now, instead of CALLB, use CALLP to invoke the module.
/copy prototypes,ymod02rg
D SomeNumber s 11 2
C eval SomeNumber = 265.21
C callp WeirdCalc (SomeNumber)
Prototyping Is Good
If you're an RPG programmer, you probably have rules that you code by, such as never
use a conditioning indicator in calculation specs, or never use the RPG cycle. Consider
adding another rule: Never use a CALL or CALLB operation in an RPG IV program.
Prototype your calls from now on, as a way to prevent runtime errors.
In my next article, I will continue my discussion on prototyping. I'll teach you how to
write internal subprocedures and offer my opinion about why it's better to use internal
subprocedures rather than executing a subroutine.
Ted Holt is a consultant who lives in northeast Mississippi. He edits the OS/400
edition of the Midrange Guru
newsletter. You can contact Ted at tholt@itjungle.com.
|