Parameter passing

From MidrangeWiki
Revision as of 19:13, 4 December 2018 by DaveLClarkI (talk | contribs) (Brief explanation)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Parameter passing in IBM i

This is a first cut at explaining the mechanism behind passing parameters from a command line or from a CLP program to other programs on the IBM i platform. Once someone edits this to something reasonable, hopefully it will include sub-procedures, too.

Summary

This might be the most frequently asked midrange question: Why do I get garbage in my parameter when I call a program from the command line?

The answer is: Write a command to wrap the program call.

This is a buffer overflow (or, underflow) situation. The caller (the command line) sets aside a buffer that is the larger of:

  • 32 bytes OR
  • the number of characters you type (including spaces) between the quotes.

If the callee declares its parameter as 128 bytes and you type something like CALL MYPGM 'ABC' on the command line, the command line sets aside 32 bytes, initialises it and calls your program. Which proceeds to read the 32 initialised bytes and also the remaining 96 uninitialised bytes - garbage.

Brief explanation

IBM i programs pass parameters by reference. This means that the caller and callee are using the same storage, defined in the caller. If the declaration of the parameters does not match exactly in type and length, unpredictable results may occur because the callee will be referring to storage that the caller did not set.

Calling a program from the command line, or passing literals, the operating system has to make some assumptions about the declaration of the passed values:

  • all numeric literals are treated as packed decimal 15, 5
  • all other literals are treated as char(32) minimum or the number of characters between the quotes, whichever is longer.

Using a user-defined command for the caller, the exact matching declaration could be defined in the command and the operating system does all needed casting.

Detailed explanation

Taken from the Midrange FAQ

CL Parameter Basics

When a variable is declared within a CL program, the system assigns storage for that variable within the program automatic storage area (PASA). If you subsequently use the variable as a parameter within a CALL command, the system does not pass the value of that variable to the called program, but rather a pointer to the PASA of the calling program. This is known as parameter passing by reference.

For this reason, it is very important that both programs declare the parameter to be of the same type and size. To illustrate, let's look at the following example:

PgmA: Pgm
  DCL &Var1 *CHAR 2 Inz( 'AB' )
  DCL &Var2 *CHAR 2 Inz( 'YZ' )
  Call PgmB Parm( &Var1 &Var2)
EndPgm
PgmB: Pgm Parm( &i_Var1 &i_Var2 )
  DCL &i_Var1 *CHAR 4
  DCL &i_Var2 *CHAR 2
EndPgm

Hopefully, you've noticed that the first parameter is declared to be larger in PgmB than it was in PgmA. Although you might expect &i_Var1 to contain 'AB ' after the call, the following is what the input parameters in PgmB actually contain:

   &i_Var1 = 'ABYZ'
   &i_Var2 = 'YZ'

&i_Var1 shows the contents of the first parameter, and the second, because the second parameter is immediately adjacent to the first within the storage area. If the second parameter was not contiguous to the first, then the last two bytes of &i_Var1 would show whatever happened to be in the storage area at that time.

You can think of &i_Var1 as a 4-byte "window" into the storage area of the calling program. It's passed a pointer that tells it where the view begins, and it accesses anything in storage from that point up to the parameter's declared length.

Looking at Literals

There are several ways that a program can be called, other than from another program. Examples include the command line, SBMJOB, job scheduler etc. In the case of an interactive call from the command line, you specify the parameters as literals, ie:

   Call PgmB Parm('AB' 'YZ')

Consider that when we do this, there is no PASA. We'll look at the implications of that in a minute, but for now, just make a note of it.

Submitting a job from the command line isn't any different. If you're submitting a CALL, then you'll be specifying any associated parameters as literals. However, things can get a bit deceiving when you submit a job from within a program, as the following example illustrates:

PgmC: Pgm
 DCL &Var1 *CHAR 2 Inz( 'AB' )
 DCL &Var2 *CHAR 2 Inz( 'YZ' )
 SbmJob Cmd(Call PgmB Parm( &Var1&Var2))
EndPgm

Clearly, we're not passing literals here. Or are we?

Let's think about how things would work if we passed variables:

  • PgmC submits a call to PgmB, passing two variables as parameters.
  • PgmC immediately ends as a result of the EndPgm statement.
  • PgmB begins running in batch and receives pointers to PgmC's PASA.
  • PgmB crashes when it attempts to use the pointers.

We have invalid pointers because PgmC is no longer running. If you've ever tried this personally, you know that it doesn't happen in practice. The reason for that is that the system is converting those variables to literals before issuing the CALL command. Very sneaky, but effective.

