Wednesday, August 12, 2015

Using CL procedures

cl procedure callprc dclprcopt

All of the examples I can find of using procedures on the IBM i are mainly in RPG, with a few in C. This made me want to demonstrate that it is possible to create procedures in CL, and to call procedures using CL programs and procedures.

This is also a time to move from using "CLP" source members to "CLLE", as there are commands that will be used in this post that are not supported by old "CLP" source or "CLP" programs.

After thinking about this for a while I have decided to give two examples of using procedures in CL:

  1. CL program calling a procedure
  2. CL procedure calling a procedure

I am sure all of you regular readers will appreciate what the examples I am giving are simple. It is how the procedure prototypes, interfaces, etc. are coded that is important, not what happens within the procedures and programs.

In RPG the values returned from a procedure are normally placed in a data structure. I am going to do the same in CL. I am not going to describe in detail how to code these in CL as I have already done so in the post Data structures in CL.

 

CL program calling a procedure

I thought I would start with the simplest example: have a CL program call a RPG procedure.

In my CL source there are two commands that will be new to you:

The Declare Processing options command, DCLPROCOPT, acts like RPG's Control Options, CTL-OPT. This allows me to give the activation group information and the binding directory within the source, so I will not have to remember to enter them every time I compile the program.

The Call Bound Procedure, CALLPRC, command does as its name suggests: it is used to call procedures. It does have one parameter that the regular CALL command does not, the return value from the procedure, RTNVAL.

And now to the code:

01  PGM

02  DCLPRCOPT DFTACTGRP(*NO) ACTGRP(*NEW) +
                BNDDIR(*LIBL/TESTBDIR)

03  DCL       VAR(&PASSED) TYPE(*CHAR) LEN(25)

04  DCL       VAR(&FIRST) TYPE(*CHAR) STG(*DEFINED) +
                LEN(2) DEFVAR(&PASSED 1)

05  DCL       VAR(&SECOND) TYPE(*DEC) STG(*DEFINED) +
                LEN(5 2) DEFVAR(&PASSED 3)

06  DCL       VAR(&THIRD) TYPE(*CHAR) STG(*DEFINED) +
                LEN(15) DEFVAR(&PASSED 6)

07  CHGVAR    VAR(&FIRST) VALUE('BB')

08  CALLPRC   PRC('PROCTEST') PARM((&PASSED *BYVAL)) +
                RTNVAL(&PASSED)

09  ENDPGM

Line 2 shows the DCLPROCOPT as I described above. It will look familiar to those you who use RPG's control options.

Lines 3 – 6 define the data structure, &PASSED, and its sub fields, &FIRST, &SECOND, and &THIRD.

Line 8 contains the call to the procedure. I am passing to it the data structure. The *BYVAL is similar to RPG's value, meaning that the value of the parameter is passed rather than a copy of it, and prevents the called procedure from changing the value.

The RPG procedure is very simple too:

01  ctl-opt nomain ;

02  dcl-pr ProcTest char(25) ;
03    *n char(25) value ;
04  end-pr ;

05  dcl-ds TestData len(25) qualified ;
06    First char(2) ;
07    Second packed(5:2) ;
08    Third char(15) ;
09  end-ds ;

10  dcl-proc ProcTest export ;
11    dcl-pi *n char(25) ;
12      InData char(25) value ;
13    end-pi ;

14    TestData = InData ;
15    TestData.Third = 'THIRD' ;
16    TestData.Second = 22.48 ;

17    return TestData ;
18  end-proc ;

Line 1 the control option indicates that there is no main procedure in this member, and when compiled this module.

Lines 2 – 4 are my Procedure prototype.

I decided to code the definition of the data structure outside of the procedure, lines 5 - 9, just because I can. I could have just as well placed it within the procedure.

Line 10 is the start of the procedure. I have to code it as export so that the returned value is passed back to the CL program.

The procedure interface is defined between lines 11 – 13. Notice that the input parameters on line 12, and the corresponding parameter in the procedure prototype, line 3, are both coded as receiving the parameter as value, which matches how the CL program passes it.

As the input parameter is passed by value I have to move its value to my data structure, line 14, before I can update the subfields, lines 16 – 16.

