Jon Paris and Susan Gantner

IBM i Consultants, Developers, Educators and Apostles

May 29, 2018
Published on: IBM Systems
3 min read
RPG Prototypes


Getting More Help From RPG Prototypes

In our last EXTRA article, we discussed how RPG prototypes can make developers' lives easier. We promised to continue our coverage of prototypes, so in this article we'll move on to discussing how prototypes enable us to pass more "interesting" types of parameters, including files, and also how to prototype calls to bound procedures and functions.

Passing Data Structures as Parameters

Passing "ordinary" character, numeric or date parameters is pretty straightforward, but what about more complex parameters? For example, how about passing data structures or even data structure arrays? To define a data structure parameter, simply use either the LIKEDS or LIKEREC keyword to refer to the definition of the data structure elsewhere in the code. If it's a data structure array, just add the DIM keyword since LIKEDS will bring in the structure definition but not the number of elements from the original DIM statement.

If you wanted to pass a data structure array of up to 20 customer names and addresses, it could be done like this:

       Dcl-DS CustomerDataTemplate   Dim(20)  Qualified Template;
            Name              Char(20);
            AddressLine1      Char(30);
            AddressLine2      Char(30);
            AddressCity       Char(20);
            AddressStateProv  Char(30);
            AddressPostalCode Char(30);

       Dcl-Pr GetCustomerData ExtPgm('GETCUSTDTA');
            CustDataDS        LikeDS(CustomerDataTemplate) Dim(20);
            CustDataCount     Int(5) Const;

       Dcl-DS CustomerData    LikeDS(CustomerDataTemplate) Dim(20);                        

       GetCustomerData( CustomerData : CustomerCount );     

You may be wondering about the use of the TEMPLATE keyword on our data structure definition. That keyword means the data structure is only to be used to provide a definition of the structure in a LIKEDS. It is not an actual data structure which can hold data. Note that the parameter we passed on the call to GetCustomerData is the real structure name. We aren’t required to use a template with our LIKEDS, but it is a common practice. That's because prototypes are typically coded in a separate source member and then copied into any/all programs that need to call the code. Coding a template in the same source member with the prototype means we know the name we use in LIKEDS will always be valid, even when the actual DS name in the calling program is different The Procedure Interface (PI), which will be included in the called program, should mirror the parameter definitions from the PR shown here, including the LIKEDS keyword. That also simplifies the code there by supplying all the subfield definitions automatically.

Passing Files as Parameters

How about passing a file as a parameter? This is a situation that may not occur often, but when it does, this provides a very clever solution. What does it mean to pass a file as a parameter? You can literally pass control of a file, including its current positioning, to another program (or procedure). The called routine can then perform I/O operations to the file, such as READ, WRITE, CHAIN, UPDATE, etc. After returning to the original (calling) program, the file is potentially re-positioned based on activity done in the called routine.

While the file being passed as a parameter may be a database file, one of the best use cases we've seen for this support is with printer files. Suppose you have a report with different sections. In some cases, you need to produce all the sections of the report; in other cases only a subset of sections is needed. Ideally, you may want to modularize each section into its own program or procedure. But it can be very tricky to produce a single spooled file report when multiple independent programs are producing different sections.

However, if you simply pass control of the printer file between the initial program and the program(s) producing each section, this becomes much more straightforward. Let's first look at the prototype in the initial program needed to call a separate program to write a different section of the same report. Note the use of the LIKEFILE keyword to specify the file for which we want to pass control. Then on the call we simply pass the file name.

Dcl-F ProdRptf printer oflind(Overflow);

       Dcl-Pr PrintSection2 ExtPgm('PRTRPT2');
         printFile   LikeFile(ProdRptf);
         overFlowInd Ind;

       PrintSection2(ProdRptf : overFlow);   

We had to pass the overflow indicator as a second parameter because details such as the value of the overflow indicator aren’t passed along with the file parameter.

In the called program, we will include a declaration for the report printer file, but in this case we will use the TEMPLATE keyword. Much as when we used the same keyword for the data structure earlier, this file declaration tells the compiler that we want to use the file’s details (record formats, fields, etc.) but that the file will not be opened in the program. This template enables us to use the LIKEREC keyword to create data structures for each of the record formats that we use in the called program (more on those data structures in a moment).

Below you can see the file template declaration and the Procedure Interface(PI), which mirrors the parameter definitions from the prototype used for the call. This is code from the called program (PRTRPT2)

       Dcl-F ProdRptf printer Template;

       Dcl-PI PrintSection2 ExtPgm('PRTRPT2');
         PrintFile   LikeFile(ProdRptf);
         OverFlowInd Ind;

Since I and O specs aren’t created for a template file, we must use data structure I/O on our WRITE operations. In the code example below, you can see the data structure name used as the last parameter on each WRITE operation. You will also notice that we must qualify the record format names on our WRITE operation. That is a requirement when using the LIKEFILE keyword to define the file.

Take a look at the code related to these points below. Of course, we have omitted the parts of the logic that actually populate the data structures to be written to the report.

Dcl-Ds PageHdrDS  LikeRec(PrintFile.Heading:*Output);

       Dcl-Ds PrtRec2DS  LikeRec(PrintFile.PrtFmt2:*Output);
       Write PrintFile.Heading PageHdrDS;
       Write PrintFile.PrtFmt2 PrtRec2DS;           

Prototyping Calls to Procedures or Functions

So far, we’ve focused on calls to programs. Procedures and functions (which are really just a special type of procedure) have additional capabilities. Now we'll look at the additional things you can do when prototyping calls to procedures.