Now that we've seen some examples of where literals are used, and why, it's time to talk about the PASA again. When we discussed the basics of CL parameter passing, we learned that the called program expects to receive a pointer to a storage area within the PASA for each input parameter. This requirement hasn't changed. So now we have a situation where the CALL command is passing literals, but the called program is still expecting pointers.

Obviously, it's time for the system to perform some more magic behind the scenes. In order to accomodate the requirements of the called program, the system creates a space in temporary storage for each literal being passed, and moves the value of the literal into that storage space. Now it can pass pointers to the called program, and everyone is happy.

Except you that is, because none of this changes the fact that you're getting "garbage" in the input variables of your called program! Fair enough. I'm getting to that now, but you needed the background in order to understand the next part. Sizing It All Up

Now that you know the system is creating variables behind the scene, you might wonder how it knows what size those variables need to be. The answer is that it doesn't. Instead, the designers have imposed some specific rules about how literals are transformed to variables, and thereby passed as parameters.

CL supports only three basic data types: character, decimal, and logical. For the purposes of this discussion, you can consider the logical data type equivalent to the character type, because it's treated in the same manner.

The simplest rule is the one that handles decimal literals. All decimal literals will be converted to packed decimal format with a length of (15 5), where the value is 15 digits long, of which 5 digits are decimal places. Therefore, any program that you expect to call from the command line, or SBMJOB etc., needs to declare its numeric input parameters as *DEC(15 5).

Character literals are a little bit more complicated, but still fairly straightforward. There are two rules to remember. The first is that any character literal up to 32 characters in length will be converted to a 32 byte variable. The value is left justified, and padded on the right with blanks.

So if you were to pass the following literal:

   Call PgmB 'AB'

the associated storage space for that literal would contain:

   'ABxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' (where "x" represents a blank space)

The second rule is that character literals longer than 32 bytes are converted to a variable of the same length as the literal value itself, as in the following example:

   Call PgmB 'This is a long character literal that will exceed 32 bytes.'

the associated storage space for that literal would contain:

   'This is a long character literal that will exceed 32 bytes.'

Finally, since the logical data type follows the same rules as the character type, and the only possible values for a logical data type are '0' or '1', we know that a logical literal will always be created as a 32 byte, left justified, padded character variable.

Command-Line or SBMJOB CMD(CALL) Parameter Problems

In the beginning of this explanation, you learned that it was important for the parameter declarations to match between a called program and its caller. Then you discovered that the system sometimes has to take it upon itself to declare the parameters of the caller on your behalf. If the two declarations don't match, we have the potential for trouble.

In the case of a decimal value, the result is immediate and obvious; you get a data decimal error. Character variables are more difficult to debug because they don't generate any immediate errors. What actually happens depends upon the length of the parameter in the called program.

If the length of the parameter in the called program is less than the length of the parameter being passed, the extra characters are effectively truncated, as follows:

Call SomePgm ('ABCDEFG') /* system creates 32 byte *CHAR */
SomePgm: Pgm Parm( &i_Var1 )
 DCL &i_Var1 *CHAR 4
EndPgm

What happens is that the system passes 'ABCDEFGxxxxxxxxxxxxxxxxxxxxxxxxx' ('x' is a blank), but because of the declared length of &i_Var1, SomePgm only sees 'ABCD'. For most of us, this is the behaviour that we would expect.

Things get nasty when the declared length of the variable is longer than what is being passed in. Using the same example as we've just seen above:

SomePgm: Pgm Parm( &i_Var1 )
 DCL &i_Var1 *CHAR 34
EndPgm

In this case, the system will still allocate 32 bytes of storage and assign 'ABCDEFGxxxxxxxxxxxxxxxxxxxxxxxxx' to it, but because &i_Var1 is now declared to be 34 bytes long, SomePgm will see more storage than it was intended to. It will see the 32 bytes that were allocated for it, plus two additional bytes. It's those two additional bytes that can cause the infamous "unpredictable results" which IBM's documentation often refers to.

If the extra bytes contain blanks, chances are that you won't notice a problem, but if they contain something else, your input parameter will contain "garbage".

As you can see, when dealing with literals, the magic number for character parameters is 32. If the called program declares the parameter to be less than or equal to 32, you'll never see "garbage" in the parameter. Once you cross that 32 byte threshold, you need to take extra care to ensure that the size of the literal being passed is equal to (or greater than) the declared size of the input parameter.

Command-Line or SBMJOB CMD(CALL) Things to Remember

  • always match the type/size of parameters on your pgm to pgm calls.
  • remember that the system converts literals to variables in the background.
  • remember that decimal literals are always converted to *DEC(15 5).
  • and that char literals less than or equal to 32 bytes are converted to *CHAR(32).
  • and that char literals greater than 32 bytes are converted to variables of equivalent size.