I return the data structure on line 17, and end the procedure on line 18.

A fixed format version of this code can be seen at the bottom of this post here.

What of the binding directory? This is where I define that the compile is to bind the RPG module, TESTRPGMOD, that contains the procedure ProcTest, to my CL program when I create it:

                     Work with Binding Directory Entries

Binding Directory:   TESTBDIR       Library:   MYLIB

Type options, press Enter.
  1=Add   4=Remove

Opt   Object       Type      Library      Activation
 _    __________   _______   __________   __________
 _    TESTRPGMOD   *MODULE   MYLIB

When the RPG module and CL program have been created I call the CL program and use debug to check the value of &PASSED at the last line of the CL program:

&PASSED = 'BB  ±THIRD               '
&FIRST = 'BB'
&SECOND = 022.48
&THIRD = 'THIRD          '

The sub field &FIRST contains the value I gave it in the CL program on line 7, and was not changed in the RPG procedure.

The sub fields &SECOND and &THIRD contain the values I gave them in the RPG procedures on lines 15 and 16.

 

CL procedure calling a procedure

Now to give a more complicated scenario. I have a RPG program that contains two procedures. The main procedure will call a CL procedure, which in turn called the second procedure in the RPG. Sounds a bit complicated? Not really, it will become a lot clearer when I show the code.

Let me start with the code for the CL procedure. Unlike RPG there can only be one CL procedure in each source member and module. It looks almost identical to the CL source I gave above when I created the CL program. The only difference is I had to remove the DCLPROCOPT command.

01  PGM

02  DCL       VAR(&PASSED) TYPE(*CHAR) LEN(25)

03  DCL       VAR(&FIRST) TYPE(*CHAR) STG(*DEFINED) +
                LEN(2) DEFVAR(&PASSED 1)

04  DCL       VAR(&SECOND) TYPE(*DEC) STG(*DEFINED) +
                LEN(5 2) DEFVAR(&PASSED 3)

05  DCL       VAR(&THIRD) TYPE(*CHAR) STG(*DEFINED) +
                LEN(15) DEFVAR(&PASSED 6)

06  CHGVAR    VAR(&FIRST) VALUE('BB')

07  CALLPRC   PRC('PROCTEST') PARM((&PASSED *BYVAL)) +
                RTNVAL(&PASSED)

08  ENDPGM

When I am ready to compile the source I need to compile it as a module. Which I can either by using option 15 in PDM or by using the CRTCLMOD command.

I then add this module to a new binding directory:

                     Work with Binding Directory Entries

Binding Directory:   TESTBDIR2       Library:   MYLIB

Type options, press Enter.
  1=Add   4=Remove

Opt   Object       Type      Library      Activation
 _    __________   _______   __________   __________
 _    TESTCLPRC    *MODULE   MYLIB

Now to the RPG, which contains two procedures: Main and ProcTest. ProcTest is identical to the standalone procedure I mentioned above. If you are unfamiliar with the purpose of the Main procedure you should read Getting off the RPG cycle.

01  ctl-opt actgrp(*new) dftactgrp(*no) main(Main)
              bnddir('*LIBL/TESTBDIR2') ;

02  dcl-pr Main extpgm('TESTRPG') ;
03  end-pr ;

04  dcl-pr ProcTest char(25) ;
05    *n char(25) value ;
06  end-pr ;

07  dcl-pr TESTCLPRC char(25) ;
08    *n char(25) value ;
09  end-pr ;

10  dcl-ds TestData len(25) qualified ;
11    First char(2) ;
12    Second packed(5:2) ;
13    Third char(15) ;
14  end-ds ;

15  dcl-proc Main ;
16    dcl-pi *n ;
17    end-pi ;

18    TestData.First = 'AA' ;

19    TESTCLPRC(TestData) ;
20    return ;
21  end-proc ;


22  dcl-proc ProcTest export ;
23    dcl-pi *n char(25) ;
24      InData char(25) value ;
25    end-pi ;

26    TestData = InData ;

27    TestData.Third = 'THIRD' ;
28    TestData.Second = 22.48 ;

29    return TestData ;
30  end-proc ;

The control options on line 1 are pretty straight forward, and I don't think they need any description.

