In the AS/400 environments I've worked in, there is an almost universal understanding of the roles of CL and RPG that goes something like this: Use CL to control the operating system and system objects, and use RPG to control database, display, and print files. Certainly, some sites employ CL far more than others, but I expect most of you would recognize this as a generalization.
Of course, this is no accident. The respective capabilities of CL and RPG are such that this kind of use is natural. But haven't you ever wished that CL had a more readable syntax? Or had arrays? Maybe better file handling and, while we're at it, select, do while, and do until statements? In short, if CL had all the features of RPG, everything would be fine and dandy. You could put this in reverse and wish that RPG had all the powerful features of CL. Well, you need wish no more-with RPG IV and ILE, all of this is possible.
But there's more to this than just gaining a flashy CL or RPG syntax. The division of computer tasks into system (read CL) and business (RPG) isn't particularly relevant when designing components for software models that have to deal with real-world applications as diverse as ordering wood pulp and landing a probe on Mars. What is relevant is that these components match the concepts we visualize when designing our model. It is the computer that should mimic us, not the other way round.
The RPG IV procedure goes a long way toward making this possible, and you should adopt it as the fundamental building block of the more complex systems you design and assemble. Allow me to show you how easy this is to do. In this article, I'll introduce you to two service programs that make interacting with OS/400 a breeze and are indispensable in developing more sophisticated procedures. The first is the COMMAND service program, which allows you to issue commands, and the second is the SYSTEM service program, which provides all the functionality usually associated with CL.
Service programs are a new concept to many RPG programmers. You code them in the same way as you would any other RPG IV program, only there isn't any main-line. That's because you don't execute a service program-you just call its procedures. You can think of a service program as a collection of procedures that any program can use. Creating a service program is similar to creating a program. With both programs and service programs, you use the Create RPG Module (CRTRPGMOD) command to compile your source and create a module. The module is a system object consisting of compiled code, but it isn't executable. You first have to bind one or more modules together to create a program or service program using the CRTPGM (Create Program) or CRTSRVPGM (Create Service Program) commands. The key difference between the two is that a program must include at least one module that has a main-line.
If you want to use a procedure that resides in another service program, you have to make the compiler aware that this procedure exists and aware of how it can be used (i.e., what parameters are expected and what values are returned, if any). This is what the prototype is for. Whenever a procedure is referred to in source code, the compiler checks the prototype to ensure the usage conforms to it. If it doesn't, the compiler will issue an error message, and no module will be created. As you can imagine, coding a prototype in your source every time you want to use a commonly used procedure would become tedious. The best approach is to code your prototypes only once and keep them in a source file of their own. I keep my prototype source in a file called QCPYSRC. Whenever I want to use the procedures in a service program, I include the line /COPY QCPYSRC,xxx where xxx is the name of the prototype source member. I also make a point of having a prototype source member for every service program, rather than lumping all prototypes together in one big member. An outcome of this technique is that, for every service program, I have a source member in QRPGLESRC (where the actual procedure code is) and another member by the same name in QCPYSRC (which consists of prototypes only for these procedures).
It should be clear now why a module isn't executable. If you use a procedure that is defined elsewhere, the compiler may be satisfied that you are using this procedure correctly (by checking your usage against the prototype) and successfully create a module, but the code that makes the referenced procedure work is still unknown to the module. The module must be linked to the service program where the called procedure resides. You do this in the CRTPGM and CRTSRVPGM steps, where you list all the service programs required as the BNDSRVPGM parameter.
COMMAND is about as simple as a service program can get. It consists of only one procedure and requires no other service program. The procedure is called ExecCmd, and the prototype is shown in Figure 1. First, a word or two about the conventions I use: The return value of ExecCmd is a 7-alpha character string, which will be loaded with a message identifier should anything unexpected happen. If you pass OptParms, it will be loaded with any parameters associated with the error message. I use these two parameters in this way very often for indicating error situations and providing information about them, but I never use them to communicate informational-only messages (as opposed to true errors). This is to make life simple for the caller. If a procedure returns blanks, it succeeded. If it returns a message, it failed. If you want to pass a message back indicating something like "Job submitted" or "Operation succeeded," then use another parameter dedicated to this purpose. I also prefix all optional parameters with the letters Opt. While this enhances readability a little, the real reason will
become apparent soon. Let's get back to ExecCmd.
As Figure 1 shows, ExecCmd has only one mandatory parameter, and that's the command you want to execute. Note that this is a pass-by-value parameter (indicated by the word value on the prototype), so the command you pass doesn't have to be 2,048 characters long and could be a literal. Let's delete all files starting with WORK currently in library QTEMP.
The program in Figure 2 shows how you would do this. This would work fine if there were some files starting with WORK in QTEMP, but what if there weren't? With CL, we have to insert MONMSG statements to deal with the unexpected; otherwise, the program crashes. Not so with ExecCmd. It just passes back the message indicating an error occurred, then carries on to the next statement.
In the above example, I'm not tracking the return value, because I want to delete WORK* files only if they exist. If I were interested in the success or failure of the command, I would code something similar to Figure 3.
So what's really going on behind the scenes when ExecCmd gets called? Let's take a look at the code in Figure 4. Yes, it's the API QCAPCMD. Like all APIs, if you pass an error data structure (ERRC0100) and an error occurs, the error details are passed back to the caller, and the system error handler is not invoked (that's the thing that displays highlighted messages when you forget to code a MONMSG in a CL). But rather than dwell on the code, let's just compare it to the prototype. Which do you think is easier to understand? Would you rather use ExecCmd or QCAPCMD directly? This highlights the thinking behind using procedures. Hide all the code behind the prototype and make the prototype clear and practical. Then, no one else needs to waste any time figuring out how it works in order to use it.
So why do I think ExecCmd warrants a service program all of its own? The answer to this requires an understanding of activation groups. An activation group isn't some kind of system object that you have to create-it's an attribute of a program or service program that you specify when creating the program. You use them to subdivide your job. Amongst other things, programs that have the same activation group can issue the Override Database File (OVRDBF) and Open Query File (OPNQRYF) commands and share the same open data paths without affecting programs running in other activation groups. COMMAND should be created with an activation group of *CALLER, meaning every activation group that uses COMMAND gets its very own copy of it. This allows commands like OVRDBF and OPNQRYF to be effective over the activation group that invokes them. If COMMAND resided in an activation group of its own and another activation group issued an OVRDBF using ExecCmd, that override would be effective only within the activation group that COMMAND resided in. The caller would be wasting its time. In most cases, it's better to create service programs with activation groups of their own so that only one copy is active per job. COMMAND is an exception to this. SYSTEM, the next service program I'll deal with, is not. It will have dozens, perhaps hundreds, of procedures, and only one copy of it needs to be active per job.
(I'm using the term "copy of a service program" very loosely here. There will only ever be one copy of the executable code running, no matter how many activation groups. By "copy" I'm referring to file data paths and buffers, variables, and allocated memory.)
Within SYSTEM, you'll recognize many of the procedures that are more commonly connected with CL. Rather than go into all the procedures I'm using, I'll demonstrate how to create them and leave the rest up to you. The key thing here is that service programs are inherently extendable, so, as the need for more procedures arises, just add them. Let's create a procedure called CpyF that allows us to use the Copy File (CPYF) command. It makes sense to use the same names for procedures as the command you're implementing so other programmers don't have to guess, though I do capitalize key letters to enhance readability. I show the prototype in Figure 5.
Anyone familiar with CPYF will understand these parameters without explanation. Because the parameters you pass will be embedded within a real CPYF command, any value that is acceptable to CPYF can be passed (e.g., *LIBL is valid as a FromLib value). Notice also that the same convention I used with ExecCmd for dealing with errors is at work here. If CpyF ends without incident, *BLANKS are returned; otherwise, the message identifier is returned and optional parameters are loaded. Figure 6 shows code that is extracted from an interactive program that has display fields X_FILE, X_LIB, X_TOFILE, and X_TOLIB.
Here, the user enters the name of the file and library to be copied, along with the name of the file and library to be copied into. If anything goes wrong, the error message and any parameters are passed to procedure DspError, which highlights and positions to the field named and displays the error (DspError is part of a suite of procedures I use that streamline interactive programming, but that's another story). Notice that I haven't bothered to edit any of the screen values before calling CpyF. That's because I don't need to. If the values of X_FILE and the rest are invalid, CpyF will fail and return the appropriate message. Sometimes, system return messages can be a bit vague, so in those cases, a precheck of data is appropriate. Figure 7 shows how CpyF works.
At this point, I should explain optional parameters a bit further. You can specify options(*NOPASS) against a parameter to indicate that it doesn't need to be passed. Once you do this, though, all the following parameters must be defined as optional. You must also take care not to use an optional parameter in your procedure if it hasn't been passed. If you do, you'll get a "Reference to location not found" error at runtime, causing your program to crash. My approach is to define a local variable within the procedure for every optional parameter. I initialize this local variable with a default value (i.e., the value used if the parameter isn't passed), and then I use the %parms built-in function to see if the parameter was passed or not. If it was, I reset the value of the local variable to what was passed.
Once I've counted the parameters and set the values of the local variables, all the following logic will use only these locals, with the assurance that they do indeed exist. The optional parameters, those that I prefix with an Opt, are referenced only within a %parms condition. The rest is pretty straightforward. The CPYF command is constructed using the passed parameters and the priceless %trim function, and ExecCmd is called to do the job.
It's also no big deal if you want to specify parameters other than the ones I'm using. By adding new optional parameters after those already in place (rather than rearranging them), you won't need to recode any logic already using CpyF. What I've done here for CpyF goes just as well for any other command you can think of, and, within minutes, you can create DltF, ClrPFM, ChkObj, and more. Once you've got these basic procedures together, it's an easy step to create more complex functions like CpyPF and LFs. (If you can't guess what that does, then I've failed
to give it an adequate name.)
Some system commands, like Create Program (CRTPGM) and Create Service Program (CRTSRVPGM), require lists of parameter values, rather than the single instance parameters I've used up until now. In those cases, I pass the name of a dynamic table (see "Dynamic Tables in RPG," MC, December 1997), which I preload before calling. CRTPGM requires a list of the modules and, optionally, the service programs that are to be bound together. My CrtPgm prototype looks like Figure 8. But CL does more than just run commands. What about all those functions like Retrieve Job Attributes (RTVJOBA) and Retrieve System Value (RTVSYSVAL) that extract valuable system information? How can ExecCmd retrieve it? Well, to tell the truth, I don't know of a way yet, so I don't use it. In these cases, I put together small CL modules dedicated to retrieving these values, prototype the procedure as usual, and then bind them into SYSTEM.
To create a CL ILE module, the source code must first be of type CLLE rather than the CLP you're familiar with. It would be wise to create a new source file called QCLLESRC to keep it in. You use command Create CL Module (CRTCLMOD) to create the module and then bind it into a program or service program as you would any other module, whether it be RPG, C, or CL. This is, after all, what ILE is all about.
You needn't limit yourself to implementing commands that operate on system objects currently available on the AS/400. CrtWidget, DltWidget, and CpyWidget could all take their places alongside the more familiar CpyF and CrtDupObj commands. Widget could be a combination of files, data structures, user spaces-you name it.
RPG IV may not be a fully fledged object-oriented language yet, but it certainly makes it a lot easier to adopt object-oriented design philosophy than its predecessor.
John V. Thompson is a technical consultant at Honda New Zealand. He can be contacted by email at
Figure 1: The ExecCmd prototype
*===============================================================
* Execute CmdString, return message.
*IfOptParmsispassed,itwillbereturnedwithanyparameters
* associated with the returned error message.
* If OptSubmit passed as *ON, command will be submitted. Default
*is*OFF
*
D ExecCmd PR 7A
D CmdString 2048A value
D OptParms 128A options(*NOPASS)
D OptSubmit 1A value options(*NOPASS)
*===============================================================
*========================================================
* To compile:
*
* CRTRPGMOD MODULE(XXX/EXECTEST) SRCFILE(XXX/QRPGLESRC)
*
* CRTPGM PGM(XXX/EXECTEST) BNDSRVPGM(XXX/COMMAND)
*
*===============================================================
/COPY XXX/QCPYSRC,Command
C callp ExecCmd('DLTF QTEMP/WORK*')
Figure 2: Using ExecCmd in a test program
C Eval *InLr = *On *====================================================
C eval MsgID=
C ExecCmd('DLTF QTEMP/WORK*':Parms)
C select
C when %subst(MsgID:1:3)='CPF'
*
* logic to process any kind of CPFxxxx message.
C when %subst(MsgID:1:3)='CPA' *
* logic to process any kind of CPAxxxx message.
C endsl
*========================================================
*=========================================================
* To compile:
*
* CRTRPGMOD MODULE(XXX/COMMAND) SRCFILE(XXX/QRPGLESRC)
*
* CRTSRVPGM SRVPGM(XXX/COMMAND) MODULE(XXX/COMMAND) +
* EXPORT(*ALL) ACTGRP(*CALLER)
*
*=========================================================
H nomain
H/TITLE Command Procedures
*******************************************************
* PROTOTYPES
*******************************************************
/COPY XXX/QCPYSRC,Command
*
D QCAPCMD PR extpgm('QCAPCMD')
D CmdString 2048A
D CmdLength 9B 0
D OptCtlBlk 20A
D OCBLen 9B 0
D OCBFmt 8A
D ChgCmdStr 1A
D LenAvailCS 9B 0
D LenOfChgCS 9B 0
D ErrorCode 116A
**********************************************************
* EXPORTED PROCEDURES
**********************************************************
P ExecCmd B export
* Execute command in CmdString
*
D ExecCmd PI 7A
D CmdString 2048A value
D OptParms 128A options(*NOPASS)
D OptSubmit 1A value options(*NOPASS)
*
* Locals:
D Submit S 1A inz(*OFF)
D CmdLength S 9B 0 inz(2048)
D OptCtlBlk DS
D TypOfCmdP 1 4B 0 inz(0)
D DBCS 5 5A inz('0')
D Prompt 6 6A inz('2')
D CmdStrSyn 7 7A inz('0')
D MsgKey 8 11A inz(*BLANKS)
D Resvd1 12 20A inz(*LOVAL)
D OCBLen S 9B 0 inz(20)
D OCBFmt S 8A inz('CPOP0100')
D ChgCmdStr S 1A inz(*BLANKS)
D LenAvailCS S 9B 0 inz(0)
D LenOfChgCS S 9B 0 inz(0)
D ERRC0100 DS
D BytesProv 1 4B 0 inz(144)
D BytesAvail 5 8B 0 inz(0)
Figure 3: Scenario for retrieving error information
Figure 4: COMMAND service program source
D ExcepID 9 15A inz(*BLANKS)
D Resvd2 16 16A inz(*BLANKS)
D Exception 17 144A inz(*BLANKS) *
C if %parms 2
C eval Submit=OptSubmit
C endif *
C if Submit=*ON
C eval CmdString=
C 'SBMJOB CMD(' + %trim(CmdString)
C + ') LOG(*JOBD *JOBD *NOLIST)'
C endif *
C callp QCAPCMD(CmdString:CmdLength
C :OptCtlBlk:OCBLen
C :OCBFmt:ChgCmdStr
C :LenAvailCS:LenOfChgCS
C :ERRC0100)
*
C if %parms 1
C eval OptParms = %trim(Exception)
C endif *
C return ExcepID *
PE ************************************************************
*===============================================================
*CpyF
* If OptCrtFile not passed, defaults to *NO.
* If OptReplAdd not passed, defaults to '*REPLACE'.
*
DCpyF PR 7A
D FromFile 10A value
D FromLib 10A value
D ToFile 10A value
D ToLib 10A value
D OptParms 128A options(*NOPASS)
D OptCrtFile 4A value options(*NOPASS)
D OptReplAdd 8A value options(*NOPASS)
*
*=========================================================
C eval MsgID=CpyF(X_FILE:X_LIB
C :X_TOFILE:X_TOLIB:Parms)
C if MsgID<>*BLANKS
C callp DspError(MsgID:'X_FILE':Parms)
C endif
*============================================================
*===============================================================
* To compile:
*
* CRTRPGMOD MODULE(XXX/SYSTEM) SRCFILE(XXX/QRPGLESRC)
*
* CRTSRVPGM SRVPGM(XXX/SYSTEM) MODULE(XXX/SYSTEM) +
* EXPORT(*ALL) ACTGRP(SYSTEM) +
* BNDSRVPGM(COMMAND)
*===============================================================
H nomain
/COPY XXX/QCPYSRC,System
/COPY XXX/QCPYSRC,Command
P CpyF B export *
*CopyFile
Figure 5: CpyF prototype
Figure 6: CpyF in use in an interactive program
Figure 7: SYSTEM service program source (CpyF procedure only)
* If OptCrtFile not passed, defaults to *NO.
* If OptReplAdd not passed, defaults to '*REPLACE'.
*
DCpyF PI 7A
D FromFile 10A value
D FromLib 10A value
D ToFile 10A value
D ToLib 10A value
D OptParms 128A options(*NOPASS)
D OptCrtFile 4A value options(*NOPASS)
D OptReplAdd 8A value options(*NOPASS)
*
* Locals:
D MsgID S 7A inz(*BLANKS)
D Parms S 128A inz(*BLANKS)
D CrtFile S 4A inz('*NO')
D ReplAdd S 8A inz('*REPLACE') *
C if %parms 5
C eval CrtFile=OptCrtFile
C endif *
C if %parms 6
C eval ReplAdd=OptReplAdd
C endif *
C eval MsgID=ExecCmd(
C 'CPYF FROMFILE(' + %trim(FromLib)
C+ '/'
C + %trim(FromFile) + ') '
C + 'TOFILE(' + %trim(ToLib) + '/'
C + %trim(ToFile) + ') '
C + 'FROMMBR(*FIRST) '
C + 'TOMBR(*FIRST) '
C + 'MBROPT(' + %trim(ReplAdd) + ') '
C + 'CRTFILE(' + %trim(CrtFile) + ') '
C :Parms)
*
C if %parms 4
C eval OptParms=Parms
C endif *
C return MsgID *
PE *==================================================================
* Create Program. Returns error message, and loads OptParms, if
* unsuccessful.
* T_Modules is a dynamic table holding the names of all modules to
* be bound together.
* Element is 20 alpha, 1-10 is the module name, 11-20 is the
* library name.
* OptT_SrvPgms is a dynamic table of service programs to be bound
* together.
* Element is 20 alpha, 1-10 is the service pgm name, 11-20 is the
* library name.
* If OptActGrp not passed or blank, defaults to value of Pgm.
* If OptEntryMod not passed,defaults to first module in T_Modules.
*
D CrtPgm PR 7A
D Pgm 10A value
D PgmLib 10A value
D T_Modules 64A value
D OptT_SrvPgms 64A value options(*NOPASS)
D OptActGrp 10A value options(*NOPASS)
D OptEntryMod 64A value options(*NOPASS)
D OptParms 128A options(*NOPASS)
******* End of data ***********************************************
LATEST COMMENTS
MC Press Online