Perhaps the most obvious and commonly exploited difference is the ability of procedures to have a return value, which technically makes them functions. Functions tend to make our code more readable and understandable. Let's look at a simple example.

If we want to call a program that calculates sales tax for an item, the prototype and call may look something like this:

Dcl-Pr CalcSalesTax ExtPgm('UTIL0045R');
         Amount    Packed(7:2) Const;
         State     Char(2)     Const;
         County    Char(15)    Const; 
         Tax       Packed(5:2);
       CalcSalesTax(ItemAmt : StateCode : County: TaxAmt);     

If it were written as a procedure returning a value, it would look like the following, with the definition of the return value supplied on the Dcl-Pr instead of as a parameter.

       Dcl-Pr CalcSalesTax  Packed(5:2);
         Amount    Packed(7:2) Const;
         State     Char(2)     Const;
         County    Char(15)    Const; 
       TaxAmt = CalcSalesTax(ItemAmt : StateCode : County);     

This style of call makes the effect of calling CalcSalesTax more obvious. I.e., the calculation uses three parameters to control the calculation and the result of that is placed into TaxAmt. Perhaps our use of meaningful prototype and variable names made it pretty obvious even in the first example, but imagine the difference if the names weren't quite so obvious.

In addition to return values, procedures allow parameters to be passed by value instead of the default method of passing "by reference." By default, when a parameter is passed a pointer to the variable in the calling program or procedure is sent to the called program or procedure. That's how the calling routine sees the results of changes to the parameter values, not because the value went to the called routine and the updated value came back but because the called routine changed the original variables value in situ.

By contrast, when calling a procedure, we can pass the actual value of the parameter to the called routine. It’s a one-way street in that case: If the called routine changes the value, we won't see that change back in the calling routine. Since a copy of the value of the variable is always going to be made, passing by value has similar advantages to using the CONST keyword we covered in our previous article. The big difference is that when passing by value you are 100-percent guaranteed that the called routine cannot impact the content of that variable. Our tax calculation prototype using value would look like this:

       Dcl-Pr CalcSalesTax  Packed(5:2);
         Amount    Packed(7:2) Value;
         State     Char(2)     Value;
         County    Char(15)    Value; 

Prototyping Non-RPG Procedures and Functions

We can prototype calls to any language, not just RPG. So prototypes can be used to call CL or COBOL programs or procedures. We can also use prototypes to call C functions, including all the standard C functions that are on every IBM i and many system APIs, which are often called C-style APIs. Calls to C functions, including the C-style APIs, nearly always involve return values and they typically pass parameters by value. The procedure/function names tend to be lowercase and since, by default, RPG translates names to uppercase, it’s necessary to use EXTPROC to specify the lowercase function names. Alternatively, as of V7.1 if you code the prototype name in the appropriate case, you can simply specify EXTPROC(*DCLCASE).

Let's look at a prototype for a very simple C function—sleep. It will "hibernate" the program for a given number of seconds. It returns a value of 0 for success or -1 for failure

    Dcl-Pr sleep Int(10) ExtProc('sleep');
      seconds    Uns(10) value;

    Success = sleep(10);  // Sleep for ten seconds

For the most part, you likely won’t need to be concerned about writing prototypes for standard C functions or C-style system APIs. You can find most of those prototypes already "translated" into RPG for you with a quick internet search. That should be a relief for those of you who, like us, aren't exactly C language experts.

Still, you may want to at least be able to understand what's happening with those prototypes. To that end, there’s one additional quirk about calling C that we should cover. C doesn't deal with character variables the way RPG does. In RPG, character variables are typically of a fixed length. Even variable length fields only vary within a given range and always carry their actual length around with them. C deals with character variables as strings, which are indefinite in length. The end of the string is determined by the position of a X'00' value.

C functions also expect strings to be passed as a pointer passed by value. Similarly, any string returned by a C function will be represented by a pointer to a null terminated string. While we could always ensure we have the requisite X'00' value at the end of any character values we want to pass to a C function and we could strip it off of any character fields returned to us, fortunately we don't need to go to that trouble. The RPG compiler will do that work for us if we ask.

To pass a character string to a C function, specify the option *STRING to ask the compiler to format it properly before passing it. If the character variable is fixed-length, you will typically also want to specify *TRIM so that blanks at the end will be removed first. When using *STRING, even though the prototype specifies a pointer as the parameter type, the parameter on the call may simply be specified as a character variable. RPG will take care of translating it all the way C expects to see it.

As a simple example, look at the system function below. This function runs a CL command, much like QCMDEXC does. Unlike QCMDEXC it notifies the caller whether or not the command ran successfully. A return value of zero means success; anything else means failure.

Dcl-Pr CallSystem Int(10)    ExtProc('system');
     CmdString   Pointer   Value  Options(*String : *Trim);

   Dcl-S Command Char(40) Inz('CL command goes here');
   Reply = CallSystem(Command);

If an error is signalled, you can find out the exact cause by using the C function __errno() and its companion API strerror(). It's beyond the scope of this article to go into detail but there are number of examples of their use on the web.

More About Prototypes

Even after two articles on prototypes, we still haven't covered it all. We've tried to provide a good foundation, but we haven't covered all the options available or the different ways prototypes can be used. The rules changed in V7.1 about when prototypes are required. We wrote about this and our thoughts about some best practices in an earlier EXTRA article including why prototypes are typically copied into source members instead of hard-coded.