What follows are the procedure prototypes for the three procedures. Lines 2 and 3 for the Main procedure, lines 4 – 6 for ProcTest, and lines 7 – 9 for TESTCLPRC.

The data structure which is used to pass the parameters between the procedures is defined on lines 10 – 14. As this is not in a procedure it is available to both Main and ProcTest.

The procedure Main starts at line 15, and is followed by the procedure interface definition. Even if there are no parameters passed to a RPG procedure their procedure interface still must be defined.

The CL procedure is called on line 19. Notice that the procedure name is the same as the module that contains it.

Line 20 has the return that exits the Main procedure, and the end of the procedure is on line 21.

Lines 22 – 30 are the procedure ProcTest.

A fixed format version of this code can be seen at the bottom of this post here.

To execute all of this I call the program TESTRPG. If I place a debug breakpoint on the return, line 20, I can see that the data structure has been changed by both TESTCLPRC and ProcTest.

&PASSED = 'BB  ±THIRD               '
&FIRST = 'BB'
&SECOND = 022.48
&THIRD = 'THIRD          '

The sub field &FIRST contains the value I gave it in the CL procedure on line 6, and was not changed in the RPG procedure.

The sub fields &SECOND and &THIRD contain the values I gave them in the ProcTest procedures on lines 27 and 28.

 

You can learn more about these on the IBM website:

 

This article was written for IBM i 7.2, and should work for earlier releases too.


Fixed format version of first RPG procedure

01  H nomain

02  D ProcTest        PR            25
03  D                               25    value

04  D TestData        DS            25    qualified
05  D   First                        2
06  D   Second                       5P 2
07  D   Third                       15

08  P ProcTest        B                   export

09  D ProcTest        PI            25
10  D   InData                      25    value
     /free
11     TestData = InData ;
12     TestData.Third = 'THIRD' ;
13     TestData.Second = 22.48 ;

14     return TestData ;
     /end-free
15  P                 E

Return


Fixed format version of second RPG member containing two procedures

01  H actgrp(*new) dftactgrp(*no)
02  H   bnddir('*LIBL/TESTBDIR2')

03  D ProcTest        PR            25
04  D                               25    value

05  D TestClle        PR            25
06  D                               25    value

07  D TestData        DS            25    qualified
08  D   First                        2
09  D   Second                       5P 2
10  D   Third                       15
     /free
11    TestData.First = 'AA' ;

12    TESTCLPRC(TestData) ;

13    *inlr = *on ;
     /end-free
14  P ProcTest        B                   export

15  D ProcTest        PI            25
16  D   InData                      25    value
     /free
17       TestData = InData ;

18       TestData.Third = 'THIRD' ;
19       TestData.Second = 22.48 ;

20       return TestData ;
     /end-free
21  P                 E

Return

6 comments:

  1. I Am A COBOL Guy - I Never Learned RPG.

    Back In The Day, The AS400 CL Was 1 Of My Expertises.

    Cheers.

    ReplyDelete
  2. Query: TESTCLPRC(TestData) ; --> but the CL procedure doesnt have PGM PARM to accept this TestData? - J

    ReplyDelete
  3. Query 2: RPRLE procedure can have different input and return variables.
    Looks like CLLE module can get values and return only via the same PGM PARM variable. There cannot be a separate return variable in CLLE module. - J

    ReplyDelete
  4. so as a good practice the procedures PR's are sometimes kept inside a copybook and not declared in the procedure itself to leverage not having to specify them in every pgm that calls the procedure. So in this case, is there a way to work with that, OR does the PR HAVE to be in the procedure?

    ReplyDelete
    Replies
    1. The code above is just for example, therefore, I may not do things this way in my "live" code.

      I agree I do tend to put all the DCL-PR's in a "copybook" so that they can be copied into the source I need to use them in. I use the DEFINE/UNDEFILE compiler directives to only include the bits of code I desire.

      Delete
    2. The Compiler binds only procedures you have used ... there is no need for "define/undefine"

      Delete

To prevent "comment spam" all comments are moderated.
Learn about this website's comments policy here.

Some people have reported that they cannot post a comment using certain computers and browsers. If this is you feel free to use the Contact Form to send me the comment and I will post it for you, please include the title of the post so I know which one to post the comment to.