Fundamentals: Parameter Passing
November 29, 2016 Jon Paris
Even though high-level languages make it unnecessary–and in many cases, difficult–to understand what is going on “under the covers,” I have always found a basic knowledge of internal processes to be invaluable. This is particularly true when it comes to resolving mystery bugs. I was reminded of this recently by a number of problems related to parameter passing that appeared on Internet lists and also in my email directly from customers.
My need to understand internals is due, at least in part, to the fact that I learned assembler and other low-level languages at an early stage in my career. Actually, the very first thing I learned was how to program calculators and tabulators using plug boards. With those things you had to understand the mechanics of what was going on!
I’m going to start my discussion of parameter passing from the beginning, so apologies to those readers who already know some of this stuff.
The most important thing to remember is that when you pass a parameter from one program to another, you are not passing any data. What you are passing is a pointer to (i.e., the memory address of) the first byte of the parameter’s storage in the originating program. And that is all that you are passing.
This is important to understand because it means that it is the receiving program that effectively defines what that parameter looks like in terms of data type and length, and that can cause some “interesting” problems if you are not careful. Let’s look at a simple example to see what I mean.
(A) Dcl-pr CallTgt1 extPgm; request char(4) Const; result char(10); End-Pr; (B) Dcl-ds data; parmData10 char(10) Inz('Input Parm'); moreData char(30) Inz('Original value of moreData'); End-Ds; Dcl-s wait char(1); (C) Dsply ( 'parmData10 = [ ' + parmData10 + ' ]' ); Dsply ( 'moreData = [ ' + moreData + ' ]' ); (D) CallTgt1 ( 'Fill': parmData10 ); (E) Dsply ( 'parmData10 = [ ' + parmData10 + ' ]' ); Dsply ( 'moreData = [ ' + moreData + ' ]' ); Dsply 'Press enter to continue' ' ' wait; *InLR = *On;
In the code above, notice that at (A) you can see the prototype for CALLTGT1, the program we are going to call. This program will change the content of the second parameter. Notice that the second parameter is defined as a 10-character variable.
At (B) you can see the definition of the variable parmData10, which will be passed (D) as that second parameter. Before we do the call, we display the content of both parmData10 and the variable moreData so that we can check their content.
Following the call at (D) we then display the content of the two variables again. Here are the results of running the program.
DSPLY parmData10 = [ Input Parm ] DSPLY moreData = [ Original value of moreData ] DSPLY parmData10 = [ Fill ] DSPLY moreData = [ alue of moreData ]
As expected, the content of parmData10 has been changed. But notice that so has the content of the variable moreData. How could that have happened?
You’ll hopefully see the problem when you look at the source of the program being called. Here it is:
pgm ( &input &output ) dcl &input *char 4 dcl &output *char 20 chgvar var(&output) value(&input) endpgm
See a problem? Yup! The parameter defined as 10 characters in the calling program is defined as 20 long in the receiving program. So, when the four character &input variable is moved to &output, instead of six spaces being added as padding, as the programmer might be expecting, a total of 16 are added, with the result that the first 10 characters of the variable moreData are overwritten because moreData has the misfortune of following parmData10 in memory.
Sadly, spotting this kind of corruption is rarely this easy. Just to demonstrate the point, try making this simple change to the calling program. Reverse the order of the variables moreData and parmData10 in the data data structure (DS). The DS now looks like this:
Dcl-ds data; moreData char(30) Inz('Original value of moreData'); parmData10 char(10) Inz('Input Parm'); End-Ds;
What do think will happen now? Well in this case the program runs successfully and the results look like this:
DSPLY parmData10 = [ Input Parm ] DSPLY moreData = [ Original value of moreData ] DSPLY parmData10 = [ Fill ] DSPLY moreData = [ Original value of moreData ]
Looks good, right? No corruption in sight. Except we know that corruption must still be occurring! So where did those extra 10 spaces go? Your guess is as good as mine! Truth is that we have no clue exactly what data is now being overwritten. The RPG compiler places variables in memory in what, from our perspective, is a completely arbitrary sequence. In the original example I deliberately placed the variables in a DS because that is the only time that you can guarantee exactly where in memory variables are located relative to one another and therefore observe the corruption taking place. But even then, there is no way to know what follows the end of the DS.
It is important to understand this because it can be really hard to diagnose the results of such errors. I have seen cases where the corrupted variables were subsequently written back to the database by an UPDATE operation, only to be discovered weeks later, forcing a complete rebuild of the history file they were part of.
In another case, the corruption was originally to a print buffer but because the print line was assembled after the corruption had occurred, the program apparently ran correctly. And kept on running for many, many years. Then one day it had to be recompiled. The changes made by the programmer were trivial and had no impact on the memory layout, but in the interim period the compiler folks had changed the way that variable storage was generated. The result was that all of a sudden, the corruption was to important internal program control pointers, and a few minutes into the program run, “BOOM!”
So how do we avoid such errors? Well the answer is the use of prototypes or, to be more precise, accurate prototypes. The code I showed here is typical of what I often see on customer sites. The prototype, at label (A), was apparently written by the programmer who wrote the program that uses it. The clue is in the fact that the prototype was hard coded in the source, whereas it should really be coming in via a /COPY directive. If you ever see a hard-coded prototype in an RPG source, be suspicious–be very suspicious–and get rid of it as soon as possible.
That /COPY source should have been written by the programmer who coded the CL routine being called. When calling CL programs, we really have no choice but to manually ensure that the prototype matches the actual parameters used by the called program. For C functions and system APIs there are IBM-supplied prototypes, but if, like me, you don’t like the style and naming conventions of those, then a quick Internet search will often locate excellent examples written by others.
When the prototype relates to an RPG program, things are better. By coding the /COPY in the called program and in all the calling programs, we can ensure that the prototype is a valid representation of the interface. When both are present, the compiler will compare the prototype (PR) and procedure interface (PI) and fail the compile if they don’t match. While we love the recent relaxation of the compiler’s rules on prototypes (i.e., that we no longer need to code prototypes for internal subprocedures), it has a regrettable side effect; namely that prototypes are no longer necessary in any program that has a PI defined. As a result, you can get away with not /COPYing in the prototype, but resist the temptation. Let the compiler validate the prototype you plan to use and you’ll avoid problems down the road.
Externalizing and /COPYing prototypes may seem like extra work up front. But while you’re doing it, just imagine how many future bugs you may be preventing! COBOL and CL programmers, I’m afraid you are on your own, so be careful out there!
Jon Paris is one of the world’s most knowledgeable experts on programming on the System i platform. Paris cut his teeth on the System/38 way back when, and in 1987 he joined IBM’s Toronto software lab to work on the COBOL compilers for the System/38 and System/36. He also worked on the creation of the COBOL/400 compilers for the original AS/400s back in 1988, and was one of the key developers behind RPG IV and the CODE/400 development tool. In 1998, he left IBM to start his own education and training firm, a job he does to this day with his wife, Susan Gantner–also an expert in System i programming. Paris and Gantner, along with Paul Tuohy and Skip Marchesani, are co-founders of System i Developer, which hosts the new RPG & DB2 Summit conference. Send your questions or comments for Jon to Ted Holt via the IT Jungle Contact page.