Guru: Parameter Passing Fundamentals Of Programs Versus Procedures
February 20, 2017 Jon Paris
In my earlier tip, Fundamentals: Parameter Passing, I discussed the basics of parameter passing and the importance of ensuring that parameter lengths and data types match those expected by the called routine. In this tip I want to go one step further and discuss differences between passing parameters to programs and to procedures.
Let’s start with passing parameters between programs. In particular, I will focus on the impact of passing fewer parameters than the called program is expecting because this is one area in which programs and procedures differ, and a failure to understand that can lead to some “interesting” situations.
For example, let’s say I had a program that took a single parameter and performed a specific operation on the data. Now suppose that I am asked to modify that program so that it performs one of a number of different operations on the data. The actual operation to be performed will be specified by a second parameter.
This story contains code, which you can download here.
No problem, but won’t I have to go back and change every single one of the many programs that currently call the routine? Luckily, I don’t have to. As shown below, I can simply modify the prototype for the called program to define the second parameter as optional by specifying Options(*NoPass).
// Prototype CALLTGT3PR - used for program CALLTGT3 dcl-pr CallTgt3 ExtPgm; output char(20); request char(4) Options(*NoPass); // request parm is optional End-Pr;
Existing programs that only pass one parameter will continue to work as long as I make a simple change in the called program (CALLTGT3) to arrange for the original behavior to be the default.
Before I describe that change though, take a look at this initial version of CALLTGT3. As you can see this doesn’t do anything useful – it just serves to help illustrate the situation.
/Copy ITJTIPSRC,CallTgt3Pr dcl-pi CallTgt3; output char(20); request char(4) Options(*NoPass); End-Pi; Dsply ( 'Request value is: ' + request ); *InLR = *On;
Now put your thinking cap on and decide what you think what will happen if I run the code below to call the program.
CallTgt3 ( Result: request ); CallTgt3 ( Result );
If your answer is that the first call will work, but that the second call will blow up with a pointer error when the parameter field request is referenced by the Dsply operation, congratulations! You are 100% correct. But what if I were to switch things up a bit and make the call target a subprocedure rather than a program, like this?
Dcl-s request char(4) Inz('Low'); ..... CallTgt5 ( Result: request ); CallTgt5 ( Result ); *InLR = *On; // Internal procedure CallTgt5 dcl-proc CallTgt5; dcl-pi CallTgt5; output char(20); request char(4) Options(*NoPass); End-Pi; Dsply ( 'Request value is: ' + request ); End-proc;
Do you expect the same thing to happen on the second call; i.e. a pointer error? If you answer “Yes” to that question then you are wrong. In fact, the real answer is: “Only if you are very lucky.” A “lucky” error? Yes, indeed, because if there is no error signaled, your code will execute but the value in the request parameter could be anything! Literally anything! In this example, I placed the two calls, one after the other, with the result that the second call “sees” the same second parameter as the first call – i.e., the contents of the original request field (containing the value ‘Low’). As you’ll see in a moment, the fact that this “works” is basically just luck. Put a call to a different procedure between the two and you’ll see the difference. I should also point out here that this effect does not depend on my having called the same procedure. Any procedure call involving parameter(s) would have a similar effect. Any procedure call – no matter whether the procedure was written in RPG, COBOL, or indeed was an IBM supplied API.
Why The Difference?
So why is this? Basically the mechanism used by the system to pass parameters differs significantly between dynamic program calls and bound procedure calls. For a program call, the calling program passes a set of parameter pointers to the operating system which stores them away. The called program then retrieves these parameter pointers by calling an operating system API. That API ensures that valid pointers are returned for the passed parameters, and any parameters requested beyond the number passed are returned as null (i.e., invalid) pointers. This is an oversimplification of the process, but it is accurate enough for our purposes.
When it comes to procedure calls, the mechanism is completely different. In effect the caller pushes its parameter pointers onto a stack, and the caller pulls off that stack the number that it needs. Again an over simplification, but. . . consequently, if the caller only pushes one pointer onto the stack and the called procedure pulls two, there is no operating system mechanism in play to ensure that the second one is null. In fact, as our demonstration program shows, the second “pointer” will be whatever was left there by an earlier call! I will leave it to your imagination to think of the havoc that inadvertently changing such values might cause. Note: I put the word “pointer” in quotes because what we will actually get are the next 16 bytes on the stack – which may not be a pointer at all; they could represent data passed by value on a previous call to a completely different procedure. In this case we would indeed be “lucky” because the procedure would blow up with an invalid pointer error!
Why did IBM implement it in this way? Speed. The number of instructions required to pass and retrieve parameters on a procedure call is a minuscule fraction of that required on a similar program call. And ILE was initially all about improving the call speed of modern highly modular applications. With today’s super-fast systems, that speed gain is perhaps less important than it was back in the early RISC processor days. Nevertheless, that was one of the basic reasons behind the differences.
Fixing The Problem
So how do we provide the parameter versatility we seek while ensuring that we practice safe hex? The answer is simple: The %Parms built-in function is your friend, aided and abetted by his sibling %ParmNum(). Since the last code sample I showed above used an internal subprocedure, let’s fix the problem in that one.
This code demonstrates my preferred approach, since it reduces the amount of duplicated logic that might otherwise be required, but of course it is not the only way.
// Modified version of Internal procedure CallTgt5 dcl-proc CallTgt5; dcl-pi CallTgt5; output char(20); request char(4) Const Options(*NoPass); End-Pi; (A) dcl-s req char(4) Inz('High'); // Default value for parm (B) If %Parms >= %ParmNum ( request ); req = request; EndIf; (C) Dsply ( 'Request value is: ' + req ); End-proc;
At (A) I have defined the field req that will be used internally to control the operation. It is initialized to the default value of ‘High’.
At (B) I test to see if the second parameter was actually passed or not by comparing the value in %Parms with the parameter number of the request parameter as retuned by %ParmNum(). %Parms returns the total number of parameters that were passed on this call and %ParmNum() returns the ordinal position in the parameter list of the named parameter; i.e., in this case %ParmNum (request) returns a value of 2. If this test indicates that the parameter was indeed passed, then its value is copied into the internal version, overwriting the default value.
%ParmNum() is a great addition to the language as it helps to bulletproof my code by removing the need for hard-coded values. Prior to its introduction I would have had to have coded:
If %Parms >= 2;
But whenever I needed to add an additional compulsory parameter, the optional parameter would become the third parameter and I would have to make sure that I modified the test to compare against the value 3 and not 2. With %ParmNum() this is automagic.
The resulting value in req (default or passed value) is then used in the subsequent operation (C).
The most important thing to remember when using Options(*NoPass) is that the called routine should never, ever refer to a *NoPass parameter without verifying that it was indeed passed. Failure to follow this rule may either result in a runtime error or, more likely, hours of debugging fun after the problem is eventually discovered.
The changes necessary to protect our original program example from blowing up would have been identical as you can see if you download the code bundle that accompanies this article.
I hope this pair of tips has helped you to better understand the mechanics of parameter passing. If you have any related questions that you would like me to address, or that you think others could learn from, please contact me through Ted Holt via the IT Jungle Contact page.
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.