and last, but not least:

  • the called program "sees" as much storage as it declares for an input parameter, regardless of whether or not the caller actually allocated that much storage for it.

Taken from the Midrange FAQ

When one runs debug and sees garbage in input parameters, it's often due to a mismatch in definition or size between the caller and called programs.

Some solutions:

  • Solution 1: For SBMJOB, do not use the CMD() parameter to pass a CALL command if the PARMs contain *CHAR variables greater than 32 bytes in size that need trailing blanks. Construct the RQSDTA() string instead.
  • Solution 2: Store all numerics being used as parameters as 15,5 packed.
  • Solution 3: Store all parameters embedded in a character datastructure and pass a single parameter, the datastructure.
  • Solution 4: Use another object, such as a file or a data area, to contain the parameters.
  • Solution 5: And my favorite. Write your own command and use that, instead of CALL, on your SBMJOB commands.

SBMJOB has a parameter called CMD. Traditionally people put a CALL command here, and then call their program directly.

Let's say you have a RPG program which calls another program. The first program has a screen which processes some information and passes it to the second program. And maybe this second program is rather intensive and you want that to run in batch. Then you may have the following in your first program:

    C                     CALL 'MYPGM'
    C                     PARM           MYNBR   50

Then you create a source member of type CMD. And this may contain:

            CMD        PROMPT('Your description here')
            PARM       KWD(MYNBR) TYPE(*DEC) LEN(5) MIN(1) CHOICE('Valid +
                         number') PROMPT('Enter number here')

To compile this you do CRTCMD CMD(MYLIB/MYPGM) PGM(MYLIB/MYPGM)

To see how this looks you can they type in the following at a command line MYPGM and hit <Enter> and you will get a prompt screen. Yes, you can design a 400 screen with so few lines of code.

And your second program may have:

PGM (                                                                +
     &PARM1           /* My numeric variable                     */ +
     &PARM2           /*                                         */ +
   )
   DCL  &PARM1       *DEC    5  /* My numeric variable              */
   DCL  &JOBINFO     *CHAR   1  /* 1=Interactive, 0=Batch           */
   /*                                                                         +
    | Retrieve current job type                                               +
   */
            RTVJOBA    TYPE(&JOBINFO)
   /*                                                                         +
    | Submit job if currently interactive                                     +
   */
            IF         COND(&JOBINFO = '1') THEN(DO)
            SBMJOB     CMD(MYPGM MYNBR(&MYNBR))
            GOTO       CMDLBL(END)
            ENDDO
   /*                                                                         +
    | Do your real processing here                                            +
   */
END:
ENDPGM

Further background Midrange FAQ

Q When I pass numeric parameters to my RPG program from the command line my program isn't working as expected, why?

A The command line doesn't know what type of parameters it is trying to pass, so it converts all numeric variables into 15,5 before passing them.

Number of work arounds include:

Create a command to call the RPG program, which will convert the numeric variables to the length declared in the command. Pass the numeric values as character data (i.e. PARM('0009') rather than PARM(9))

This is part of a common misunderstanding of how to pass a constant as a parameter.

The caller's parameter definitions must match the called program's definitions. The command line has no way to declare a variable so it uses a default type and length.

Numeric default is packed 15, 5 decimals (8 bytes). Character default is character 32 bytes.

The manual that describes this is the OS/400 CL Programming manual,

 section 3.4 (Passing Parameters between Programs and Procedures)
 http://publib.boulder.ibm.com/cgi-bin/bookmgr/BOOKS/qb3auo03/3.4

Check out the helpful section entitled

 Common Errors when calling Programs and Procedures at
 http://publib.boulder.ibm.com/cgi-bin/bookmgr/BOOKS/qb3auo03/3.4.2

...and also the section entitled

 Using the Call Command at
 http://publib.boulder.ibm.com/cgi-bin/bookmgr/BOOKS/qb3auo03/3.4.1

(all references V4R5)

All of this is predicated on the concept that you MUST pass a constant from the command line by a mechanism like SBMJOB CMD(call myprog ('VALUE1' 25.00 'VALUE2')) You could also write a command as a wrapper so you'd end up with something more like SBMJOB CMD(PRTATB VALUE1 25.00 VALUE2)

Taken from the Midrange FAQ


Example submitting batch jobs with fields longer than 32 bytes.

There are multiple ways you can setup the program to submit another job with fields longer than 32 bytes.

Here are two examples. 1. Pad the Parameter with 1 additional non-blank character when running SBMJOB. 2. Create a command to use with SBMJOB.

Example Padding Parameters in CL

This is a Sample Method of Padding a Parameter to force the command execution to provide all bytes. This example defines two fields. The first field has the desired size and this field is used as the input parameter. The second field is defined 1 character larger than the first field. The second field is used to submit the job. You load the second field with the value from the input. Then you set the last character of the second field to 'X' or any non-blank character. Then when you run the SBMJOB command you pass the second field instead of the first.

In this snippet we check if the program was called with the submit set to "Y" and if it is we submit the program to batch:

SomePgm: Pgm Parm( &submit &filename )
  DCL &submit *CHAR 1
  DCL &filename *CHAR 1024
  DCL &filename2 *CHAR 1025

  IF COND(&SUBMIT *EQ 'Y') THEN(DO)
    chgvar var(&filename2) value(&filename)
    chgvar var(%SST(&filename2 1025 1)) value('X')
    SBMJOB CMD(CALL SOMEPGM PARM('N' &filename2)) JOB(SOMEJOB) JOBQ(SOMEQUEUE)
    RETURN
  ENDDO

  ...Do Other Stuff...
 
EndPgm


Example Using a Command

This example shows how to use a command to preserve parameter size. The command is used instead of Calling the program. It is also used with SBMJOB to submit the program to batch.

SOMEPGM SUBMIT('Y') FILENAME('/home/user/stuff.txt')


SOMEPGM: CMD PROMPT('Do this process')
  PARM KWD(SUBMIT) TYPE(*CHAR) LEN(1) DFT(Y) VALUES(Y N) +
       PROMPT('Submit to batch' 1) RSTD(*YES)
  PARM KWD(FILE) TYPE(*CHAR) LEN(1024) DFT(' ') +
       PROMPT('File name' 2) 


SomePgm: Pgm Parm( &submit &filename )
  DCL &submit *CHAR 1
  DCL &filename *CHAR 1024

  IF COND(&SUBMIT *EQ 'Y') THEN(DO)
    SBMJOB CMD(SOMEPGM SUBMIT('N') FILENAME(&filename)) JOB(SOMEJOB) JOBQ(SOMEQUEUE)
    RETURN
  ENDDO

  ...Do Other Stuff...
 
EndPgm

Creating a simple command processing program

A simple CMD object, without any of the great bells and whistles capable of within a CMD object can be created quite easily from a CL program. For example, take the following:

            PGM        PARM(&WSIDX &JBTYPE)
            DCL        VAR(&WSID)    TYPE(*CHAR) LEN(10)
            DCL        VAR(&WSIDX)   TYPE(*CHAR) LEN(10)
            DCL        VAR(&JOBQ)    TYPE(*CHAR) LEN(10)
            DCL        VAR(&JOBQLBR) TYPE(*CHAR) LEN(10)
            DCL        VAR(&JOBD)    TYPE(*CHAR) LEN(10)
            DCL        VAR(&JBTYPE)  TYPE(*CHAR) LEN(1)
...

Just copy this from QCLSRC to QCMDSRC. Change the type from CLP to CMD. Drop all lines except the PGM and any variables that appear on the PGM statement.

            PGM        PARM(&WSIDX &JBTYPE)
            DCL        VAR(&WSIDX)   TYPE(*CHAR) LEN(10)
            DCL        VAR(&JBTYPE)  TYPE(*CHAR) LEN(1)

Change the DCL to PARM

            PGM        PARM(&WSIDX &JBTYPE)
            PARM       VAR(&WSIDX)   TYPE(*CHAR) LEN(10)
            PARM       VAR(&JBTYPE)  TYPE(*CHAR) LEN(1)

Drop the VAR from the PARM commands

            PGM        PARM(&WSIDX &JBTYPE)
            PARM       (&WSIDX)   TYPE(*CHAR) LEN(10)
            PARM       (&JBTYPE)  TYPE(*CHAR) LEN(1)

Drop the ampersand, and maybe the parenthesis

            PGM        PARM(&WSIDX &JBTYPE)
            PARM       WSIDX   TYPE(*CHAR) LEN(10)
            PARM       JBTYPE  TYPE(*CHAR) LEN(1)

And, now that you got the variables down, change the PGM to a CMD and drop the parameters

            CMD
            PARM       WSIDX   TYPE(*CHAR) LEN(10)
            PARM       JBTYPE  TYPE(*CHAR) LEN(1)

This will compile successfully and work just fine. This is a quick and dirty way to convert a SBMJOB CMD(CALL ... that's getting you into all sorts of trouble because of parameters lengths to a nice SBMJOB CMD(MYCMD... that won't have that trouble.

Now, there's a lot more potential there. Like keyword prompting, default values, multiple list items, indents like library/object and so on. But this will do that fast conversion.

Categories

This article is a stub. You can help by editing it.