You are on page 1of 81

Procedures and Subprocedures

============================= A procedure is a routine that is called using a bound call. You can create two kinds of procedures in RPG: a main procedure and a subprocedure. A main procedure uses the RPG cycle. It is specified in the main source section. You do not need to code anything special to define the main procedure; it consists of everything before the first Procedure specification. The parameters for the main procedure can be coded using a prototype and procedure interface in the global Definition specifications, or using a *ENTRY PLIST in the main procedure's calculations. Any procedure interface found in the global definitions is assumed to be the procedure interface for the main procedure. The name is required for the procedure interface for the main procedure, and the prototype with the matching name must precede the procedure interface in the source. The name of the main procedure must be the same as the name of the module being created. You can either use this name for the prototype and procedure interface, or specify this name in the EXTPROC keyword of the prototype. In the following example, module CheckFile is created. The main procedure has three parameters: A file name (input) A library name (input) An indicator indicating whether the file was found (output) /COPY file CHECKFILEC with the prototype for the main procedure: D CheckFile D file D library D found PR 10a const 10a const 1N

Module CheckFile: /COPY CHECKFILEC D CheckFile PI D file 10a const D library 10a const D found 1N C ... code using parameters file, library and found Using a *ENTRY PLIST, you would define the parameters this way: D file S 10a const D library S 10a const D found S 1N C *ENTRY PLIST C PARM file C PARM library C PARM found C ... code using parameters file, library and found You can also use a prototype and procedure interface to define your main procedure as a program. In this case, you would specify the EXTPGM keyword for the prototype. /COPY file CHECKFILEC with the prototype for the program: D CheckFile D file D library PR extpgm('CHECKFILE') 10a const 10a const

D found

1N

In the module source, the procedure interface would be defined the same way. A subprocedure is a procedure specified after the main source section. It can only be called using a bound call. Subprocedures differ from main procedures in several respects, the main difference being that subprocedures do not (and cannot) use the RPG cycle while running. All subprocedures must have a corresponding prototype in the definition specifications of the main source section. The prototype is used by the compiler to call the program or procedure correctly, and to ensure that the caller passes the correct parameters. This chapter discusses the following aspects of subprocedures: Subprocedure Definition NOMAIN Module Mixing Main Procedures and Exported Subprocedures Subprocedures and Subroutines Subprocedure Definition Subprocedures are defined after the main source section. Figure 37 shows a subprocedure, highlighting the different parts of it. Figure 37. Example of a Subprocedure * Prototype for procedure FUNCTION D FUNCTION PR 10I 0 D TERM1 5I 0 VALUE D TERM2 5I 0 VALUE D TERM3 5I 0 VALUE

(1)

P Function B (2) *------------------------------------------------------------* This procedure performs a function on the 3 numeric values * passed to it as value parameters. * * This illustrates how a procedure interface is specified for a * procedure and how values are returned from a procedure. *------------------------------------------------------------D Function PI 10I 0 (3) D Term1 5I 0 VALUE D Term2 5I 0 VALUE D Term3 5I 0 VALUE D Result S 10I 0 (4) /free Result = Term1 ** 2 * 17 + Term2 * 7 (5) + Term3; return Result * 45 + 23; /end-free P E (6)

(1) A Prototype which specifies the name, return value if any, and parameters if any. (2) A Begin-Procedure specification (B in position 24 of a procedure specification) (3) A Procedure-Interface definition, which specifies the return value and parameters, if any. The procedure interface must match the corresponding prototype. The procedure-interface definition is optional if the subprocedure does not return a value and does not have any parameters that are passed to it. (4) Other definition specifications of variables, constants and prototypes needed by the subprocedure. These definitions are local definitions. (5) Any calculation specifications, |standard or free-form, needed to perform the task of the procedure. The calculations may refer to both local and global definitions. Any subroutines included within the subprocedure are local. They cannot be used outside of the subprocedure. If the subprocedure returns a value, then the subprocedure must contain a RETURN operation. (6) An End-Procedure specification (E in position 24 of a procedure specification) Except for the procedure-interface definition, which may be placed anywhere within the definition specifications, a subprocedure must be coded in the order shown above. No cycle code is generated for subprocedures. Consequently, you cannot code: Prerun-time and compile-time arrays and tables *DTAARA definitions Total calculations The calculation specifications are processed only once and the procedure returns at the end of the calculation specifications. See Subprocedure Calculations for more information. A subprocedure may be exported, meaning that procedures in other modules in the program can call it. To indicate that it is to be exported, specify the keyword EXPORT on the Procedure-Begin specification. If not specified, the subprocedure can only be called from within the module.

Procedure Interface Definition If a prototyped procedure has call parameters or a return value, then it must have a procedure interface definition. A procedure interface definition is a repeat of the prototype information within the definition of a procedure. It is used to declare the entry parameters for the procedure and to ensure that the internal definition of the procedure is consistent with the external definition (the prototype).
You specify a procedure interface by placing PI in the Definition-Type entry (positions 24-25). Any parameter definitions, indicated by blanks in positions 24-25, must immediately follow the PI specification. The procedure interface definition ends with the first definition specification with nonblanks in positions 24-25 or by a non-definition specification.

Return Values

A procedure that returns a value is essentially a user-defined function, similar to a built-in function. To define a return value for a subprocedure, you must Define the return value on both the prototype and procedure-interface definitions of the subprocedure. Code a RETURN operation with an expression in the extended-factor 2 field that contains the value to be returned. You define the length and the type of the return value on the procedure-interface specification (the definition specification with PI in positions 24-25). The following keywords are also allowed: DATFMT(fmt) The return value has the date format specified by the keyword. DIM(N) The return value is an array with N elements. LIKE(name) The return value is defined like the item specified by the keyword. PROCPTR The return value is a procedure pointer. TIMFMT(fmt) The return value has the time format specified by the keyword. To return the value to the caller, you must code a RETURN operation with an expression containing the return value. The expression in the extended-factor 2 field is subject to the same rules as an expression with EVAL. The actual returned value has the same role as the left-hand side of the EVAL expression, while the extended factor 2 of the RETURN operation has the same role as the right-hand side. You must ensure that a RETURN operation is performed if the subprocedure has a return value defined; otherwise an exception is issued to the caller of the subprocedure.

Scope of Definitions
Any items defined within a subprocedure are local. If a local item is defined with the same name as a global data item, then any references to that name inside the subprocedure use the local definition. However, keep in mind the following: Subroutine names and tag names are known only to the procedure in which they are defined, even those defined in the main procedure. All fields specified on input and output specifications are global. When a subprocedure uses input or output specifications (for example, while processing a read operation), the global name is used even if there is a local variable of the same name. When using a global KLIST or PLIST in a subprocedure some of the fields may have the same names as local fields. If this occurs, the global field is used. This may cause problems when setting up a KLIST or PLIST prior to using it. For example, consider the following source. Figure 38. Scope of Key Fields Inside a Module * Main procedure definitions D Fld1 S 1A

D Fld2

1A

* Define a global key field list with 2 fields, Fld1 and Fld2 C global_kl KLIST C KFLD Fld1 C KFLD Fld2 * Subprocedure Section P Subproc B D Fld2 S 1A * local_kl has one global kfld (fld1) and one local (fld2) C local_kl KLIST C KFLD Fld1 C KFLD Fld2 * Even though Fld2 is defined locally in the subprocedure, * the global Fld2 is used by the global_kl, since global KLISTs * always use global fields. As a result, the assignment to the * local Fld2 will NOT affect the CHAIN operation. C C C EVAL Fld1 = 'A' EVAL Fld2 = 'B' global_kl SETLL file

* Local KLISTs use global fields only when there is no local * field of that name. local_kl uses the local Fld2 and so the * assignment to the local Fld2 WILL affect the CHAIN operation. C EVAL Fld1 = 'A' C EVAL Fld2 = 'B' C local_kl SETLL file ... P E

NOMAIN Module
You can code one or more subprocedures in a module without coding a main procedure. Such a module is called a NOMAIN module, since it requires the specification of the NOMAIN keyword on the control specification. When there is no main procedure, no cycle code is generated for the NOMAIN module. TIP You may want to consider making all your modules NOMAIN modules except the ones that actually contain the program entry procedure for a program. The lack of the cycle code will reduce the size of the program. Since there is no main procedure, you are restricted in terms of what can be coded in the main source section. Specifically, you cannot code specifications for

Primary and secondary files Detail and total output Executable calculations (including an initialization subroutine) *ENTRY PLIST Instead you would code in the main source section: Full-procedural files Input specifications Definition specifications Declarative calculations such as DEFINE, KFLD, KLIST, PARM, and PLIST (but not *ENTRY PLIST) Exception output Note: A module with NOMAIN specified will not have a program entry procedure. Consequently you cannot use the CRTBNDRPG command to compile the source.

Mixing Main Procedures and Exported Subprocedures


If a module contains both a main procedure and exported subprocedures, take |great care to ensure that the RPG cycle in the main procedure does not |adversely affect the global data, files, and data areas that the |sub-procedures are using. You must be aware of when files are opened and closed implicitly, when data |areas are locked and unlocked implicitly, and when global data is initialized |or re-initialized.

Implicit Opening of Files and Locking of Data Areas


UDS data areas and files that do not have the USROPN keyword are opened or locked implicitly during module initialization and during main-procedure initialization. Module initialization occurs when the first procedure (either the main |procedure or a subprocedure) is called. Main procedure initialization occurs when the main procedure is called the |first time. It also occurs on subsequent calls if the main procedure ended abnormally or with LR on.

Implicit Closing of Files and Unlocking of Data Areas


UDS data areas and files that do not have the USROPN keyword are closed or unlocked implicitly during main procedure termination when the main procedure ends abnormally or with LR on.

Initialization of Global Data


Data is initialized during module initialization and during main procedure initialization.

Possible Problems
If module initialization occurs because a subprocedure is the first procedure to be called, and main procedure initialization occurs later, errors can occur if files are already open or data areas are already locked.

If a subprocedure calls the main procedure, global data may or may not be |reinitialized during the call, depending on the way the main procedure ended the last time it was called. If the subprocedure is using any global data, this can cause unexpected results.

Recommendations
Consider moving the main procedure logic into a subprocedure, and making the module a NOMAIN module. If you mix main procedures with exported subprocedures, ensure that your main procedure is called first, before any subprocedures. Do not allow main-procedure initialization to happen more than once, since this would reinitialize your global data. The best way to prevent reinitialization is to avoid using the LR indicator. If you want to call your main procedure intermixed with your subprocedures, you should declare all your files as USROPN and not use UDS data areas. Open files and lock data areas as you need them, and close files and unlock |data areas when you no longer need them. You might consider having a subprocedure in the module that will close any open files and unlock any locked data areas.

Subprocedures and Subroutines


A subprocedure is similar to a subroutine, except that a subprocedure offers the following improvements: You can pass parameters to a subprocedure, even passing by value. This means that the parameters used to communicate with subprocedures do not have to be modifiable. Parameters that are passed by reference, as they are with programs, must be modifiable, and so may be less reliable. The parameters passed to a subprocedure and those received by it are checked at compile time for consistency. This helps to reduce run-time errors, which can be more costly. You can use a subprocedure like a built-in function in an expression. When used in this way, they return a value to the caller. This basically allows you to custom-define any operators you might need in an expression. Names defined in a subprocedure are not visible outside the subprocedure. This means that there is less chance of the procedure inadvertently changing a item that is shared by other procedures. Furthermore, the caller of the procedure does not need to know as much about the items used inside the subprocedure. You can call the subprocedure from outside the module, if it is exported. You can call subprocedures recursively. Procedures are defined on a different specification type, namely, procedure specifications. This different type helps you to immediately recognize that you are dealing with a separate unit. If you do not require the improvements offered by subprocedures, you may want to use a subroutine because an EXSR operation is usually faster than a call to a subprocedure.

Standalone Fields

Standalone fields allow you to define individual work fields. A standalone field has the following characteristics: It has a specifiable internal data type It may be defined as an array, table, or field It is defined in terms of data length, not in terms of absolute byte positions.

Variable Initialization
You can initialize data with the INZ{(initial value)} keyword on the definition specification. Specify an initial value as a parameter on the INZ keyword, or specify the keyword without a parameter and use the default initial values. If the initialization is too complicated to express using the INZ keyword, you can further initialize data in the initialization subroutine. Default initial values for the various data types are described in Data Types and Data Formats. See Using Arrays and Tables for information on initializing arrays. To reinitialize data while the program is running, use the CLEAR and RESET operations. The CLEAR operation code sets a record format or variable (field, subfield, indicator, data structure, array, or table) to its default value. All fields in a record format, data structure, or array are cleared in the order in which they are declared. The RESET operation code restores a variable to its reset value. The reset value for a global variable is the value it had at the end of the initialization step in the RPG IV cycle, after the initialization subroutine has been invoked. You can use the initialization subroutine to assign initial values to a global variable and then later use RESET to set the variable back to this value. This applies only to the initialization subroutine when it is run automatically as a part of the initialization step. For local variables the reset value is the value of the variable when the subprocedure was first called, but before the calculations begin.

INZ{(initial value)}
The INZ keyword initializes the standalone field, data structure, |data-structure subfield, or object to the default value for its data type or, |optionally, to the constant specified in parentheses. For a program described data structure, no parameter is allowed for the INZ keyword. For an externally described data structure, only the *EXTDFT parameter is allowed. For a data structure that is defined with the LIKEDS keyword, the value *LIKEDS specifies that subfields are initialized in the same way as the parent data structure. For an object, only the *NULL parameter is allowed. Every object is initialized to *NULL, whether or not you specify INZ(*NULL).The initial value specified must be consistent with the type being initialized. The initial value can be a literal, named constant, figurative constant, built-in function, or one of the special values *SYS, *JOB, *EXTDFT, *USER,*LIKEDS, or *NULL. When initializing Date or Time data type fields or named constants with Date or Time values, the format of the literal must be consistent with the default format as derived from the Control specification, regardless of the actual format of the date or time field. A numeric field may be initialized with any type of numeric literal. However, a float literal can only be used with a float field. Any numeric field can be initialized with a hexadecimal literal of 16 digits or fewer. In this case, the hexadecimal literal is considered an unsigned numeric value.

Specifying INZ(*EXTDFT) initializes externally described data-structure subfields with the default values from the DFT keyword in the DDS. If no DFT or constant value is specified, the DDS default value for the field type is used. You can override the value specified in the DDS by coding INZ with or without a parameter on the subfield specification. Specifying INZ(*EXTDFT) on the external data structure definition, initializes all externally described subfields to their DDS default values. If the externally described data structure has additional program described subfields, these are initialized to the RPG default values. When using INZ(*EXTDFT), take note of the following: If the DDS value for a date or time field is not in the RPG internal format, the value will be converted to the internal format in effect for the program. External descriptions must be in physical files. If *NULL is specified for a null-capable field in the DDS, the compiler will use the DDS default value for that field as the initial value. If DFT('') is specified for a varying length field, the field will be initialized with a string of length 0. INZ(*EXTDFT) is not allowed if the CVTOPT option is in effect. Specifying INZ(*USER) intializes any character field or subfield to the name of the current user profile. Character fields must be at least 10 characters long. If the field is longer than 10 characters, the user name is left-justified in the field with blanks in the remainder. Date fields can be initialized to *SYS or *JOB. Time and Timestamp fields can be initialized to *SYS. A data structure, data-structure subfield, or standalone field defined with the INZ keyword cannot be specified as a parameter on an *ENTRY PLIST. Note: When the INZ parameter is not specified: Static standalone fields and subfields of initialized data structures are initialized to their RPG default initial values (for example, blanks for character, 0 for numeric). Subfields of uninitialized data structures (INZ not specified on the definition specification for the data structure) are initialized to blanks (regardless of their data type). This keyword is not valid in combination with BASED or IMPORT.

Prototypes and Parameters


The recommended way to call programs and procedures is to use prototyped calls, since prototyped calls allow the compiler to check the call interface at compile time. If you are coding a subprocedure, you will need to code a procedure-interface definition to allow the compiler to match the call interface to the subprocedure. This section describes how to define each of these concepts: Prototypes Prototyped Parameters Procedure Interface.

Prototypes
A prototype is a definition of the call interface. It includes the following information: Whether the call is bound (procedure) or dynamic (program) How to find the program or procedure (the external name)

The number and nature of the parameters Which parameters must be passed, and which are optionally passed Whether operational descriptors should be passed The data type of the return value, if any (for a procedure) A prototype must be included in the definition specifications of the program or procedure that makes the call. The prototype is used by the compiler to call the program or procedure correctly, and to ensure that the caller passes the correct parameters. The following rules apply to prototype definitions. A prototype name must be specified in positions 7-21. If the keyword EXTPGM or EXTPROC is specified on the prototype definition, then any calls to the program or procedure use the external name specified for that keyword. If neither keyword is specified, then the external name is the prototype name, that is, the name specified in positions 7-21 (in uppercase). Specify PR in the Definition-Type entry (positions 24-25). Any parameter definitions must immediately follow the PR specification. The prototype definition ends with the first definition specification with non-blanks in positions 24-25 or by a non-definition specification. Specify any of the following keywords as they pertain to the call interface: EXTPROC(name) The call will be a bound procedure call that uses the external name specified by the keyword. EXTPGM(name) The call will be an external program call that uses the external name specified by the keyword. OPDESC Operational descriptors are to be passed with the parameters that are described in the prototype. A return value (if any) is specified on the PR definition. Specify the length and data type of the return value. In addition, you may specify the following keywords for the return value: DATFMT(fmt) The return value has the date format specified by the keyword. DIM(N) The return value is an array with N elements. | |LIKEDS(data_structure_name) |The returned value is a data structure. (You cannot refer to the |subfields of the return value when you call the procedure.) LIKE(name) The return value is defined like the item specified by the keyword. PROCPTR The return value is a procedure pointer. TIMFMT(fmt) The return value has the time format specified by the keyword. VARYING A character, graphic, or UCS-2 return value has a variable-length format. For information on these keywords, see Definition-Specification Keywords. Figure 63 shows a prototype for a subprocedure CVTCHR that takes a numeric input parameter and returns a character string. Note that there is no name associated with the return value. For this reason, you cannot display its contents when debugging the program. Figure 63. Prototype for CVTCHR

* The returned value is the character representation of * the input parameter NUM, left-justified and padded on * the right with blanks. D CVTCHR PR 31A D NUM 30P 0 VALUE * The following expression shows a call to CVTCHR. If * variable rrn has the value 431, then after this EVAL, * variable msg would have the value * 'Record 431 was not found.' C EVAL msg = 'Record ' C + %TRIMR(CVTCHR(RRN)) C + ' was not found '

If you are writing a prototype for an exported subprocedure or for a |main procedure, put the prototype in a /COPY file and copy the prototype into |the source file for both the callers and the module that defines the |procedure. This coding technique provides maximum parameter-checking | benefits for both the callers and the procedure itself, since they all use the |same prototype.

Prototyped Parameters
If the prototyped call interface involves the passing of parameters then you must define the parameter immediately following the PR specification. The following keywords, which apply to defining the type, are allowed on the parameter definition specifications: ASCEND The array is in ascending sequence. DATFMT(fmt) The date parameter has the format fmt. DIM(N) The parameter is an array with N elements. LIKEDS(data_structure_name) The parameter is a data structure whose subfields are the same as the subfields identified in the LIKEDS keyword. LIKE(name) The parameter is defined like the item specified by the keyword. PROCPTR The parameter is a procedure pointer. TIMFMT(fmt) The time parameter has the format fmt. VARYING A character, graphic, or UCS-2 parameter has a variable-length format. For information on these keywords, see Definition-Specification Keywords. The following keywords, which specify how the parameter should be passed, are also allowed on the parameter definition specifications:

CONST The parameter is passed by read-only reference. A parameter defined with CONST must not be modified by the called program or procedure. This parameter-passing method allows you to pass literals and expressions. NOOPT The parameter will not be optimized in the called program or procedure. OPTIONS(opt1 { : opt2 { : opt3 { : opt4 { : opt5 } } } }) Where opt1 ... opt5 can be *NOPASS, *OMIT, *VARSIZE, *STRING, or *RIGHTADJ. For example, OPTIONS(*VARSIZE : *NOPASS). Specifies the following parameter passing options: *NOPASS The parameter does not have to be passed. If a parameter has OPTIONS(*NOPASS) specified, then all parameters following it must also have OPTIONS(*NOPASS) specified. *OMIT The special value *OMIT may be passed for this reference parameter. *VARSIZE The parameter may contain less data than is indicated on the definition. This keyword is valid only for character parameters, graphic parameters, UCS-2 parameters, or arrays passed by reference. The called program or procedure must have some way of determining the length of the passed parameter. Note: When this keyword is omitted for fixed-length fields, the parameter may only contain more or the same amount of data as indicated on the definition; for variable-length fields, the parameter must have the same declared maximum length as indicated on the definition. *STRING Pass a character value as a null-terminated string. This keyword is valid only for basing pointer parameters passed by value or by read-only reference. *RIGHTADJ For a CONST or VALUE parameter, *RIGHTADJ indicates that the graphic, UCS-2, or character parameter value is to be right adjusted. TIP For the parameter passing options *NOPASS, *OMIT, and *VARSIZE, it is up to the programmer of the procedure to ensure that these options are handled. For example, if OPTIONS(*NOPASS) is coded and you choose not to pass the parameter, the procedure must check that the parameter was passed before it accesses it. The compiler will not do any checking for this. VALUE The parameter is passed by value.

Procedure Interface
If a prototyped program or procedure has call parameters or a return value, then a procedure interface definition must be defined, either in the main source section (for a main procedure) or in the subprocedure section. A procedure interface definition repeats the prototype information within the definition of a procedure. It is used to declare the entry parameters for the procedure and to ensure that the internal definition of the procedure is consistent with the external definition (the prototype).

The following rules apply to procedure interface definitions. The name of the procedure interface, specified in positions 7-21, is required for the main procedure. It is optional for subprocedures. If specified, it must match the name specified in positions 7-21 on the corresponding prototype definition. Specify PI in the Definition-Type entry (positions 24-25). The procedure-interface definition can be specified anywhere in the definition specifications. In the main procedure, the procedure interface must be preceded by the prototype that it refers to. A procedure interface is required in a subprocedure if the procedure returns a value, or if it has any parameters; otherwise, it is optional. Any parameter definitions, indicated by blanks in positions 24-25, must immediately follow the PI specification. Parameter names must be specified, although they do not have to match the names specified on the prototype. All attributes of the parameters, including data type, length, and dimension, must match exactly those on the corresponding prototype definition. To indicate that a parameter is a data structure, use the LIKEDS keyword to define the parameter with the same subfields as another data |structure. The keywords specified on the PI specification and the parameter specifications must match those specified on the prototype.

TIP
If a module contains calls to a prototyped program or procedure, then there must be a prototype definition for each program and procedure that you want to call. One way of minimizing the required coding is to store shared prototypes in /COPY files. If you provide prototyped programs or procedures to other users, be sure to provide them with the prototypes (in /COPY files) as well.

Important Tit Bits An EXSR operation runs much faster than a bound call There are Two Error Subroutines in the As400, these are *PSSR and INFSR ILE allows you to directly manage run-time storage from your program by managing heaps. A heap is an area of storage used for allocations of dynamic storage. The amount of dynamic storage required by an application depends on the data being processed by the programs and procedures that use the heap. There are two types of heaps available on the system: a default heap and a user-created heap. The RPG storage management operations use the default heap. The following sections show how to use RPG storage management operations with the default heap, and also how to create and use your own heap using the storage management APIs. For more information on user-created heaps and other ILE storage management concepts refer to ILE Concepts . Important Question

Are tools like SDA, PRTF tools available with every IBM software which has RPG or are loaded as an additional tool set. What de me mean bv binding directory in case of RPG, and where the binding directory are used. What do we mean by threading, what are the uses of threads and where are the threads used. Chapter 4 for Procedures. Web site Address About Visual Age and Code/400 we can get more on the below mentioned site. http://www.software.ibm.com/ad/varpg/ about the Threads http://www.as400.ibm.com/infocenter/ 1.2.3 Program Call ( Difference B/w Static Call and Dynamic Call ) In ILE, you can write applications in which ILE RPG programs and OPM RPG/400_ programs continue to interrelate through the traditional use of dynamic program calls. When using such calls, the calling program specifies the name of the called program on a call statement. The called program's name is resolved to an address at run time, just before the calling program passes control to the called program. You can also write ILE applications that can interrelate with faster static calls. Static calls involve calls between procedures. A procedure is a self-contained set of code that performs a task and then returns to the caller. An ILE RPG module consists of an optional main procedure followed by zero or more subprocedures. Because the procedure names are resolved at bind time (that is, when you create the program), static calls are faster than dynamic calls. Static calls also allow Operational descriptors Omitted parameters The passing of parameters by value The use of return values A greater number of parameters to be passed Operational descriptors and omitted parameters can be useful when calling bindable APIs or procedures written in other ILE languages. 2.6 Chapter 10. Calling Programs and Procedures

In ILE, it is possible to call either a program or procedure. Furthermore, ILE RPG provides the ability to call prototyped or non-prototyped programs and procedures. (A prototype is an external definition of the call interface that allows the compiler to check the interface at compile time.) The recommended way to call a program or procedure is to use a prototyped call. The syntax for calling and passing parameters to prototyped procedures or programs uses the same freeform syntax that is used with built-in functions or within expressions. For this reason, a prototyped call is sometimes referred to as a 'free-form' call. Use the CALL or CALLB operations to call a program or procedure when: You have an extremely simple call interface You require the power of the PARM operation with factor 1 and factor 2. You want more flexibility than is allowed by prototyped parameter checking. This chapter describes how to: Call a program or procedure Use a prototyped call Pass prototyped parameters Use a fixed-form call Return from a program or procedure Use ILE bindable APIs Call a Graphics routine Call special routines 1.4.1.2 Prototyped Calls To call a subprocedure, you must use a prototyped call. You can also call any program or procedure that is written in any language in this way. A prototyped call is one where the call interface is checked at compile time through the use of a prototype. A prototype is a definition of the call interface. It includes the following information: Whether the call is bound (procedure) or dynamic (program) How to find the program or procedure (the external name) The number and nature of the parameters

Which parameters must be passed, and which are optionally passed Whether operational descriptors are passed (for a procedure) The data type of the return value, if any (for a procedure) The prototype is used by the compiler to call the program or procedure correctly, and to ensure that the caller passes the correct parameters. Figure 13 shows a prototype for a procedure FmtCust, which formats various fields of a record into readable form. It has two output parameters. +--------------------------------------------------------------------------------------------------+ * Prototype for procedure FmtCust (Note the PR on definition * specification.) It has two output parameters. D FmtCust PR D Name 100A D Address 100A +--------------------------------------------------------------------------------------------------+ Figure 13. Prototype for FmtCust Procedure To produce the formatted output fields, FmtCust calls a procedure NumToChar. NumToChar has a numeric input parameter that is passed by value, and returns a character field. Figure 14 shows the prototype for NumToChar. +--------------------------------------------------------------------------------------------------+ * Prototype for procedure NumToChar * The returned value is a character field of length 31. D NumToChar PR 31A * The input parameter is packed with 30 digits and 0 decimal * positions, passed by value. D NUMPARM 30P 0 VALUE +--------------------------------------------------------------------------------------------------+ Figure 14. Prototype for NumToChar Procedure If the program or procedure is prototyped, you call it with CALLP or within an expression if you want to use the return value. You pass parameters in a list that follows the name of the prototype, for example, name (parm1 : parm2 : ...).

Figure 15 shows a call to FmtCust. Note that the names of the output parameters, shown above in Figure 13, do not match those in the call statement. The parameter names in a prototype are for documentation purposes only. The prototype serves to describe the attributes of the call interface. The actual definition of call parameters takes place inside the procedure itself. +--------------------------------------------------------------------------------------------------+ C CALLP FmtCust(RPTNAME : RPTADDR) +--------------------------------------------------------------------------------------------------+ Figure 15. Calling the FmtCust Procedure Using prototyped calls you can call (with the same syntax): Programs that are on the system at run time Exported procedures in other modules or service programs that are bound in the same program or service program Subprocedures in the same module In order to format the name and address properly, FmtCust calls NumToChar to convert the customer number to a character field. Because FmtCust wants to use the return value, the call to NumToChar is made in an expression. Figure 16 shows the call. +--------------------------------------------------------------------------------------------------+ *------------------------------------------------------------- * CUSTNAME and CUSTNUM are formatted to look like this: * A&P Electronics (Customer number 157) *------------------------------------------------------------- C EVAL Name = CUSTNAME + ' ' C + '(Customer number ' C + %trimr(NumToChr(CUSTNUM)) + ')' +--------------------------------------------------------------------------------------------------+ Figure 16. Calling the NumToChar Procedure

The use of procedures to return values, as in the above figure, allows you to write any userdefined function you require. In addition, the use of a prototyped call interface opens up a number of new options for parameter passing.

Prototyped parameters can be passed in several ways: by reference, by value (for procedures only), or by read-only reference. The default method for RPG is to pass by reference. However, passing by value or by read-only reference gives you more options for passing parameters. If the prototype indicates that it is allowed for a given parameter, you may be able to do one or more of the following: - Pass *OMIT - Leave out a parameter entirely Pass a shorter parameter than is specified (for character and graphic parameters, and for array parameters)

1.4.3.3 Main Procedure Considerations Because the main procedure is the only procedure with a complete set of specifications available (except the P specification), it should be used to set up the environment of all procedures in the module. A main procedure is always exported, which means that other procedures in the program can call the main procedure by using bound calls. The call interface of a main procedure can be defined in one of two ways: 1. Using a prototype and procedure interface 2. Using an *ENTRY PLIST without a prototype The functionality of an *ENTRY PLIST is similar to a prototyped call interface. However, a prototyped call interface is much more robust since it provides parameter checking at compile time. If you prototype the main procedure, then you specify how it is to be called by specifying either the EXTPROC or EXTPGM keyword on the prototype definition. If EXTPGM is specified, then an external program call is used; if EXTPROC is specified or if neither keyword is specified, it will be called by using a procedure call. You cannot define return values for a main procedure, nor can you specify that its parameters be passed by value. 1.2.5 Bindable APIs ILE offers a number of bindable APIs that can be used to supplement the function currently offered by ILE RPG. The bindable APIs provide program calling and activation capability, condition and storage management, math functions, and dynamic screen management. Some APIs that you may wish to consider using in an ILE RPG application include: CEETREC - Signal the Termination-Imminent Condition

CEE4ABN - Abnormal End CEECRHP - Create your own heap CEEDSHP - Discard your own heap CEEFRST - Free Storage in your own heap CEEGTST - Get Heap Storage in your own heap CEECZST - Reallocate Storage in your own heap CEEDOD - Decompose Operational Descriptor Note: You cannot use these or any other ILE bindable APIs from within a program created with DFTACTGRP(*YES). This is because bound calls are not allowed in this type of program. 1.2.6 Multithreaded Applications The AS/400 now supports multithreading. ILE RPG does not directly support initiating or managing program threads. However, ILE RPG procedures can run as threads in multithreaded environments. If you want to call an ILE RPG procedure in a multithreaded application, you must ensure that the ILE RPG procedure is threadsafe. You must also ensure that any system functions that your procedure accesses are also threadsafe. The THREAD(*SEREALIZE) control specification keyword can be specified to help you achieve thread safety for an ILE RPG module. Specifying THREAD(*SERIALIZE) will protect most of your variables and all your internal control structures from being accessed improperly by multiple threads. The thread safe module will be locked when a procedure in the module is entered and unlocked when when no procedure in the module is still running. This serialized access, ensures that only one thread is active in any one module, within an activation group, at any one time. However, it is still up to the programmer to handle thread safety for storage that is shared across modules. This is done by adding logic in the application to synchronize access to the storage. Normally, running an application in multiple threads can improve the performance of the application. In the case of ILE RPG, this is not true in general. In fact, the performance of a multithreaded application could be worse than that of a single-thread version when the threadsafety is achieved by serialization of the procedures at the module level. Running ILE RPG procedures in a multithreaded environment is only recommended when required by other aspects of the application (for example, when writing a Domino exit program or when calling a short-running RPG procedure from Java). For long-running RPG programs called from Java, we recommend using a separate process for the RPG program. The THREAD(*SERIALIZE) control specification keyword can be specified to help you achieve thread safety for an ILE RPG module. Specifying THREAD(*SERIALIZE) will protect most of your variables and all your internal control structures from being accessed improperly by multiple threads. The thread safe module will be locked when a procedure in the

module is entered and unlocked when no procedure in the module is still running. This serialized access, ensures that only one thread is active in any one module, within an activation group, at any one time. However, it is still up to the programmer to handle thread safety for storage that is shared across modules. This is done by adding logic in the application to synchronize access to the storage. For example, shared files, exported and imported storage, and storage accessed by the address of a parameter may be shared across modules from multiple threads. To synchronize access to this type of storage, you can do one or both of the following: Structure the application such that the shared resources are not accessed simultaneously from multiple threads. If you are going to access resources simultaneously from separate threads, synchronize access using facilities such as semaphores or mutexes. For more information, refer to the Multithreaded Applications document under the Programming topic at the following URL:

2.5.6.5 Reclaim Resources Command The Reclaim Resources (RCLRSC) command is designed to free the resources for programs that are no longer active. The command works differently depending on how the program was created. If the program is an OPM program or was created with DFTACTGRP(*YES), then the RCLRSC command will close open files and free static storage. For ILE programs or service programs that were activated into the OPM default activation group because they were created with *CALLER, files will be closed when the RCLRSC command is issued. For programs, the storage will be re-initialized; however, the storage will not be released. For service programs, the storage will neither be re-initialized nor released. Note: This means that if you have a service program that ran in the default activation group and left files open (returning with LR off), and a RCLRSC is issued, when you call the service program again, the files will still appear to be open, so so any I/O operations will result in an error. For ILE programs associated with a named activation group, the RCLRSC command has no effect. You must use the RCLACTGRP command to free resources in a named activation group. 3.2.1.1.3 Differences between OPM and ILE RPG Exception Handling For the most part, exception handling behaves the same in OPM RPG and ILE RPG. The key difference lies in the area of unhandled exceptions. In OPM, if an exception occurs and there is no RPG-specific handler enabled, then an inquiry message is issued. In ILE, this will only occur if the exception is a function check. If it is not, then the exception will be passed to the caller of the procedure or program, and any eligible higher call stack entries are given a chance to handle the exception. For example, consider the following example:

PGM A calls PGM B, which in turn calls PGM C. PGM B has an error indicator coded for the call. PGM C has no error indicator or *PSSR error subroutine coded. PGM C gets an exception. In OPM, an inquiry message would be issued for PGM C. In ILE, the exception is percolated to PGM B, since it is unhandled by PGM C. The error indicator in PGM B is turned on allowing PGM B to handle the error, and in the process PGM C ends abnormally. There is no inquiry message. If PGM C has a *PSSR error subroutine coded, then in both OPM and ILE, the exception is handled by PGM C and the error subroutine is run. Note: Inquiry messages issued by ILE RPG will start with the prefix 'RNQ', not 'RPG', as in OPM RPG. Certain behavioral differences exist for some specific errors. See Appendix A, "Behavioral Differences Between OPM RPG/400 and ILE RPG for AS/400" in topic APPENDIX1.1 for further information. 2.5.6 Managing Activation Groups An activation group is a substructure of a job and consists of system resources (for example, storage, commitment definitions, and open files) that are allocated to run one or more ILE or OPM programs. Activation groups make it possible for ILE programs running in the same job to run independently without intruding on each other (for example, commitment control and overrides). The basic idea is that all programs activated within one activation group are developed as one cooperative application. You identify the activation group that your ILE program will run in at the time of program creation. The activation group is determined by the value specified on the ACTGRP parameter when the program object was created. (OPM programs always run in the default activation group; you cannot change their activation group specification.) Once an ILE program (object type *PGM) is activated, it remains activated until the activation group is deleted. 2.4.1 Service Program Overview A service program is a bound program (type *SRVPGM) consisting of a set of procedures that can be called by procedures in other bound programs. Service programs are typically used for common functions that are frequently called within an application and across applications. For example, the ILE compilers use service programs to

provide run-time services such as math functions and input/output routines. Service programs enable reuse, simplify maintenance, and reduce storage requirements. A service program differs from a program in two ways: It does not contain a program entry procedure. This means that you cannot call a service program using the CALL operation. A service program is bound into a program or other service programs using binding by reference. When you bind a service program to a program, the contents of the service program are not copied into the bound program. Instead, linkage information of the service program is bound into the program. This is called 'binding by reference' in contrast to the static binding process used to bind modules into programs. Because a service program is bound by reference to a program, you can call the service program's exported procedures using bound procedure calls. The initial call has a certain amount of overhead because the binding is not completed until the service program is called. However, subsequent calls to any of its procedures are faster than program calls. The set of exports contained in a service program are the interface to the services provided by it. You can use the Display Service Program (DSPSRVPGM) command or the service program listing to see what variable and procedure names are available for use by the calling procedures. To see the exports associated with service program PAYROLL, you would enter: DSPSRVPGM PAYROLL DETAIL(*PROCEXP *DATAEXP) 2.4.2 Strategies for Creating Service Programs When creating a service program, you should keep in mind: 1. Whether you intend to update the program at a later date 2. Whether any updates will involve changes to the interface (namely, the imports and exports used). If the interface to a service program changes, then you may have to re-bind any programs bound to the original service program. However, if the changes required are upwardcompatible, you may be able to reduce the amount of re-binding if you created the service program using binder language. In this case, after updating the binder language source to identify the new exports you need to re-bind only those programs that use them. +--- TIP ----------------------------------------------------------------+ If you are planning a module with only subprocedures (that is, with a

module with keyword NOMAIN specified on the control specification) you may want to create it as a service program. Only one copy of a service program is needed on a system, and so you will need less storage for the module. Also, you can copyright your service programs using the COPYRIGHT keyword on the control specification. +------------------------------------------------------------------------+

Binder language gives you control over the exports of a service program. This control can be very useful if you want to: Mask certain service program procedures from service-program users Fix problems Enhance function Reduce the impact of changes to the users of an application. 2.4.4 Sample Service Program The following example shows how to create a service program CVTTOHEX which converts character strings to their hexadecimal equivalent. Two parameters are passed to the service program: 1. a character field (InString) to be converted 2. a character field (HexString) which will contain the 2-byte hexadecimal equivalent The field HexString is used to contain the result of the conversion and also to indicate the length of the string to be converted. For example, if a character string of 30 characters is passed, but you are only interested in converting the first ten, you would pass a second parameter of 20 bytes (2 times 10). Based on the length of the passed fields, the service program determines the length to handle. Figure 42 shows the source for the service program. Figure 43 shows the /COPY member containing the prototype for CvtToHex. The basic logic of the procedure contained within the service program is listed below: 1. Operational descriptors are used to determine the length of the passed parameters. 2. The length to be converted is determined: it is the lesser of the length of the character string, or one-half the length of the hex string field.

3. Each character in the string is converted to a two-byte hexadecimal equivalent using the subroutine GetHex. Note that GetHex is coded as a subroutine rather than a subprocedure, in order to improve run-time performance. An EXSR operation runs much faster than a bound call, and in this example, GetHex is called many times. 4. The procedure returns to its caller.

The service program makes use of operational descriptors, which is an ILE construct used when the precise nature of a passed parameter is not known ahead of time, in this case the length. The operational descriptors are created on a call to a procedure when you specify the operation extender (D) on the CALLB operation, or when OPDESC is specified on the prototype. To use the operational descriptors, the service program must call the ILE bindable API, CEEDOD (Retrieve Operational Descriptor). This API requires certain parameters which must be defined for the CALLB operation. However, it is the last parameter which provides the information needed, namely, the length. For more information on operational descriptors, see "Using Operational Descriptors" in topic 2.6.3.2. +--------------------------------------------------------------------------------------------------+ *=================================================================* * CvtToHex - convert input string to hex output string *=================================================================* H COPYRIGHT('(C) Copyright MyCompany 1995') D/COPY RPGGUIDE/QRPGLE,CVTHEXPR *-----------------------------------------------------------------* * Main entry parameters * 1. Input: string character(n) * 2. Output: hex string character(2 * n) *-----------------------------------------------------------------* D CvtToHex PI OPDESC D InString 16383 CONST OPTIONS(*VARSIZE) D HexString 32766 OPTIONS(*VARSIZE) *-----------------------------------------------------------------* * Prototype for CEEDOD (Retrieve operational descriptor) *-----------------------------------------------------------------* D CEEDOD PR D ParmNum 10I 0 CONST

D D D D D D

10I 0 10I 0 10I 0 10I 0 10I 0 12A OPTIONS(*OMIT)

* Parameters passed to CEEDOD D DescType S 10I 0 D DataType S 10I 0 D DescInfo1 S 10I 0 D DescInfo2 S 10I 0 D InLen S 10I 0 D HexLen S 10I 0

*-----------------------------------------------------------------* * Other fields used by the program * *-----------------------------------------------------------------* D HexDigits C CONST('0123456789ABCDEF') D IntDs DS D IntNum 5I 0 INZ(0) D IntChar 1 OVERLAY(IntNum:2) D HexDs DS D HexC1 1 D HexC2 1 D InChar S 1 D Pos S 5P 0 D HexPos S 5P 0 *-----------------------------------------------------------------* * Use the operational descriptors to determine the lengths of * * the parameters that were passed. * *-----------------------------------------------------------------* C CALLP CEEDOD(1 : DescType : DataType : C DescInfo1 : DescInfo2: Inlen : C *OMIT) C CALLP CEEDOD(2 : DescType : DataType : C DescInfo1 : DescInfo2: HexLen : C *OMIT) *-----------------------------------------------------------------* * Determine the length to handle (minimum of the input length * * and half of the hex length) * *-----------------------------------------------------------------* C IF InLen > HexLen / 2 C EVAL InLen = HexLen / 2 C ENDIF

*-----------------------------------------------------------------* * For each character in the input string, convert to a 2-byte * * hexadecimal representation (for example, '5' --> 'F5') * *-----------------------------------------------------------------* C EVAL HexPos = 1 C DO InLen Pos C EVAL InChar = %SUBST(InString : Pos :1) C EXSR GetHex C EVAL %SUBST(HexString : HexPos : 2) = HexDs C EVAL HexPos = HexPos + 2 C ENDDO *-----------------------------------------------------------------* * Done; return to caller. * *-----------------------------------------------------------------* C RETURN *=================================================================* * GetHex - subroutine to convert 'InChar' to 'HexDs' * * * * Use division by 16 to separate the two hexadecimal digits. * * The quotient is the first digit, the remainder is the second. * *=================================================================* C GetHex BEGSR C EVAL IntChar = InChar C IntNum DIV 16 X1 50 C MVR X2 50 *-----------------------------------------------------------------* * Use the hexadecimal digit (plus 1) to substring the list of * * hexadecimal characters '012...CDEF'. * *-----------------------------------------------------------------* C EVAL HexC1 = %SUBST(HexDigits:X1+1:1) C EVAL HexC2 = %SUBST(HexDigits:X2+1:1) C ENDSR +--------------------------------------------------------------------------------------------------+ Figure 42. Source for Service Program CvtToHex +--------------------------------------------------------------------------------------------------+ *=================================================================*

* CvtToHex - convert input string to hex output string * * Parameters * 1. Input: string character(n) * 2. Output: hex string character(2 * n) *=================================================================* D CvtToHex PR OPDESC D InString 16383 CONST OPTIONS(*VARSIZE) D HexString 32766 OPTIONS(*VARSIZE) +--------------------------------------------------------------------------------------------------+ Figure 43. Source for /COPY Member with Prototype for CvtToHex When designing this service program, it was decided to make use of binder language to determine the interface, so that the program could be more easily updated at a later date. Figure 44 shows the binder language needed to define the exports of the service program CVTTOHEX. This source is used in the EXPORT, SRCFILE and SRCMBR parameters of the CRTSRVPGM command. +--------------------------------------------------------------------------------------------------+ STRPGMEXP SIGNATURE('CVTHEX') EXPORT SYMBOL('CVTTOHEX') ENDPGMEXP +--------------------------------------------------------------------------------------------------+ Figure 44. Source for Binder Language for CvtToHex The parameter SIGNATURE on STRPGMEXP identifies the interface that the service program will provide. In this case, the export identified in the binder language is the interface. Any program bound to CVTTOHEX will make use of this signature. The binder language EXPORT statements identify the exports of the service program. You need one for each procedure whose exports you want to make available to the caller. In this case, the service program contains one module which contains one procedure. Hence, only one EXPORT statement is required. 2.2.3 Accessing the RETURNCODE Data Area Both the CRTBNDRPG and CRTRPGMOD (see "Using the CRTRPGMOD Command" in topic 2.3.1.1) commands create and update a data area with the status of the last compilation. This data area is named RETURNCODE, is 400 characters long, and is placed into library QTEMP.

To access the RETURNCODE data area, specify RETURNCODE in factor 2 of a *DTAARA DEFINE statement. The data area RETURNCODE has the following format: Byte Content and Meaning

1 For CRTRPGMOD, character '1' means that a module was created in the specified library. For CRTBNDRPG, character '1' means a module with the same name as the program name was created in QTEMP. 2 3 4 Character '1' means that the compilation failed because of compiler errors. Character '1' means that the compilation failed because of source errors. Not set. Always '0'.

5 Character '1' means the translator was not called because either OPTION(*NOGEN) was specified on the CRTRPGMOD or CRTBNDRPG command; or the compilation failed before the translator was called. 6-10 11-12 13-14 Number of source statements Severity level from command Highest severity of diagnostic messages

15-20 Number of errors that are found in the module (CRTRPGMOD) or program (CRTBNDRPG). 21-26 27-32 Compile date Compile time

33-100 Not set. Always blank 101-110 Module (CRTRPGMOD) name or program (CRTBNDRPG) name. 111-120 Module (CRTRPGMOD) library name or program (CRTBNDRPG) library name. 121-130 Source file name 131-140 Source file library name 141-150 Source file member name 151-160 Compiler listing file name 161-170 Compiler listing library name

171-180 Compiler listing member name 181-329 Not set. Always blank 330-334 Total elapsed compile time to the nearest 10th of a second 335 Not set. Always blank

336-340 Elapsed compile time to the nearest 10th of a second 341-345 Elapsed translator time to the nearest 10th of a second 346-379 Not set. Always blank 380-384 Total compile CPU time to the nearest 10th of a second 385 Not set. Always blank

386-390 CPU time that is used by compiler to the nearest 10th of a second 391-395 CPU time that is used by the translator to the nearest 10th of a second 396.400 Not set. Always blank

Subprocedures: Better than Subroutines


by Ted Holt Many RPG programmers no longer use conditioning or resulting indicators in calculation specs. Many no longer use the cycle. Many no longer define variables in C-specs. Maybe it's time RPG programmers quit using subroutines. My RPG II teacher did not like subroutines and did not teach his students to use them. It wasn't until I began writing production code that I learned the value of subroutines. But since then, I've found something even better than subroutines: subprocedures. Subprocedures have all the advantages of subroutines and then some. In this article, I point out the advantages of using subprocedures rather than subroutines.

Subroutines Are Wonderful


I think the greatest benefit of using subroutines is intellectual manageability. That is, I can think of a program as a group of related small tasks, all of which are simple enough for me to understand, rather than as one gargantuan task that is more than I can handle. I believe--but cannot prove--that my programs have

fewer bugs because I tackle each task separately. Debugging seems to be easier because I can often determine which subroutine most likely contains an error. Finding a logic error in a program of subroutines is similar to determining why my car won't crank. I don't check to see if there is air in the tires, because I know that a lack of air in the tires won't prevent my car from cranking. I would look to see if there is gas in the tank or check whether the battery is dead. Likewise, if there was a mistake in the discount a customer received, I would begin my search for the error at the routine that calculates discounts. Subroutines also promote the reusability of code. If I developed a subroutine to calculate a profit margin, I would be able to copy it to other programs that needed to calculate profit margins. However, subroutines have their limitations, two of which really bother me. The first is that subroutines use global variables only. That is, any variable or constant or indicator that I use in a subroutine may be used anywhere else in the program--in the main calculations, in any subroutine, in output specs. This can lead to undesirable side effects. Changing the value of a variable in one part of the program causes something to go wrong in another part of a program. Global variables also work against the portability of code. For instance, I may have a dandy subroutine that calculates a scheduling function of some sort, but copying it to another program may require me to rename a lot of variables. For example, maybe the two programs use different input files, in which case field names are different. Or maybe the work variables in the subroutine have already been used for other purposes in the second program. In such cases, I'm less likely to reuse the subroutine. The other thing that bothers me is that I can't define parameters to pass data to subroutines. A subroutine that verifies a general ledger account number might need to verify several different account number variables within one program, but there is no way to pass each different account number variable to the subroutine.

Subprocedures Are Even More Wonderful


Sub procedures directly address these two problems. First, sub procedures allow you to define local variables. A local variable is one that is understood only with within the sub procedure and cannot be referenced outside of it. A local variable can have the same name as a global variable. The two are separate variables, and the compiler will not confuse them. The global and local variables of the same name do not have to be defined identically. Do you understand the ramifications of this? You can create a work variable in a sub procedure and not have to worry that there may be another variable of the same name in the main part of the program. This increases my chances of porting a sub procedure from one program to another. Parameters also make sub procedures more portable by providing internal names (i.e., internal to the subroutine) for required data values. The sub procedure does not have to know the names of variables, constants, and fields in database files or other parts of the program in order to do its assigned task.

An Example Would Be Good


If sub procedures are harder to code than subroutines, it's not by much. A subprocedure needs a procedure prototype, but that's no big deal; I just copy the procedure interface from the subprocedure and change the "pi" line to a "pr" line. The CALLP op code is no harder to code than EXSR. To illustrate, here are two versions of a routine to calculate income tax in a mythical state. I've included them inside part of a "gross-to-net" payroll program. The first is implemented as a subroutine, the second

as a sub procedure. If you compare them, you'll see there is little difference. First, here's the subroutine. Notice the identifiers that begin with PW. These are fields in the payroll work file, which this subroutine updates. The only work variable, TaxablePay, is defined in the D specs.
Fpaywork uf e s read dow exsr update read enddo eval CalcStateTax begsr PWStateTax = *zero disk 7p 2 PayRec not %eof(PayWork) CalcStateTax PayRec PayRec *inLR = *on D TaxablePay C C C C C C C* C C C* C C* C C C* C C C C C C C C* C C C C* C C C C* C C C C* C* C C C C C C C C C* C C C

eval calculate annual pay eval

TaxablePay = (PWGross * PWNbrPer) deduct allowance for employee & spouse if PWMarStat = 'M' eval TaxablePay = (TaxablePay - 10000) else eval TaxablePay = (TaxablePay - 5000) endif if zero or less, no tax due if TaxablePay <= *zero leavesr endif deduct allowance for dependents eval TaxablePay = (TaxablePay (PWNbrDep * 2500)) if zero or less, no tax due if TaxablePay <= *zero leavesr endif yearly tax is 3% of first $5,000 4% of remainder if TaxablePay <= 5000 eval PWStateTax = (TaxablePay * 0.03) else eval PWStateTax = (150 + (TaxablePay - 5000) * 0.04) endif get this period's portion of yearly tax eval PWStateTax = (PWStateTax / PWNbrPer) endsr

Here's the sub procedure. There are no references to global variables. The TaxablePay variable is now defined in the sub procedure. There may be other variables named TaxablePay in other tax routines, but they will not conflict with or affect this one. The sub procedure does not directly reference the fields from the payroll work file. Instead, all fields are passed to the sub procedure through parameters.
H dftactgrp(*no) actgrp('QILE')

Fpaywork

uf

disk

* prototype for state tax routine DCalcStateTax pr D StateTax 7p D Gross 7p D NbrPer 3p D MarStat 1 D NbrDep 3p C C C C C C C C C C C C* C read dow callp

2 2 value 0 value value 0 value

update read enddo eval

PayRec not %eof(PayWork) CalcStateTax (PWStateTax: PWGross: PWNbrPer: PWMarStat: PWNbrDep) PayRec PayRec *inLR = *on

PCalcStateTax b * parameters D pi D StateTax 7p D Gross 7p D NbrPer 3p D MarStat 1 D NbrDep 3p * local variables and constants D TaxablePay s 7p C C* C C C* C C C C C C C C* C C C C* C C C C* C C C C* C* C C C C eval calculate annual pay eval deduct allowance

2 2 value 0 value value 0 value 2

StateTax = *zero

if zero or less,

deduct allowance

if zero or less,

yearly tax is 3% 4%

TaxablePay = (Gross * NbrPer) for employee & spouse if MarStat = 'M' eval TaxablePay = (TaxablePay - 10000) else eval TaxablePay = (TaxablePay - 5000) endif no tax due if TaxablePay <= *zero return endif for dependents eval TaxablePay = (TaxablePay (NbrDep * 2500)) no tax due if TaxablePay <= *zero return endif of first $5,000 of remainder if TaxablePay <= 5000 eval StateTax = (TaxablePay * 0.03) else

C eval StateTax = C (150 + C (TaxablePay - 5000) * 0.04) C endif C* get this period's portion of yearly tax C eval StateTax = C (StateTax / NbrPer) C* PCalcStateTax e

Let me point out one more thing. A program with a subprocedure will not run in the default activation group, so I included an H-spec to force it to run in activation group QILE instead.

Returning a Value
Here's another thing subprocedures can do that subroutines can't: A subprocedure can return a value in the same way that a built-in function returns a value. This means that you can reference a subprocedure name in such operations as EVAL, IF, and DOx. The previous example is a good illustration of this. The CalcStateTax procedure yields one value--the amount of tax to withhold from a paycheck and send to the tax collector. Here is the subprocedure modified to return a value:
H dftactgrp(*no) actgrp('QILE') Fpaywork uf e pr disk 7p 7p 3p 1 3p read dow eval 2 2 value 0 value value 0 value

DCalcStateTax D Gross D NbrPer D MarStat D NbrDep C C C C C C C C C C C C* C

update read enddo

PayRec not %eof(PayWork) PWStateTax = CalcStateTax (PWGross: PWNbrPer: PWMarStat: PWNbrDep) PayRec PayRec

eval *inLR = *on * ======================================================== PCalcStateTax b * parameters D pi 7p 2 D Gross 7p 2 value D NbrPer 3p 0 value D MarStat 1 value D NbrDep 3p 0 value * local variables and constants D TaxablePay s 7p 2 D StateTax s 7p 2 C* calculate annual C C C* deduct allowance C pay eval TaxablePay = (Gross * NbrPer) for employee & spouse if MarStat = 'M'

C eval TaxablePay = C (TaxablePay - 10000) C else C eval TaxablePay = C (TaxablePay - 5000) C endif C* if zero or less, no tax due C if TaxablePay <= *zero C return *zero C endif C* deduct allowance for dependents C eval TaxablePay = C (TaxablePay C (NbrDep * 2500)) C* if zero or less, no tax due C if TaxablePay <= *zero C return *zero C endif C* yearly tax is 3% of first $5,000 C* 4% of remainder C if TaxablePay <= 5000 C eval StateTax = C (TaxablePay * 0.03) C else C eval StateTax = C (150 + C (TaxablePay - 5000) * 0.04) C endif C* get this period's portion of yearly tax C return (StateTax / NbrPer) C* PCalcStateTax e

Notice the differences. The "pi" procedure interface line now includes a data type and size to tell what type of value is being returned. Each RETURN operation includes a value to be sent back to the caller.

A Few More Tips Would Be Nice


I hear that many programmers do not use subprocedures. If you're one of them, I hope I've given you some good reasons to begin using them. And before I go, let me offer a few tips on using subprocedures. First, subprocedures follow the O-specs, if you have any. Since most RPG programs these days don't have O-specs, subprocedures usually follow the C-specs. Second, don't start converting subroutines to subprocedures. I do not advocate changing working code unless there is a clear benefit in doing so. You may benefit from converting subroutines that carry out common tasks or tend to break and need a lot of fixing; but otherwise, leave your existing code alone. Third, if a subroutine is useful throughout an application--if it can be used in more than one program-consider putting it into a service program, to which other programs can bind at compile time, rather than at the end of each program that needs it. Building service programs is a topic for another article. Speaking of topics for other articles, subprocedures support recursion. That is, a subprocedure can call itself. Try that with a subroutine sometime. This is not a terribly useful feature, but recursion does have its applications, and maybe I will write about it someday. There is one additional advantage to subroutines worth mentioning. Subroutines are faster than subprocedures. I have never had to worry about the performance of subprocedures, but in a heavily used program, performance may become an issue. Using subroutines instead of subprocedures may make a difference.

I don't know what my RPG teacher's aversion to subroutines was. Maybe he didn't like having to code SR in columns 7 and 8 of the C-specs, as the System/3 RPG II compiler required. Maybe he thought that using subroutines added too many lines of code to programs. I just hope that you will not have such an aversion to subprocedures. Ted Holt is a consultant and an editor of Midrange Guru, OS/400 Edition. He welcomes your comments at tholt@midrangeserver.com.

iSeries EXTRA: Service Programs and Signatures by Susan Gantner and Jon Paris
Many RPGers have learned about the joys of service programs, which were designed to collect multiple modules together to simplify object management. But weve found that many shops apparently create all of their service programs with only one module. While theres nothing technically wrong with this approach, it seems a waste of a collection object to have only one item in the collection. Plus, if you have programs that use many of these single-module service programs, it could impact performance, because a connection must be made at start-up between a program and each service program it uses. A program using 10 service programs will take longer to start than a program using two or three service programs, even if the number of procedures is the same. We dont understand rationale behind this single-module service program approach, but it may be because of past problems these shops have had with service program signature violation errors. Here, we explain signatures and outline your options for avoiding those errors. Q: What is a signature? A: A signature is a value that provides a similar check for service programs that a level check does for files. It helps ensure that changes made to the service program are done in such a way that the programs using them can still function properly. The signature is typically generated by the system when you issue a Create Service Program (CRTSRVPGM) command specifying Export(*ALL). The signature value is generated by an algorithm that uses as input the names of all of the service programs exports and their sequence. Exports are the names of callable procedures and any exported data items. When you create a program that references that service program, its current signature value is copied into your program. If the export list were to be changed without the programs detection, the program could potentially call the wrong procedure. Q: What causes a signature to change? A: A signature changes when the list of exports for the service program changes. The most common cause of a signature change is adding a new procedure to the service program. Despite popular wisdom to the contrary, a change in a procedures parameters doesnt change the service programs signature. Q: What happens when the signature changes? A: When a program is called, it immediately checks the signature value of any service programs that it uses and produces a signature violation message if the signatures dont match. This happens at program start up not when you actually call a service program procedure. Q: I have a Signature Violationwhat now?

A: You have a couple of options to correct this situation. You can either re-bind all of the programs that reference the changed service program or you can create binder language to manage the service programs signatures. Q: How do I re-bind the programs that reference the service program? A: Use the Update Program (UPDPGM) command. Because you arent replacing any modules, specify Module(*None). You dont need to specify the service program because the UPDPGM command automatically re-checks the signatures of any bound service programs. If any signatures have changed, it also updates the programs signature values. Q: Why not re-compile the programs or re-create the programs with the Create Program (CRTPGM) command? A: Its typically better to make only one change at a time to avoid the possibility of introducing un wanted or unnecessary changes while fixing a problem. Re-compiling or re-creating could potentially introduce changes. For example, if the source has changed, re-compiling the program definitely introduces changes. Even if you re-create the program from some previously compiled modules, you cannot always be sure they represent the exact same code currently in the program. In addition, other program attributes (related to authority, Activation Group, etc.) could change when you re-create it. But perhaps the best reason to use UPDPGM is because its easier and faster than either of the other options. Q: How do I know which programs need to be updated? A: That can be difficult. If you have a cross-reference tool that understands service programs, it should provide the information you need. Otherwise, you can display each program using the Display Program (DSPPGM) command and see (on the fourth display screen) the list of service programs it uses and what signature value its looking for. Of course, if you know the signature has changed, the fact that the program references the service program tells you it must be updated. This method is tedious to do manually. You could also write a program to automatically perform the updates for you using the appropriate APIs (DSPPGM has no outfile support). This is beyond the scope of this article but we may cover it in the future. Q: What about binder language? A: If you dont want to update several programs every time you add a new procedure to your service program, you can create binder language to manage the signature values. Lets examine an example. Say you have a service program called DateStuff that originally contained two procedures called DayOfWeek and DayName. For purposes of writing binder language, it doesnt matter if both procedures are in the same module or combination of modules. Binder language only cares about the procedure names. When you originally created DateStuff, you specified Export(*All), because you werent using binder language. Recently you added a new module to the service program that contains procedures named GetDuration and LastDayOfMonth and specified Export(*All) again. Now your signature is different because you added procedures (exports) that changed the signature value. Programs that were working fine with the old version of the service program now fail with a signature violation. Lets create some binder language to fix this. Key your binder language into a source file called QSRVSRC with a member name that matches your service program: DateStuff. Member type is BND. The binder language should look like this:

STRPGMEXP PGMLVL(*CURRENT) EXPORT SYMBOL(DayName) EXPORT SYMBOL(DayOfWeek) EXPORT SYMBOL(GetDuration) EXPORT SYMBOL(LastDayOfMonth) ENDPGMEXP STRPGMEXP PGMLVL(*PRV) EXPORT SYMBOL(DayName) EXPORT SYMBOL(DayOfWeek) ENDPGMEXP Note that the sequence of the procedure names (called export symbols in binder language) is important. Your previous export list (*PRV) must be in the sequence that the system put it in when the service program was first created with Export(*All). Because the system puts the procedure name exports in alphabetical sequence, thats what weve done here. Its equally critical that the *CURRENT export list maintains the sequence of ALL exports in ALL *PRV lists. That is, DayName must remain in the first position and DayOfWeek in position 2, etc. Now that you have the binder language created, you can issue an Update Service Program (UPDSRVPGM) command to your DATESTUFF Service Program, this time specifying Export (*SRCFILE). Make sure the source member and file names are specified correctly. Now your service program has two different signaturesone for each export list in the binder language. The signature generated by the *PRV list should match the service programs original, so all of the old programs will continue to work. New or updated programs will automatically pick up the signature generated by the *CURRENT list. If you need to add another module in the future, you can create a second *PRV list. Just remember that the sequence of the procedure names is critical. Make a block copy of the *CURRENT list and change PGMLVL in one of them to *PRV. Add any new procedure names to the END of the new *CURRENT list and dont remove or re-sequence the procedure names in any of the export lists. You can apply your binder language on CRTSRVPGM or UPDSRVPGM after its created. This technique uses system-generated signature values. Its also possible to hard code your own signature values. Well take another look at managing service program signatures in a future issue. About the Author(s): Susan Gantner: Susan Gantner, an eServer Magazine, iSeries edition technical editor, is coowner of Partner400. She spent several years working for IBM in Rochester, Minn., before moving to the IBM Toronto lab, where she continued her focus on educating programmers in ILE and RPG IV. Susan can be reached at susan.gantner@partner400.com. Jon Paris: Jon Paris, an eServer Magazine, iSeries edition technical editor, is co-owner of Partner400. Jon has more than 30 years experience in the data processing industry, and played a major role in the definition of RPG IV while working at IBMs Toronto lab. He can be reached at jon.paris@partner400.com.

What is the longest parameter usable in RPG?

Question: Does anybody know of any limitation to the length of a character parm to an RPG program, for example... *ENTRY PLIST PARM PARM1 70 PARM PARM2 6 PARM1 is 70 character string, PARM2 is 6 character string. Can I do this? Or is there any limitation to the length of a parm? Like.... 33 characters?
Answer(s):

Your final statement "Like 33?" is telling. Can you supply an exact example of how the parameter is created and used on the CALL statement? If the CALL happens at a command line or the parameter is a CL literal value or any of a number of possibilities, you'll run into possible problems. By default, every *CHAR parameter passed from CL has a minimum 32 character length. Up to 32 is always padded with blanks even if you only pass a single character. Over 32, the parameter has a default declared length of whatever the length of the literal is. If you issue the following: ===> call pgm(ABC) parm('12345678901234567890') and receive it into a character variable declared as longer than 32 positions, there will be no padding at the rightmost end. Whatever was in memory after position 32 becomes part of the value of the received argument. Positions 21 through 32 should be blank, but the rest? Who knows? (Actually, you CAN make some predictions, but that's irrelevant.) If you supply some precise examples of the CALL, corrections can be given. If you need more you can use arrays (up to 9999 times 256 bytes) or data structures (up to 32K ?) Depends on how the program is called. There is no problem if it is called from another program using variables for the parameters (using the correct definitions, of course). The padding happens when it is called using literals, mostly when used from the command line. Do you have a specific example of what you are describing? What you are describing has never happened to me or to anyone else I know. And also runs contrary to any discussion of parameter passing I have seen in any of the HLL manuals. I used to work on a project were every I/O was handled by a specific program. So in order to read/write something, a program needed to call this routine with the record buffer (was always bigger than 32 bytes). In case it would give garbage as you stated, this project shouldn't have worked... however it did ! This is not entirely accurate, either. Calling a program with a character parameter (in a CL lets say) greater that 32 character could very likely end up with garbage in the trailing character, even in the parameter on the receiving program is declared the same size, type, etc. It's happened to me, and other, many times before. This is not entirely accurate, the 32 character limitation, only applies when you use the CALL command on a command line to pass parameters to a program. When a HLL program calls another program, a pointer to the string is passed to the sub-program. If the string declared in the main program is declared shorter than the string in the sub-program, then the extra characters in the sub-program may actually overrun some other storage from the main program. Any modifcation to the string by the sub-program could yield "unpredicatable results". On the other hand, the sub-program declares the string as shorter than what is passed, the subprogram

will not have any problems but it won't be able to address all the positions in the string. While strings may be limited to 256 in RPG/400, Data structures are not and can be passed as a parameter. The main thing to watch out for with long character fields is that right-padding with spaces is not done consistently if the field is over 32 in length. If your content does not fill the field, garbage characters may end up padding the field on the right. In your example, the contents of PARM2 will probably end up in PARM1 following the last non-blank character. If you need a field with a length greater than 32, you must ensure that all characters are filled in. I usually handle this by making the field one character longer than needed, and place some character such as "." in the final character. PARM1 at 70 is more than acceptable. I constantly use 100 byte parms. The limitation is the max length of RPG, of course it depends on your version that you are using. RPG/400 max length is 256 for character fields. We are running V3R2

OVRDBF and SECURE() keyword in an ILE environment


Question: Does anyone know what the SECURE() keyword is supposed to do on the OVRDBF? It seems to have no effect at all. Also, is it true that OVR's are not scoped to the call level in a native ILE application but rather they are scoped to an activation group name? IOW, if I have programs A, B, and C all compiled to run in activation group FUNNY, and program A calls, B calls C, and C does an override to file PF, and then returns to B which in turn returns to A which performs an OPEN on file PF -- will program A use this OVR? Did you get that?
Answer(s):

The ILE program must be running in an ILE activation group for activation group level scoping to take effect. If it is running in the default activation group, call level scoping will be in effect. Thanks for the tip! I'll go back and re-read your News/400 article, and perhaps we will change our OVR commands to use *CALLLVL. Actually, I'm not even sure this will work. You see, we are creating a procedure called CRTHUBDDM() that the user will call from their programs. This procedure will create a DMM to the "hub" machine Db, and override the "F" spec to use this DDM file. The problem is, trying to determine if there are any outstanding overrides in effect already against this file that the tool will OVR() unbeknownst to the programmer. If you do the following: PGMA issues OVRDBF FILE(FUNNY) TOFILE(*LIBL/FUNNY) SECURE(*YES) This then calls PGMB which calls PGMC PGMC issues OVRDBF FILE(FUNNY) TOFILE(QGPL/FUNNY) SECURE(*NO) <--the default value PGMC then tries to OPEN file FUNNY. The original override will be the one in effect. If the second OVRDBF also specified SECURE(*YES) then the second override would be in effect. If an ILE program issues an OVRDBF command and the OVRSCOPE parameter is left at *ACTGRPDFN, then the scenario you describe will be true. If you specify OVRSCOPE(*CALLLVL), then it will work the "old" way. We were severely burned by this when we converted everything to ILE. I had a tech tip published in News/400 about this a year

ago or so. We just went through all of our CL and specified the OVRSCOPE(*CALLLVL) to make the application work as before. Back to the SECURE issue, one interesting situation I bet that comes up, is that after PGMC goes out of scope, the OVRDBF command that it issued would still be in effect, negating the original OVRDBF. If *CALLLVL were used, then the original OVRDBF would come back into the picture.

Randomize function for the AS/400


Question: I am looking for any randomize function in AS/400, has anyone done that before? Please advice.
Answer(s):

An Idea but i don't how far it is going to help you out. You can use the API which martin has specified. If you feel like how to go about in creating a random number by your own here is method. You can generate a random number given a range . I have written a program to do so. The logic is like this. Input = range is 200 to 230 Solution: 1.Get the difference i.e.230-200 = 30 now get a random number between 0 to 30 2.Calculate how much seconds we are from a fixed date take a date which is around 1980. 3.Now add the seconds by any constant and multiply by another constant. This is to boost the random number for a small change in seconds. 4.Convert both the seconds and the difference to binary and then do an AND operation. 5.Convert the result binary to decimal. It will be in the range you desire. 6.Add the result to base i.e. 200. You will get the random number. It may look hectic, but very interesting to do. This i did when i was new to AS/400 as well as Software field so did the binary conversion and AND by using Array by division and addition. This really works good and we are using it to select a random part number for inspection. Or I think so you can call up a c program in AS/400 and get the random number which looks easier . I have not tried till now. Sorry if i have confused you through my poor english. Shouldn't the result of the random function be between 0 and 1 ? You'll only get overflows by your method. Being serious again, take a look at http://as400bks.rochester.ibm.com/cgibin/bookmgr/bookmgr.cmd/BOOKS/QB3AMM01/5.7.1 for information on the CEERAN0 API. Just hook up your AS/400 to an NT server, ping it regularly, and record the time it crashes, that should give you a nice random number. Basic Random Number Generation (CEERAN0) API The Basic Random Number Generation (CEERAN0) API generates a sequence of uniform pseudorandom numbers between 0 and 1 using the multiplicative congruential method with a user-specified seed. as described in System API Reference OS/400 Integrated Language Environment (ILE) CEE APIs Document Number SC41-4861-01

Soft Coding Module Names


Question: Anyone out there have a solution for this?

I am writing an interactive application that will be made up of around 50 modules. (All RPG) As I complete a module and test it I need it to be available to the users. I have a main selection menu that is a full page sub file. To display the eligible options, I read through a DBF, and if the users authority level is >= the authority required to use the app I write the description of the app to the sub file along with a code to a hidden field in the sub file. Then as I process my READC to see which option the user selected, I call various other modules based on the hidden field. My question: Has any one done something similar except write the name of the module to the hidden field and called the required module. This way I wouldn't have to add a WHEN to the select group, recompile the menu module and update the program every time I add a new app to this system I could just add a record to a DBF and no recompiling needed.
Answer(s):

If you're going to call a module, you must use CallB or CallP. Both of these instructions require that you hard code the name of the possible modules in your program. Even if you specify a procedure pointer in a CallB statement, you must set the procedure pointer by hard-coding the name of the procedure either in the Inz keyword for the procedure pointer or in an Eval statement in your program. As far as I know, there is no way to do what you want given that you are invoking modules. It may sound like heresy, but why do these have to be modules? If they were standalone programs, you could set a 10-character variable with the name of a program can use oldfashioned Call. Unless you are invoking the module many times, there won't be a noticable difference in performance between a bound call and a dynamic call. Mike Cravitz NEWS/400 Technical Editor I don't know what mean by invoking a module, but if you want to call a procedure in a module it must be in a program or service program. The procedure to call can be determined at run time. Any procedure in a service program can be called dynamically with a procedure pointer. The procedure name could be in a file or program variable. Before calling the procedure in the service program you need to "activate" the service program. This can be done two ways. Call any procedure in the service program or use the activate it (you might just create an actsrvpgm) procedure which you bind into every service program). Or, you can activate the service program using the activate program API (This is easiest in C). After the service program is active you can use the retrieve exports api to retrieve a pointer to the procedure you want to call dynamically. I think the point you might be missing here is any program that invokes a procedure must know at bind time (before run time!) the name of that procedure unless you either use MI (as was suggested in a previous post) or you use one of the new APIs which allow you to invoke procedures in a service program without binding. Both of these run time techniques are a bit

tricky. NEWS/400 has a web posting on how to invoke the APIs. However, it was written by a C programmer for C programmers. One of the things I want to do when I find the time is to figure out how to do this with RPG IV. Take care. Mike Cravitz NEWS/400 Technical Editor Mike, here's an RPG IV procedure that uses the APIs to return a procedure pointer to a procedure in a service program. Note that the "SysPtr" type depends on an unsupported feature where a declared procedure pointer can be used to hold a system pointer. If you use this feature, I recommend that you use a similar technique (using LIKE) so that if the feature goes away and RPG defines a system-pointer type, you only have to change one place. (I wrote this procedure a while back when we first found out about this "feature" - thought I might as well share it. It would need some additional error-checking to be really useful - I leave that as an exercise for the keen programmer :-) Barbara Morris IBM Toronto Lab RPG Compiler Development
* File: GetProcPtr * Sample invocations for GetProcPtr: * EVAL ptr = GetProcPtr('MYPROC' : 'MYSRVPGM' : '*LIBL') * EVAL ptr = GetProcPtr('MYPROC' : Srvpgm : Library) * EVAL ptr = GetProcPtr(%TRIM(Procname) : SrvPgm : Lib) * (Note that the first parameter can't have trailing blanks) D GetProcPtr PR * PROCPTR D Procedure 100A VARYING CONST D SrvPgm 10A CONST D Library 10A CONST * File: GetPPtr H NOMAIN /COPY GetProcPtr * Define a "System-pointer" type. D SysPtr S * PROCPTR BASED(dummy)

*------------------------------------------------------------------P GetProcPtr B EXPORT *------------------------------------------------------------------D GetProcPtr PI * PROCPTR D Procedure 100A VARYING CONST D Srvpgm 10A CONST D Library 10A CONST *----------------------------------------------------* Prototypes and templates for calling ResolveSystemPtr * (RSLVSP.H) *----------------------------------------------------D RSLVSP2 PR EXTPROC('_RSLVSP2') D Ptr LIKE(SysPtr) D Template LIKE(RslvTemplt) CONST D RSLVSP4 PR EXTPROC('_RSLVSP4') D Ptr LIKE(SysPtr) D Template LIKE(RslvTemplt) CONST

LibPtr DS 32A 2A 30A 2A

LIKE(SysPtr) CONST

D RslvTemplt D Gen_Mat_Id D TypeSubtyp D Object D Req_Auth

OVERLAY(Gen_Mat_Id:1) OVERLAY(Gen_Mat_Id:3) INZ(AUTH_NONE)

* See QSYSINC/MIH/MICOMMON for authority constants D AUTH_NONE C X'0000' *----------------------------------------------------* Prototype and templates for ActivateBoundProgram * (QLEAWI.H) *----------------------------------------------------D ActBndPgm PR EXTPROC('QleActBndPgm') D SrvpgmPtr LIKE(SysPtr) CONST D ActMark 10I 0 D ABPInfo LIKE(ABP_Info) D ABPInfoLen 10I 0 CONST D ErrorCode LIKE(ErrCode) D ABP_Info D ABP_Ret D ABP_Avail D D ABP_ActGrp D ABP_ActMark D D ABP_Flags D DS 10I 10I 8A 10I 10I 7A 1A 1A 0 INZ(%size(ABP_INFO)) 0 INZ(*ALLX'00') 0 0 INZ(*ALLX'00') INZ(*ALLX'00')

*----------------------------------------------------* Prototype and templates for ActivateBoundProgram * (QLEAWI.H) *----------------------------------------------------D GetExport PR EXTPROC('QleGetExp') D SrvpgmMark 10I 0 D ExportId 10I 0 CONST D NameLen 10I 0 CONST D ExportName 100A CONST D ExportPtr * PROCPTR CONST D ExportType 10I 0 D ErrorCode LIKE(ErrCode) D D D D EX_NOT_FOUND EX_PROC EX_DATA EX_NO_ACCESS C C C C DS 0 1 2 3 10I 0 INZ(0)

D ErrCode D ErrProv

*----------------------------------------------------* Local Variables *----------------------------------------------------D LibPtr S LIKE(SysPtr) D SrvpgmPtr S LIKE(SysPtr) D ActMark S 10I 0 D ProcPtr S * PROCPTR

D ExportType

10I 0

*----------------------------------------------------* First, get the pointer to the service program *----------------------------------------------------C IF Library = '*LIBL' * They specified *LIBL C EVAL TypeSubtyp = x'0203' C EVAL Object = Srvpgm C CALLP RSLVSP2(SrvpgmPtr : RslvTemplt) C * * C C C C C C C C * ELSE They specified the library ... Get the pointer to the library EVAL TypeSubtyp = x'0401' EVAL Object = Library CALLP RSLVSP2(LibPtr : RslvTemplt) Get the pointer to the service program EVAL TypeSubtyp = x'0203' EVAL Object = Srvpgm CALLP RSLVSP4(SrvpgmPtr : RslvTemplt : LibPtr) ENDIF

*----------------------------------------------------* Now, activate the service program *----------------------------------------------------C C C CALLP ActBndPgm(SrvpgmPtr : ActMark : ABP_Info : %size(ABP_Info) : ErrCode)

*----------------------------------------------------* Finally, get the procedure pointer * We're using nameLen+name rather than export-number * so we pass 0 as the export number. *----------------------------------------------------C CALLP GetExport(ActMark : 0 : C %len(Procedure) : Procedure : C ProcPtr : ExportType : C ErrCode) *----------------------------------------------------* Return the procedure pointer *----------------------------------------------------C IF ExportType = EX_PROC C RETURN ProcPtr C ELSE C RETURN *NULL C ENDIF P GetProcPtr E

Here's my test program. I created two service programs each containing a procedure called ABC - the procedures just DSPLY the names of their service program.
* File GetPPtrT /COPY GetProcPtr D ptr s C *entry plist * PROCPTR

C C

C C C

parm parm eval callb seton

srvpgm 10 lib 10 ptr = GetProcPtr('ABC':srvpgm:lib) ptr lr

Sorting a user space Question: Currently I get a list of jobs, by user, and place that into a user space. Unfortunately, when I push that list into a UIM interface for a user to scroll and select from the list is not sorted. I would like to sort the data in the user space. Does anyone know of a resource that I can use? I did not find a sort api anywhere...... My other choices are to sort the list in the uim (can this be done?) or put the list into a phyical file and do the sort there. I don't want to do either of those :-(
Answer(s):

You can use the QLGSORT API, or a user index. I would suggest reading the user space into an overlaying array. Then you can sort by any field in the array very easily... something like this...
D USpaceArrDS100DIM(9999) D UserName 10overlay(USpaceArr:1) D JobName 10overlay(USpaceArr:10)

etc.... (hope this is right... not at work....) The size of USpaceArr should be the total of bytes from all the fields defined using overlay. This way, you can sort by any subfield using SORTA keeping the data intact and sequenced. Refer to subfileds as UserName(i) or JobName(i) as you would any other array element. Hope this helps!

Example of ILE RPG CGI Program


Question: I'm looking for a CGI program example. I've heard that you can program it in RPG/ILE, but I have never seen it. Do you have any books or sample code?
Answer(s):

Take a look at http://publib.boulder.ibm.com:80/cgibin/bookmgr/BOOKS/QB3AEQ02/CCONTENTS A very simple one that only outputs data looks as follows;
DOutBuff DOutBuffLn DAPIError S S DS 2048A 9B 0 Inz(2048)

D APIBytes 1 D CPFId 9 DAPIStdOut C DHTML S DNewLine C C MoveL C 1 Do 0 C Cat C Cat C EndDo C ExSr C SetOn Lr C Return C StdOut BegSr C Eval C Eval C CallB C Parm C Parm C Parm C EndSr **CTDATA HTML Content-type: text/html

4B 0 15 'QtmhWrStout' Dim(6) PerRcd(1) CtData X'15' *Blanks OutBuff 6 n 5 100 HTML(n):0 NewLine:0 StdOut OutBuff OutBuff

APIBytes = %Size(APIError) OutBuffLn = %Len(%Trim(OutBuff)) APIStdOut OutBuff OutBuffLn APIError

<HTML> <HEAD><TITLE>Hello world CGI sample</TITLE></HEAD> Hello world. </BODY></HTML>

BTW, if you build the output string like this, it's recommended to use varying length fields, and just before the API copy them to the fixed length output field. The difference in performance is huge when some string handling is done. I continually cringe at CGI examples that use arrays in the RPG Program. Try this hello world program out for size.
************************************************************** ** * 1. CRTRPGMOD MODULE(lib/HELLOW) + * * SRCFILE(lib/QRPGLESRC) TGTRLS(V3R7M0) * * * * 2. CRTPGM PGM(lib/HELLOW) BNDSRVPGM(QTCP/QTMHCGI) * * * ************************************************************** ** D WPError DS D EBytesP 1 4B 0 INZ(40) D EBytesA 5 8B 0 D EMsgID 9 15 D EReserverd 16 16 D EData 17 56

* D HTTPHeader C CONST('Content-type: text/html') D NewLine C CONST(X'15') * D WrtDta S 1024 D WrtDtaLen S 9B 0 ************************************************************** ** C EXSR $Main * C eval *INLR = *On ************************************************************** ** * Main Subroutine ************************************************************** ** C $Main BEGSR * C eval WrtDta = '<html><head>' + C '<title>Hello World</title>' + C '</head><body>' + C 'Hello World!' + C '</body></html>' + C NewLine C EXSR $WrStout * C ENDSR ************************************************************** ** * Write to Standard Output ************************************************************** ** C $WrStout BEGSR * C ' ' CHECKR WrtDta:1024 WrtDtaLen * C CALLB 'QtmhWrStout' C PARM WrtDta C PARM WrtDtaLen C PARM WPError * C ENDSR ************************************************************** ** * Initialization Subroutine ************************************************************** ** C *INZSR BEGSR * C eval WrtDta = %trim(HTTPHeader) + C NewLine + NewLine C EXSR $WrStout * C ENDSR

This should do it...


DOutBuff S 2048A DOutBuffLn S 9B 0 Inz(2048) DAPIError DS D APIBytes 1 4B 0 D CPFId 9 15 DAPIStdOut C 'QtmhWrStout' DHTML S 100 Dim(8) PerRcd(1) CtData DNewLine C X'15' C Eval OutBuff = '' C 1 Do 8 n 5 0 C Cat HTML(n):0 OutBuff C Cat NewLine:0 OutBuff C EndDo C ExSr StdOut C SetOn Lr C Return C StdOut BegSr C Eval APIBytes = %Size(APIError) C Eval OutBuffLn = %Len(%Trim(OutBuff)) C CallB APIStdOut C Parm OutBuff C Parm OutBuffLn C Parm APIError C EndSr **CTDATA HTML Content-type: text/html !!!this is a blank line!!! <HTML> <HEAD><TITLE>Hello world CGI sample</TITLE></HEAD> <BODY> Hello world. </BODY> </HTML>

Don't forget to compile it with service program QTCP/QTMHCGI

First time Data queue application in RPG


Question: Can someone suggest a quick way to get up to speed on using data queues in an RPG program. Any manual references or code snippets would be much appreciated.
Answer(s): DDAT C 'hello world'

C PSNDDQ PLIST C PARM DataQ C PARM DataQLib C PARM DataLength 0 C PARM Data C C PRCVDQ PLIST C PARM DataQ C PARM DataQLib C PARM DataLength 0 C PARM Data C PARM Wait 0 C ...... * * Place an entry in a dataq * C MOVEL 'MyDataQ' DataQ C MOVEL 'MyLib' DataQLib C Z-ADD 11 DataLength C MOVEL DAT Data C CALL 'QSNDDTAQ' PSNDDQ ....... * * Read from dataq until the data read is 'QUIT' * C dqdata doueq 'QUIT' C movel 'MyDataQ' DataQ C movel 'AGCTI' DataQLib C move *BLANKS Data C z-add *ZERO DataLength C z-add -1 Wait forever C call 'QRCVDTAQ' PRCVDQ * Add code to process the data received C enddo

10 10 5 50 10 10 5 50 5

//Wait

How do you change the signon screen?


Question: Can anyone tell me how to edit / change the signon screen on the as400. All I need to do is to add a sentence to the bottom, company warnings about mis-use etc. Cheers
Answer(s):

usually, you can find member QDSIGNON in file QDDSSRC in library QGPL. Copy this member to new one and edit by SEU or SDA. After creating new object you have to change subsystem description (usually QINTER) - CHGSBSD - keyword SGNDSPF. I just did this for our AS/400 and added a "security" message and the company logo on it. What you need to do is get the QDSIGNON source and edit it to show/say what you want. then for any subsystem you want this to show up on (don't use QCTL so you can at least get into your console) you will have to do a ENDSBS on the subsystem. Then compile the DDS source into a library other than QSYS I put mine in QGPL. Then STRSBS on the subsystems you want the QDSIGNON used in. Then do a CHGSBSD SBSD(QINTER) SGNDSPF(QGPL/QDSIGNON). (Change SGNDSPF to whatever Subsystem(s) you want to use the new sign on screen. If you have any problems let me know. The source file member is QDSIGNON in QGPL/QDDSSRC. Do not change the order of input capable fields or remove them. If you only want users to be able to enter User ID and Password, you can protect and hide the other fields in the changed DDS. Compile the source into one of your libraries and then change the sub system description for the subsystem such as QINTER by using WRKSBSD. I would advise that you do not change your controlling subsystem just in case! You can add many lines of output text and you can move the positions of input capable fields providing you do not alter their sequence in the DDS. go to http://as400bks.rochester.ibm.com/bookmgr/home.htm and look up the book OS/400 Work management there you will find some information on changing QDSIGNON. The source of QDSIGNON is shipped in QGPL/QDDSSRC. I copied the source and added the following lines:
A A A A A A A A MSG001 MSG002 MSG003 MSG004 MSG005 MSG006 MSG007 MSG008 79 79 79 79 79 79 79 79 O O O O O O O O 11 12 13 14 15 16 17 18 2MSGID(S000001 2MSGID(S000002 2MSGID(S000003 2MSGID(S000004 2MSGID(S000005 2MSGID(S000006 2MSGID(S000007 2MSGID(S000008 SIGNON) SIGNON) SIGNON) SIGNON) SIGNON) SIGNON) SIGNON) SIGNON)

create a MSGF SIGNON and add the MSGID's with your text. When creating the sign-on display file with the Create Display File (CRTDSPF) command, secify 56 on the MAXDEV parameter. I created QDSIGNON in QGPL, and changed the SBSD QINTER to look at QGPL/QDSIGNON. I would recommmend not to change the controling subsystem. HTH

ILE RPG, RPG IV, vs. RPG/400


Question: We are primarily RPG 400 site. We are contemplating on moving to RPG IV (ILE RPG). Advice from those who have done this will be appreciated Thanks
Books on this subject: RPG IV Jump Start : Moving Ahead With the New RPG Programming in RPG IV

RPG IV By Example Answer(s):

Just take a look at all the limitations that have disappeared, think about the easier coding for string and arithmetic expressions and the choice should be clear. Whether or not you'll be using the ILE capabilities (service programs, modules, ...) is another question, but I would at least start with RPG IV (or whatever it might be called) and use the CRTBNDRPG option. So far I haven't encountered any disadvantage except that compilation times (including the CRTPGM) are a bit longer compared to before. Take your migration in two steps. 1) Focus on the Language (i.e. RPG-IV). The features such as date/time arithmetic and many more are more than worth the effort which is relatively minor. As Paul indicated, the freedom from limits is worth the move all by itself. 2) Learn the ILE piece (they are separate - one does not require the other) separately. ILE tends to be difficult for many AS/400 people. If you have any old mainframe folks in your shop, they'll tell you that IBM finally brought the linkage editor to the 400. Approcah ILE with caution using modular programming only where necessary and major benefit can be gained. In most cases we've found that the complexity of modular programming isn't worth the gain, but RPG-IV is. We converted all of our old RPG/400 (actually RPG-III and some RPG-II code) to RPG-IV via the converter that IBM provided without incident one weekend. All of our 5,000 plus programs compiled and ran without incident. They did a real good job on the converter, just create new source files with the proper record lenght, set up a CL and let her rip. You may want to check NEWS/400's or Midrange Computing's list of books and buy some of. I can't remember the title, but there is on book (small green book) that explains the differences and introduces the new op codes to an experienced RPG-III programmer. That was all we needed to get the migration done. We did conduct a seminar where we used overhead foils to present the new features to the programmers and spend 1/2 day talking about them (including ILE). We did no other training. There is another "problem", the compiled objects are about double size than RPG400 programs. I have a shareware tool that might help you out too. It is call Convert ILE Format (CVTILEFMT). It is run after you convert the source from RPG to RPG/IV using IBMs CVTRPGSRC command. It formats the source into a free-format style. It is very similar to Connection 2000's convert program (but doesn't cost anything). You can view examples and download the program at my web site: http://www.bvstools.com Feel free to ask questions, etc.

DSPDTAARA to an outfile - possible?


Question: Hi, I would like to make a dspdtaara for every dtaara named dsp* to an outfile. Is that

possible? (DSPDTAARA doesnt do it) (My problem is that we have a dtaara for every Session (Terminal or PC) and in that dtaaras is saved, which printer is to be used from the Session - and I would like to check this Data)
Answer(s):

With some quick and dirty programming this can't be a problem. I would suggest to use following steps; 1. DSPOBJD the necessary *DTAARA to an outfile 2. Write a CL that reads this outfile and does a RTVDTAARA for each of them 3. Call an RPG program for each of them with name and contents to write to a file. This can be done in 10 minutes.

Differences Between RPG400, RPG IV, and ILE RPG


Question: Please excuse my ignorance on this subject, but could someone enlighten me on the main differences between RPGIV, RPG400 and the newer ILE version?
Answer(s):

In a nutshell, ILE is an architecture. RPG400 is the current version of the RPG language and is available on all AS400's. RPG IV has a lot of changes to the whole structure of the RPG language and is only available starting with V3R7 (I think!) Both RPG400 and RPG IV can use ILE architecture. >>In a nutshell, ILE is an architecture. 100% correct

>>RPG400 is the current version of the RPG language and is available on all AS400's. Wrong. It is certainly available on all AS/400s, but it is not current - hell it hasn't been updated since V2R2 (or was there a tiny change in V2R3?) The current version is RPG IV (otherwise known as ILE RPG). >>RPG IV has a lot of changes to the whole structure of the RPG language and is only available starting with V3R7 (I think!) Wrong. RPG IV is available starting from V3R1 and is the _only_ RPG compiler (not counting VARPG on the PC) being enhanced by IBM. >>Both RPG400 and RPG IV can use ILE architecture. Wrong. Only RPG IV can use ILE. There are really only two current RPGs for the AS/400, RPG/400 and ILE RPG. RPG IV is a popularized name for ILE RPG.

RPG/400: This is the older RPG that has been around for several years. It used the "Old Programming Model" popularly known as OPM, while ILE RPG uses the ILE model. The following information applies to ILE RPG. ILE RPG: It became available at Version 3. If you are at V3R1 on a CISC machine, I would recommend going to V3R2 to take advantage of the best features of ILE RPG. Some ILE RPG Features: Syntax: All non-external data definitions can now be specified in a D-specifications that are new to ILE RPG. In addition you can define "named constants" that greatly simplify coding in the C-spec's. Also C-spec formats have changed slightly to provide for variable names of up to 10 characters (up from 6 in RPG/400) and longer operation codes. New Operations: Several have been added. One that I like is EVAL which allows you to evaluate a mathematical expression similar to Cobol and other mathematical programming languages such as Basic, FORTRAN, PL/1, etc. Modularity: This is a big plus. You can now write modules (non-executable) in several languages and bind them together into a single ILE program. Thus you can use the best language (ILE C, ILE Cobol, ILE RPG, ILE CLP) for a process or use existing modules to write a program. You can also write callable procedures or procedures that function like builtin functions. More: There is of course much more that is new in ILE RPG. For performance reasons, you should have a good understanding of ILE. Bryan Meyers has written several very good articles in NEWS/400 that can help you avoid some ILE traps. He is also one of the moderators of NEWS/400's RPG Programmers Community.

Compare two strings in RPG character by character


Question: Now I want to compare two strings letter by letter using RPG. Example: 'America' and 'American' Who knows any function I can implement this(except %scan)?
Answer(s):

I'm not sure what you want to achieve, but to compare two fields character-by-character you could use the following code:
(sample definitions) D FIELD1 S D FIELD2 S D LENGTH S D INDEX S C C C ... 1 DO IF 999 999 3 2

0 INZ(%SIZE(FIELD1)) LIKE(LENGTH)

LENGTH INDEX %SUBST(FIELD1:INDEX:1) = %SUBST(FIELD2:INDEX:1)

(your code - you might want to leave the loop using the LEAVE op-code) ... C ENDIF C ENDDO

Why can't IF Field1=Field2?

you

just

compare

them?

If Field1 is 7 char long and Field2 is 8 char long, RPG will test with the greatest length padded with blanks so you will test : "America " (with a blank) = "American" and that's false. See http://publib.boulder.ibm.com/cgi-bin/bookmgr/BOOKS/QB3AGZ00/4.3.6. How about
If Trim(Field1) = Substr(Field2,Len(Field1)) Then

Shouldn't it be
If Trim(Field1) = Substr(Field2,Len(trim(Field1))) ??

But they don't match... with or without the blank...so I don't see the point. Unless you want
If D strcmp D s1 D s2 Str1 = %SubSt(Str2:1:%Len(%Trim(Str1)) PR 10I 0 1000 1000 ExtProc('strncmp') Value Value

you could use the c function strcmp

To use this you would have to terminate the string with nulls
C Eval rc = strcmp(s1+x'00':s2+x'00)

But using strcmp gives the exact same answer as comparing the strings in RPG, only in a slower and more complicated way. By the way, Mike, your prototype for strcmp won't work. > D strcmp PR 10I 0 ExtProc('strncmp') > D s1 1000 Value > D s2 1000 Value It should be one of these (two ways to fix the parameters + two different functions (strcmp takes 2 parms, strncmp takes 3)
D strcmp D s1 D s2 PR 10I 0 1000 1000 ExtProc('strcmp') Const Const

D strcmp PR 10I 0 D s1 * D s2 * OPTIONS(*STRING) isn't strictly necessary. D strcmp D s1 D s2 D len D strcmp D s1 PR 10I 0 1000 1000 10u 0 10I 0 *

ExtProc('strcmp') Value options(*string) Value options(*string) ExtProc('strncmp') Const Const Value ExtProc('strncmp') Value options(*string)

PR

D D

s2 len

* 10u 0

Value options(*string) Value

Barbara Morris

A silly ILE RPG question


Question: Since there's no such thing as a comp.languages.rpgiv group I need to bother you people with a rookie question I'm calling C routines from ILE-RPG, using the PCALL instruction. Works great. To pass null-terminated strings I figured to throw in the %STR function, like
PCALL LOG(LOGLVL : %STR(LOGTXT))

but the compiler consistently flags %STR as an invalid token. (N.B. It works when I leave out the %STR() but then I have to modify my C routines to trim and terminate strings). I did read the FM ;-) but can't find any restrictions on using %STR. Perhaps it's not allowed in a PCALL ? That would be a shame because it's the most obvious place to use it. Any ideas most welcome. TIA
Books on this subject:
RPG IV Jump Start : Moving Ahead With the New RPG

Answer(s):

Is LOGTXT defined as a It sounds like it's If so, try this (not properly aligned!)
D LogTxt D LogTxtPtr S S

character defined

field, as

or a

as

a basing character

pointer? field.

20A *

Based(LogTxtPtr)

then use %Str(LogTxtPtr) Hope this helps. Gary Guthrie Editor, The RPG Source Technical Editor, NEWS/400 magazine Another thing that might work, but wouldn't be as elegant as the pointer solution, would be to concatenate an X'00 (Hex 00 - Null) to the end of the string On your prototype define your second parameter with the keyword Options(*String) and then use the built-in-function %TrimR to call your procedure like this:
CALLP LOG(LOGLVL : %TRIMR(LOGTXT))

Here's another example:

Dperror D C

PR Eval RC

10I 0 ExtProc( 'perror' ) * Value Options( *String ) = perror( %TrimR( FileNam ))

YES ! That works. Hurray, I've written my 'hello world' program in RPG :
D txt D log D txt C C S PR CALLP RETURN 40A * INZ('Hello, world !') EXTPROC('logit') VALUE OPTIONS(*STRING)

log(%TRIMR(txt))

And the C module linked to this is simply


void logit (char *t) { printf("'%s'\n", t); }

Thanks for the solution. Are you sure the RPG compiler you use has the same release level as the manual? There are many enhancements to RPGLE between OS/400 3.2 and 4.2. If your compiler is old many of the other suggestions I read so far will also fail to work. In that case, using %trimr() and appending a x'00' manually is your only solution. Also, you cannot write your own str() function in rpg or C, unless you also pass the length of the character field, or know the length of the character field in advance. Since you want to find the last non-blank character, you have to start looking at the end of the character string, and the compiler has no way of finding out, unless you either pass fixed-length fields, or pass the length as a parameter. It would be a great idea though, if the rpg compiler writers gave us a macro or template facility, so that we could write our own functions, which would look like procedure calls, but would be able to handle arbitrary parameter variable lengths, just like the built in functions. Right now prototyped functions cannot handle parameters of different storage size very well. They can if they are character variables, but not numeric ones. You need to code the prototype with Options(*Varsize) which causes the compiler to pass "operational descriptors" along with the parameter. The function then must do a call to CEEDOD to obtain the size of the expression or variable passed. This allows you to do implement this type of function even in V3R2 on CISC machines.

Unfortunately operational descriptors are not available for numeric variables so we don't have a good way of knowing length and decimal positions, which makes it hard to implement a %editc() look-alike. Check out the new (V4R2) keyword VARYING available to variables & parameters in RPG/IV. Once the length is set (using %Len or %TrimR where appropriate), the program keeps track of it. Passing procedure parameters this way you won't have to check the actual length, the program will know what storage to access (and what not to). Me thinks this does it:
. . .

* * * C C C C C C C C C

Determine input parameter lenght for procedure parameter. CALLP CEEDOD(1 : DescType : DataType : DescInfo1 : DescInfo2: InLen : *OMIT) CenterString LeftByte

* * ' ' check EVAL * * EVAL IF

WrkString = %subst(CenterString:LeftByte: InLen - LeftByte + 1) NbrofChar = %LEN(%trim(WrkString)) NbrofChar < pre>

Found it now.... It's because I use TGTRLS(V3R2M0). Leave that out and it works. Probbaly %STR was introduced after 3.2. Drat ! Thanks all for pointing me in the right direction syntactically.

Rules for converting C prototypes to RPG prototypes are as follows:


Even though int * is a pointer, it can be thought of as an int passed by reference, so it's better to prototype it in RPG as an int passed by reference. C | RPG ----------------|------------------------------------------int, long | 10i 0 value unsigned int | 10u 0 value double | 8f value int * | 10i 0 unsigned * | 10u 0 double * | 8F char * | * value options(*string) void * | * value | OR LIKE(someStructure) XXX | value LIKE(XXX) XXX * | LIKE(XXX) ... (ellipsis)| figure out what the ... stands for | and make one or more prototypes to | match the various combinations, possibly | with OPTIONS(*NOPASS) on some parms Where XXX is some struct/union, defined in RPG as a data structure. This part is for those advanced folks who know C and are creating RPG prototypes for homegrown C functions (i.e. not part of the C runtime):

Also, watch out for C's widening rules. Even though short = 5I 0, when it comes to passing parameters by value, short=10i 0 and char=10u 0. These differences don't always cause a problem since the use of registers to pass parameters can cause these differences to be hidden. Luckily, the C runtime rarely if ever has short or even char parameters passed by value. Unfortunately, C doesn't ALWAYS widen it depends on the absense of a #pragma nowiden. Barbara

Rules for Converting from C Prototypes to RPG Prototypes


IBM Toronto's Barbara Morris has a tutorial describing the differences between C and RPG that make some functions somewhat challenging to prototype. Check it out at: http://www.opensource400.org/callc.html

Rules to remember when subtracting dates.


POTENTIAL DATE ARITHMETIC GOTCHA EXPOSED Several readers wrote in to report a bug in the way RPG handles data arithmetic, specifically citing that the SubDur op-code intermittently returns erroneous information. Readers reported that the problem sometimes occurs when subtracting the end of one month from the end of another month and requesting the result in terms of months. For example, subtracting the end of March from the end of April returns the value 0 rather than 1 for the number of months. Consider the following code:
C DateFld1 SubDur DateFld2 Months:*M

When field DateFld1 is April 30, 2001 (2001-04-30) and field DateFld2 is March 31, 2001 (2001-03-31), field Months contains the value 0 rather than 1.

Though some expected a PTF to fix the problem, this is not a bug! RPG is working exactly as designed. It is important to note this behavior so that your applications work properly. As you'll soon see, it is possible to first add a specific duration to a date and then subtract that same duration with the resulting date not equaling the original date! From the ILE RPG Reference: A month can contain 28, 29, 30, or 31 days. A year can contain 365 or 366 days. Because of this inconsistency, the following operations can give unexpected results: * Adding or subtracting a number of months (or calculating a duration in months) with a date that is on the 29th, 30th, or 31st of a month * Adding or subtracting a number of years (or calculating a duration in years) with a February 29 date.

The following rules are used:


* When months or years are added or subtracted, the day portion remains unchanged if possible. For example, 2000-03-15 + %MONTHS(1) is 2000-04-15. * If the addition or subtraction would produce a nonexistent date (for example, April 31), the last day of the month is used instead. * Any month or year operation that changes the day portion is not reversible. For example, 2000-03-31 + %MONTHS(1) is 2000-04-30 changes the day from 31 to 30. You cannot get back the original 2000-03-31 by subtracting one month. The operation 2000-03-31 + %MONTHS(1) - %MONTHS(1) becomes 2000-03-30. * The duration between two dates is one month if the later date minus one month gives the first date. For example, the duration in months (rounded down) between 2000-03-31 and 2000-0430 is 0 because 2000-04-30 - %MONTHS(1) is 2000-03-30 (not 2000-03-31). Tip from: Club Tech iSeries Programming Tips Newsletter An AS400 Network Publication http://www.as400network.com

SEU - User defined Line Commands code sample - long


Here's a sample - lurkers please feel free to critique! h dftactgrp(*no) actgrp('QILE') indent(*none) * dbgview(*list) * Buck Calabro April 2000 * large portions lifted from the SEU User's Guide and Reference SC092605-00 * If you have a really large source file, increase the size of SourceStmt * Note that this is really a boiler-plate more than anything else. * To activate, STRSEU, press F13, page down and fill in the name of this program * SEU puts data in QTEMP/QSUSPC * this space has 3 data blocks:

* * *

1. Input from SEU 2. Output back to SEU 3. Actual source lines

* Supports the following line commands: * ATTRxx - set line attribute (colour, highlight, etc.) * Supports the following F keys: * F7 - Split/join a line (Splits this line to next if cursor in the middle of a line, * joins next line to this if cursor at the end of a line) * F8 - NOP * Uses messages in a user-created message file: * Message ID Severity Message Text * SEU0001 0 Cursor is not positioned within a source statement. * SEU0002 0 Line split complete. * SEU0003 0 Line join complete. * SEU0004 0 Cannot update in Browse mode * SEU0005 0 ATTR command processed * SEU0006 0 ATTR command not valid for this member type * Input from SEU D SEUInput DS D StmtLength D CurRec D CurCol D CCSID D InputRecords D SrcMbr D SrcFil D SrcLib D MbrType D FnKey D SEUMode D SplitSession D ReservedInp * Output to SEU D SEUOutput D ReturnCode D ReservedOut1 D OutputRecords D InsertedSeq D ReservedOut2 DS BASED(SEUInputP)

10i 10i 10i 10i 10i 10 10 10 10 1 1 1 1

0 0 0 0 0

1 3 10i 0 7 21

BASED(SEUOutputP)

* Source statements. * D SEUSource DS D LineCmd D LineRetCode D SourceSeq D SourceDate D SourceStmt * Work variables D SEUInputPParm S D SEUOutputPParm S D SEUSourcePParm S

SEU passes the line the cursor is on, and the next line BASED(SEUSourceP) 7 1 6 6 256 * * *

D ThisLineP D NextLineP D WorkLineP D D D D D D D D D i CutColumns ThisLineCmd ThisStmt NextStmt SourceLength CutLen BlankLineCmd RtnCode

S S S s s s s s s s s s pr

* * * 10i 0 inz like(SourceStmt) like(LineCmd) like(SourceStmt) like(SourceStmt) 10i 0 10i 0 like(LineCmd) 7 7 const Like(RtnCode) * const like(LineCmd) like(LineRetCode) like(SourceSeq) like(SourceDate) like(SourceStmt)

DSndMsg D MsgID D RtnCodeOut DLoadWorkFromInp D SrcDtaPtrInp D LineCmdOut D LineRetCodeOut D SourceSeqOut D SourceDateOut D SourceStmtOut DLoadOutFromWork D SrcDtaPtrInp D LineCmdInp D LineRetCodeInp D SourceSeqInp D SourceDateInp D SourceStmtInp DGetAttrFromCmd D LineCmdInp C C C C C C C C C C

pr

Options(*Omit) Options(*Omit) Options(*Omit) Options(*Omit) Options(*Omit) const Options(*Omit) Options(*Omit) Options(*Omit) Options(*Omit) Options(*Omit)

pr

* like(LineCmd) like(LineRetCode) like(SourceSeq) like(SourceDate) like(SourceStmt)

pr

like(LineCmd) const

*================================================================ *Entry Plist Parm SEUInputPParm Parm SEUOutputPParm Parm SEUSourcePParm

* Get the data referred to by the input pointers Eval SEUInputP = SEUInputPParm Eval SourceLength = %len(SEUSource) %len(SourceStmt) + StmtLength Eval SEUOutputP = SEUOutputPParm Eval ThisLineP = SEUSourcePParm C Eval NextLineP = SEUSourcePParm + SourceLength C C C C * Set default values Eval Eval Eval ReturnCode = '0' OutputRecords = InputRecords - 1 InsertedSeq = '0000000'

* Allow updates only if in Update mode If SeuMode = 'U'

C C C C

Exsr LineCommands Exsr CmdKeys Else Eval ReturnCode = '1' * Send back "Not in update mode" message C CallP SndMsg('SEU0004': RtnCode) C EndIf C C Eval Return *InLR = *On

area)

*================================================================ * Process all the line commands (commands typed in the seq number

* InputRecords includes the "next" line. * For example, if a line command is placed on lines 1 and 5, InputRecords will be 6 C C C C C C C C C C C C C C C C C C column C C C C C C C C C C C LineCommands Begsr Eval Eval DoW Callp WorkLineP = ThisLineP i = 1 i <= (InputRecords - 1) LoadWorkFromInp(WorkLineP: ThisLineCmd: *Omit: *Omit: *Omit: ThisStmt)

Select * Line command to set the attribute of the line When %subst(ThisLineCmd: 1: 4) = 'ATTR'

* Blank out the line command Callp LoadOutFromWork(WorkLineP: BlankLineCmd: *Omit: *Omit: *Omit: *Omit) * Highlight the line by forcing an attribute byte in the proper * based on the source member type If MbrType = 'RPG' or MbrType = 'RPGLE' or MbrType = 'SQLRPG' or MbrType = 'SQLRPGLE' or MbrType = 'PF' or MbrType = 'PRTF' or MbrType = 'DSPF' Eval %subst(ThisStmt: 1: 1) = GetAttrFromCmd(ThisLineCmd) * Put the work fields back into the source space Callp LoadOutFromWork(ThisLineP: *Omit:

C C C C C C

*Omit: *Omit: *Omit: ThisStmt)

* Send back a message to show that we saw and processed the line cmd CallP SndMsg('SEU0005': RtnCode) Else * Send back a message to show that we saw and ignored the line cmd C CallP SndMsg('SEU0006': RtnCode) C EndIf C C C C C EndSL Eval Eval EndDO EndSR *================================================================ * Process the command keys (F7/F8) C C C C C C C CmdKeys Begsr Select * Is the cursor outside of the source When (FnKey = FnKey = CurCol = statement with an F key press? '7' or '8') and 0 i = i + 1 WorkLineP = WorkLineP + SourceLength

* Tell SEU that the cursor is outside the source area CallP SndMsg('SEU0001': RtnCode) * F7 = split/join When FnKey = '7'

* Should we do a split or a join? * Get the line the cursor is on C Callp LoadWorkFromInp(ThisLineP: C *Omit: C *Omit: C *Omit: C *Omit: C ThisStmt) * Get the next line C Callp LoadWorkFromInp(NextLineP: C *Omit: C *Omit: C *Omit: C *Omit: C NextStmt) * If there is data beyond the current column, split it * If the rest of the line is blank, join the next line to this one C if %subst(ThisStmt: CurCol: C StmtLength - CurCol - 1) <> C *Blanks C Exsr SplitLine

C C C C C

Else Exsr EndIf EndSL EndSR

JoinLine

*================================================================ * Split line at blank C C C C C C C C C C C C C C C C C C C C C C C C C C SplitLine Begsr

* Cut the columns to the right including the column the cursor is in Eval CutColumns = %subst(ThisStmt: CurCol) * Drop the rightmost columns into the next line Eval NextStmt = CutColumns * Trim the cut columns off the right side of this line If CurCol > 1 Eval ThisStmt = %subst(ThisStmt: 1: CurCol - 1) Else Eval ThisStmt = *Blanks EndIf * Put the work fields back into the source space Callp LoadOutFromWork(ThisLineP: *Omit: *Omit: *Omit: *Omit: ThisStmt) Callp LoadOutFromWork(NextLineP: *Omit: *Omit: *Omit: *Omit: NextStmt)

* Tell SEU that we're returning 2 lines Eval OutputRecords = 2 * Tell SEU that the split is complete CallP SndMsg('SEU0002': RtnCode) EndSR *================================================================ * Join line

C C

JoinLine

Begsr

* Don't try to join if the next line is a blank If NextStmt <> *Blanks * Grab the leftmost columns from the next line (as many columns

C C C C C C C C C C C C C

* as are blank at the end of this line) Eval CutColumns = %subst(NextStmt: 1: (StmtLength CurCol + 1 * Add the columns from the next line onto the end of this line ' ' Checkr CutColumns CutLen Eval ThisStmt = %subst(ThisStmt: 1: CurCol - 1) %subst(CutColumns: 1: CutLen) * Blank out the cut columns Eval

))

%subst(NextStmt: 1: CutLen) = *Blanks

* If we've cut the entire next line, delete it. Otherwise, * simply cut the columns out - don't shift the remainder of the line C If NextStmt = *Blanks C Eval OutputRecords = 1 C Eval InsertedSeq = 'A000000' C Else C Eval OutputRecords = 2 C Eval InsertedSeq = 'A000000' C EndIf C C C C C C C C C C C C C C C * Put the work fields back into the source space Callp LoadOutFromWork(ThisLineP: *Omit: *Omit: *Omit: *Omit: ThisStmt) Callp LoadOutFromWork(NextLineP: *Omit: *Omit: *Omit: *Omit: NextStmt)

* Tell SEU that the join is complete CallP SndMsg('SEU0003': RtnCode) EndIf EndSR *================================================================ * Send a "status" message back to SEU * There's a trick in use here that you need to be aware of. * the message stack count is determined by how deep in the call stack

the

* subprocedure is! Here's why it was set to 3: * STRSEU 1 * SEUEXIT 2 * SndMsg 3

PSndMsg DSndMsg D MsgID D RtnCodeOut D D D D D D D D D

b pi 7 const Like(ErrSMsgID)

* Send message API parameters MsgIDWrk s MsgFil s MsgData s MsgDataLen s MsgType s MsgStackEnt s MsgStackCnt s MsgKey s MsgErrStruc s

like(MsgID) 20 inz('SEUEXIT *LIBL 1 inz(' ') 10i 0 inz 10 inz('*INFO') 10 inz('*') 10i 0 inz(3) 4 like(ErrStruc) inz 10i 0 inz(%len(ErrStruc)) 10i 0 7 1 80 MsgIdWrk = MsgID MsgErrStruc = ErrStruc 'QMHSNDPM' MsgIDWrk MsgFil MsgData MsgDataLen MsgType MsgStackEnt MsgStackCnt MsgKey MsgErrStruc ErrStruc = MsgErrStruc RtnCodeOut = ErrSMsgID

')

* API error structure D ErrStruc DS D ErrSSize D ErrSUse D ErrSMsgID D ErrSResrv D ErrSData C C C C C C C C C C C C C C PSndMsg e eval eval Call Parm Parm Parm Parm Parm Parm Parm Parm Parm Eval Eval

*================================================================ * Load the work fields from the data SEU sent us PLoadWorkFromInp DLoadWorkFromInp D SrcDtaPtrInp D LineCmdOut D LineRetCodeOut D SourceSeqOut D SourceDateOut D SourceStmtOut C C C b pi * const like(LineCmd) like(LineRetCode) like(SourceSeq) like(SourceDate) like(SourceStmt) Options(*Omit) Options(*Omit) Options(*Omit) Options(*Omit) Options(*Omit)

* Point to the data within the SEU space Eval SEUSourceP = SrcDtaPtrInp If Eval %addr(LineCmdOut) <> *Null LineCmdOut = LineCmd

C %subst(SourceStmt: 1: C C P e

C C C C C C C C C C C

Endif If Eval Endif If Eval Endif If Eval Endif If

%addr(LineRetCodeOut) <> *Null LineRetCodeOut = LineRetCode %addr(SourceSeqOut) <> *Null SourceSeqOut = SourceSeq %addr(SourceDateOut) <> *Null SourceDateOut = SourceDate %addr(SourceStmtOut) <> *Null Eval SourceStmtOut StmtLength) =

Endif

*================================================================ * Load data back to SEU from the work fields PLoadOutFromWork DLoadOutFromWork D SrcDtaPtrInp D LineCmdInp D LineRetCodeInp D SourceSeqInp D SourceDateInp D SourceStmtInp C C C C C C C C C C C C C C C C P e *================================================================ * Extract an attribute byte from the input line command * The line command is formatted "ATTRxx" where XX is a mnemnonic for * the attribute byte to assign to the line. The mnemnonics are the * as used by DDS with the addition of colours. PGetAttrFromCmd DGetAttrFromCmd b pi 1 b pi

* like(LineCmd) like(LineRetCode) like(SourceSeq) like(SourceDate) like(SourceStmt)

const Options(*Omit) Options(*Omit) Options(*Omit) Options(*Omit) Options(*Omit)

* Point to the data within the SEU space Eval SEUSourceP = SrcDtaPtrInp If Eval Endif If Eval Endif If Eval Endif If Eval Endif If Eval Endif %addr(LineCmdInp) <> *Null LineCmd = LineCmdInp %addr(LineRetCodeInp) <> *Null LineRetCode = LineRetCodeInp %addr(SourceSeqInp) <> *Null SourceSeq = SourceSeqInp %addr(SourceDateInp) <> *Null SourceDate = SourceDateInp %addr(SourceStmtInp) <> *Null SourceStmt = SourceStmtInp

same

D LineCmdInp D AttributeByte D AttrTest D i DAttrMnemDS D D D D D D D D D AttrMnem DAttrDS D D D D D D D D D Attr C C C 20 C C C C P Buck Calabro e s s s ds 1 2 10i 0 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 Eval

like(LineCmd) const

inz(' ') inz('RI') inz('HI') inz('UL') inz('BL') inz('CS') inz('CP') inz('CL') dim(8) overlay(AttrMnemDS) inz(x'20') inz(x'21') inz(x'22') inz(x'24') inz(x'28') inz(x'30') inz(x'38') inz(x'3A') dim(8) overlay(AttrDS)

ds

* Default to normal

AttributeByte = Attr(1)

* Extract the mnemnonic from the line command Eval AttrTest = %subst(ThisLineCmd: 5: 2) * Convert the mnemnonic to an attribute byte Eval i = 1 C AttrTest Lookup If Eval EndIf Return *In20 = *On AttributeByte = Attr(i) AttributeByte

AttrMnem(i)

Stored Procedures, Result Sets, RPG, and Native I/O!


From: *************** Club Tech iSeries Programming Tips Newsletter *************** An iSeries Network Publication http://www.iseriesnetwork.com/ Home of iSeries NEWS Magazine Issue 115 2003

March 13,

Q: I'd like to create an RPG program as a stored procedure that creates and passes a multi-row result set back to the calling SQL statement. Is it possible to use native I/O operations (e.g., Chain, ReadE) in the RPG program to create the result set? If so, that does the RPG program look like? A: You can indeed use native RPG I/O functions such as Chain and ReadE to create a result set that you can return to the caller. As you read records with native I/O functions, you store the information read in an array or multiple occurrence data structure that you return as the result set. I've created a simple example that reads five records from file Customer and returns them as a result set. The example uses a multiple occurrence data structure to contain the result set information. Let's look at the example, now. Physical file Customer: * =================================================================== * = File.......... Customer = = * = Description... Sample customer file

* =================================================================== A A A A R CUSTOMERR CUSTID CUSTNM K CUSTID

5P 0 10

As you can see, I've kept file Customer simple! It contains a customer ID (field CUSTID) that serves as the key and a customer name (field CUSTNM). You can enter the following sample data for our example: CUSTID CUSTNM 1 2 3 4 5 6 7 8 9 10 Gary Mel Anna Mike Paul Carson Katie Dale Wayne Kathy

Let's now see what the RPG program looks like. Program: Sample

* =================================================================== = = = * * * = Program....... Sample = Source type... SQLRPGLE = Description... SQL result set sample

* =================================================================== FCustomer IF E K Disk

* =================================================================== * = Data definitions = * =================================================================== D ResultSet D D D RowCount E DS ExtName( Customer ) Qualified Occurs( 20 ) 5U 0

* =================================================================== * = Key list for Customer = * =================================================================== C C CustKey KList KFld CustID

* =================================================================== * = Load result set information = * =================================================================== C C C C C C C C C CustKey RowCount For Chain If Eval Occur Eval Eval EndIf EndFor CustID = 1 to 5 Customer %Found( Customer ) RowCount = RowCount + 1 ResultSet ResultSet.CustID = CustID ResultSet.CustNm = CustNm

* =================================================================== * = Return result set information =

* =================================================================== C/Exec SQL C+ Set Result Sets Array :ResultSet for :RowCount Rows C/End-Exec * =================================================================== * = End of program = * =================================================================== C Eval *InLR = *On

Because the program uses embedded SQL to return the result set, its source type must be SQLRPGLE. The program begins by defining file Customer for input. The data definition for qualified multiple occurrence data structure ResultSet appears next. The program uses keyword ExtName to externally define this data structure, deriving its subfields from the fields in file Customer. The data definitions end with a definition for field RowCount, which is simply a count of the rows read and placed in data structure ResultSet. The C-specs begin by defining key list CustKey that is used to randomly read records from file Customer. Next, the program enters a For loop that loads data structure ResultSet. The program uses the index for the loop as the key value on the Chain to file Customer. If the Chain finds a record, the program increments the row count (field RowCount), sets the next occurrence of data structure ResultSet, and then loads the data structure's subfields with the data from file Customer. The program finishes by returning the result set with SQL's Set statement. Notice that the Set statement uses a host variable (:ResultSet) to identify the result set to return as well as uses a host variable (:RowCount) to identify the number of rows to return. You can easily view your result set information using the SQL scripting feature of iSeries Navigator (formerly known as Operations Navigator). To use the scripting feature, launch iSeries Navigator and perform the following steps: A. Expand the branch for the system to which you wish to connect. B. Right-click on Database. C. Select Run SQL Scripts. D. Enter the call statement to call program Sample. The default naming convention requires that you use the form Library.Program on the call operation. Using the default naming convention, you would enter this: Call YourLib.Sample; (Note: Notice the semi-colon at the end of the statement. Also, if you wish to use this technique for programs that require parameters, enclose the parameters within parentheses after the program name, separated by commas.) E. Select the appropriate run option from the Run menu to execute program Sample.

When program Sample runs, you'll see a spreadsheet-like display of the rows in your result set.

Sending Escape Messages from RPG


Hey, Ted:
I know how to use the Send Program Message (SNDPGMMSG) command to send an escape message from a CL program to its caller. How do I send an escape message from an RPG program? -- Len
Use the Send Program Message API, QMHSNDPM. Here's an example:
D SendEscMsg D MsgID D MsgFile D MsgDta D MsgDtaLen D MsgType D MsgQ D MsgQNbr D MsgKey D ErrorDS D ErrorDS D BytesProv D BytesAvail D ExceptionID D MsgDta D MsgKey C C C C C C C C C C pr extpgm('QMHSNDPM') 7 const 20 const 80 const 10i 0 const 10 const 10 const 10i 0 const 4 16 16 10i 0 inz(16) 10i 0 7 80 4 eval callp MsgDta = 'Something went wrong' SendEscMsg ('CPF9898': 'QCPFMSG QSYS': MsgDta: %len(MsgDta): '*ESCAPE': '*': 2: MsgKey: ErrorDS)

ds

s s

Notice the sixth and seventh parameters. The asterisk (*) in parameter six refers to the current call stack. The seventh parameter tells how far up the call stack to send the message. In this example, it has a value of two, because there is a program-entry-point call stack entry between the sender of the message and the calling program. In OPM programming, this parameter should have a value of one. For more information, see the "Send Program Message (QMHSNDPM) API ," iSeries Information Center . -- Ted

What are data queues and how to use Question: Can anyone that actually know, explain to me what are data queues, what the benefit of using them , and how to use them. An IBM book number would greatly be appreciated? I am considering using them in a system we are writing at work to get our AS400 to dial out on an asynchronous line to Transunion for credit checks. This program will be accessed by a number of Service Reps. and I am hoping that a Data Queue Guru will shed some light on the in's and out's.
Answer(s):

Data Queues are a cross between data areas, and message queues. They are a method for asynchronous communication between programs. A typical use for a data queue is to have a job sitting in a batch subsystem waiting for a data queue entry to be created, and multiple programs dropping entries into the data queue. The ERP system my company uses has a single process to print invoices which is triggered by entries from multiple order entry staff to the data queue. It sounds like your application fits the bill for using data queues. There are API programs to read and write to data queues, and they are quite straight-forward to use. If memory serves, they are QSNDDTAQ and QRCVDTAQ, and they are well documented in the book, although I don't know the number. If you like I can send you examples. Benefits: - performance can be dramatically improved over individual submits if the job is complex - record locking conflicts are eliminated if only one job is updating. - they can facilitate clean modular design Drawbacks - they are hard to document well - the next programmer will have to think to figure them out they can't really be audited, backed up, or for that matter conveniently examined. - the contents are almost invisible, although smart programmers have written program to read the queue, print the entry, and re-write it. Once the entry is read it is gone; if the program halts the entry is lost. This can be gotten around with an audit file; write a record when the entry is written, nad have the receiver program update a status field when done.

Also, data queues don't support data definition, so you do need to use data structures if you intend to pass more than a single data element. In the example above, the data queue holds the order to be invoiced as well as the output queue to place the spooled file and the user to notify when it is printed. Hope this helps! Explaining them to a 'data queue beginner' is maybe easiest by comparing them to other objects to see similarities and differences. Then you can get into purpose and useability. A data queue is similar to a database file that has records written to it. One program (or many programs) can send entries to the queue. Each entry is similar to a record. Another program (or many programs) can read entries back from the queue, similar to reading records. Differences to begin with are in formats (record descriptions), reading the same entries more than once and speed. An entry on a data queue has no external description; it's just a string of bytes. If you want something like "fields", you'll have to do all the concatenating and substringing yourself. Normally, an entry is read only once. When the entry is read off the queue, it is gone. The first program to read the entry gets it and then it's gone. (It's possible to get around this, but there's seldom a reason to.) Data queues are designed to provide fast communication between programs. You might have a dozen programs feeding entries onto a queue and a single program receiving those entries. The entries might represent transactions that you want performed against your database and you don't want those dozen programs all doing it individually. You centralize the process in the receiver program. The time it takes for an entry to be sent from one program and be received by another is minimal, less than if you used a file to hold records. Alternatively, you might have one program feeding entries as fast as it can onto a queue and have a dozen programs receiving entries. By having the transactions processed by a dozen programs, you can multiply the work being done. And since each entry is removed from the queue when it's received, you don't have to worry about another program getting the same entry. The speed is partially achieved by eliminating any overhead done by the system. An example is the way the system handles the space used by a data queue as entries are added and removed. If you start a program up to add entries to the queue but there's no program started to receive the entries, the allocated space gets bigger. When the entries are later received and removed from the queue, the space allocated does _not_ get smaller. You must delete and recreate the data queue to recover excess space if want it back. This means you must know the original parameters used to create the *DTAQ object so you can recreate one to match. (There's an API to get this info that you can get into later.) If you prefer, you can think of a *dtaq as being similar to a message queue. You can send messages from one program and another can receive them from the *msgq. If you do a RCVMSG RMV(*YES), the message is gone from the *msgq, similar to how an entry is

removed from a *dtaq. And a *dtaq entry has a format similar to a message; i.e., there's no format except what you create yourself. (Note that MSGDTA() can be used to provide some general formatting with a message.) Entries are generally sent by calling the QSNDDTAQ API and received by calling the QRCVDTAQ API. One handy use for me is in CL programs where you're limited to a single file declaration. If you use these APIs, you can use any number of *dtaqs to simulate physical files, either for passing info from one part of a program to another or for passing to a different program(s). Perhaps start by creating a *dtaq with CRTDTAQ and writing a program to send some entries to it. Then do a DMPOBJ and examine the output. Then write a second program to receive the entries and do a second DMPOBJ. Testing it out can be done with some pretty small CLPs. Data queue APIs are technically described for Version 4 in the OS/400 Object APIs manual on the Systems Programming Support Bookshelf. Good luck. Here are some parts from the IBM OS/400 manuals. http://publib.boulder.ibm.com/cgi-bin/bookmgr/BOOKS/QB3AMQ02/1.0 http://publib.boulder.ibm.com/cgi-bin/bookmgr/BOOKS/QB3AVC00/B.8 http://publib.boulder.ibm.com/cgi-bin/bookmgr/BOOKS/QB3AUO01/3.5.7 http://publib.boulder.ibm.com/cgi-bin/bookmgr/BOOKS/QB3ALE01/2.3.3.2 http://publib.boulder.ibm.com/cgi-bin/bookmgr/BOOKS/QB3ALC01/D.28 http://publib.boulder.ibm.com/cgi-bin/bookmgr/BOOKS/QB3AUP01/3.1.438 http://publib.boulder.ibm.com/cgi-bin/bookmgr/BOOKS/QB3AUP01/3.1.1337 http://publib.boulder.ibm.com/cgi-bin/bookmgr/BOOKS/QB3AUP01/3.1.558 Shameless plug :-) I located those http://www.xs4all.nl/~hgj/find400.html books with my Find/400 page

They work quite easily: One program stores information in then :


CALL PARM PARM dataqueue) PARM PARM 'QSNDDTAQ' 'DTQ_PMC' P1DTAQ 10 '*LIBL' P1DLIB 10 8 P1LEN P1RC 50 90 (name of the dataqueue) ( libl of the (length of answer) ( answer)

The background job reads the information


CALL PARM PARM PARM PARM PARM something in it) 'QRCVDTAQ' 'DTQ_PMC' P1DTAQ 10 '*LIBL' P1DLIB 10 P1LEN 50 P1RC -1 P1WAIT 50 9192

(wait till somebody puts

If the background job receives a 9 the program stops receiving. Quit simple, but effective. Others described how data queues enable asynchronous communications between multiple jobs running on AS/400. Another aspect is the ability to communicate between PC programs and AS/400 jobs via Client Access APIs. For info on this go to the Info Center: http://publib.boulder.ibm.com/html/as400/infocenter.htm Select Information Center, then Client Access Express, then Programming, then Express C/ C++ APIs The data queue is a very simple concept. In your application you would have a single job that handles credit checks. When a credit check is needed, the program talking to the service rep sends a message to the data queue for the credit check job. This "wakes up" the waiting credit check job and it proceeds to dial and do the credit check. When it's done it sends a message back to a data queue for the requesting job, waking that job back up and giving it the results of the check. You can do various things, like have the credit check job check the incoming data queue for new messages needing processing before hanging up the line after completing a credit check. Just use a "dequeue but don't wait" operation in this case, vs the usual "dequeue with wait" operation. For some reason queues are rarely used (or provided as primitives) in computer systems, even though they are one of the most efficient and easiest to manage mechanisms for synchronizing multi-threaded applications. More on changing the SIGNON screen Question: We have changed the signon screen to contain company information but we have a need to do more. I thought there was a way that you can use a variable field to display something on the signon screen.

For example: If we will have the system down this saturday - on Friday we would like to have a message on the signon screen say - "System will be unavailable on Saturday due to upgrade." or During nightly backups - "System will be unavailable until approx. 4am" I thought I had read a year or 2 ago that this was possible by using a message file. Then all a person has to do is to change the message description for the message file. I am not sure if this is true or not but I cannot find it anyplace. Any help would be appreciated - Thanks
Answer(s):

Yes - it is possible to do this. Add extra fields to the display file after the last IBMprovided field UBUFFER. Use DDS keyword MSGID( ) where = name of the message

file you want to use and = the message ID containing the text you want to display. You can use as many message-derived fields as will fit on the screen. Each message field size can be anything from 1 to 132 bytes (I think that's the maximum length of 1stlevel message text - someone will correct me if not). I'm sure you know this already - but since I've been a victim in the past - ensure that you don't resequence your DDS fields in SDA and that you add all extra fields to the end of the DDS source. Also, you need to compile QDSIGNON as LVLCHK(*NO).

There are products (check ProSign/400, AS/SURE) that will allow you to change your signon screen. They might help you do what you need to do. Now, a bit of a change in subject - both these products allow you to make changes while the subsystems are up, without recompiling the screen, bringing down the subsystem, etc. Just curious here, how do they do that?

If you use message fields (see my earlier reply) then the contents of the sign-on screen change every time the underlying message text changes. I use this to show a real-time display of date & time + % disk used by updating the sign-on screen messages every minute from a background server job. Every time I press Enter on the signon screen without actually logging on (e.g. by leaving user-ID blank), I get the current time etc. refreshed on the sign-on screen. In addition to the other comments, here is an observation on the subject. If most of your users are connecting via Client Access they will not see the Sign On screen and therefore will not see your message. I had once changed the Sign On screen and put a daily message on it, but as we migrated to PCs it became a wasted effort so we stopped it. We have the following on our signon screen:
A A A MSG001 MSG002 MSG003 60A 60A 60A O 19 11MSGID(ON001 DPLIBR/ONMSGF) O 20 11MSGID(ON002 DPLIBR/ONMSGF) O 21 11MSGID(ON003 DPLIBR/ONMSGF)

To change the message text on the signon screen, we just CHGMSGD for the line we want changed. This does not change the menu interactively - if a workstation is already dislplaying the signon screen, it does not change. The change will only show up on the next initial display of the signon screen - like when the terminal is powered on, the PC starts emulation, or the user signs on then back off.

A message field is the best way to do this for QDSIGNON (or your own signon display file). A routing program that pops up an info window before TFRCTL QCMD might be better, to catch those who bypass signon. For a message field, I currently use:
A MSGLINE 640A O 13 1

with keyword MSGID(SYU 0001 SYUSRSYS/SYUSRMSG) The length of 640 gives me eight full lines I can fill. In order to actually get that much into the message, you must use second-level text rather than first-level text. In order to make second-level text available, all you need to do is specify nothing but the message ID in the first-level text. So, in the case of my example, first-level text for message ID SYU0001 is simply... 'SYU0001'. Be prepared to look long and hard for the documentation on this though. But it is more or less documented and it's a base part of the S/36 support, so IBM assures me it will continue to work. And if all you are doing is adding message fields, you shouldn't need LVLCHK(*NO). At least, I don't use it and my format level identifiers don't change. If you add an actual input or output field, however, this will no longer be true.

Sending E-Mail Using RPG


From: ************ Club Tech iSeries Programming Tips Newsletter *********** An iSeries Network Publication http://www.iseriesnetwork.com/ Home of iSeries NEWS Magazine Issue 133 July 24, 2003 Before you sit down to write from scratch that e-mail program your boss has been pestering you about, consider starting with one of the several freely available working sample programs. There's a whole gaggle of them, and they appear below in alphabetical order: Aaron Bartell's RPGMail Utility

This is an RPG service program that interfaces with Sun's JavaMail APIs. http://mowyourlawn.com/html/RPGMail.html Genesis V's Email Utility This utility includes the CMD, CL, and RPG source, along with installation instructions. http://www.genesisv.com/freesource/email.shtml. IBM article "Mail Enabling AS/400 Applications with Java" http://www-919.ibm.com/developer/java/topics/javamail.html IBM article "iSeries-Based E-mail Processing Applications with Java" http://www-919.ibm.com/developer/java/topics/mailproc.html IBM's Easy400 iSeries MIME & MAIL Utility This utility enables you to create and send Multipurpose Internet Mail Extensions (MIME) e-mail. The utility includes an address book and distribution list that you can use to send e-mails. All source is included in the download, and the site includes a tutorial and links for more information. http://www.easy400.ibm.it/mmail/start iSeries NEWS Utility SNDEMAIL This utility was published in the September 1998 iSeries NEWS article "RPG Utility Puts QtmmSendMail API to Work," which can be viewed and downloaded by iSeries Professional members. http://www.iseriesnetwork.com/article.cfm?ID=2806 Uzaemon'sSend SMTP Mail (SNDM) This enables you to send an e-mail from an iSeries command line or CL program. The complete source and installation instructions are included. http://homepage1.nifty.com/uzaemon/#download

Setting RPG Indicator Variables


From: ************ Club Tech iSeries Programming Tips Newsletter *********** An iSeries Network Publication http://www.iseriesnetwork.com Home of iSeries NEWS Magazine Issue 140 September 11, 2003 FOR NEW SUBSCRIPTIONS, you can subscribe by joining the iSeries Network with our handy Web form at http://www.iseriesnetwork.com/join/ . Indicator variables are 1-character fields that contain a value of *ON or *OFF. You define an indicator variable in the D-specs using the N data type: d ok s n

You use indicator variables just like RPG's standard *INxx indicators. An interesting property of an indicator is that you can directly assign the result of a logical expression to it. This feature can greatly simplify your code. The traditional way to set an indicator is to condition it with a logical expression:

if error = *blank; ok = *on; else; ok = *off; endif; However, you can also code it like this: ok = (error = *blank); If error is blank ok is set on, else ok is set off. The parentheses are optional, but I like to add them for readability. Reversing an indicator setting is also simple. The traditional way to do this is like this: if switch; switch = *off; else; switch = *on; endif; However, you can code it like this: switch = not switch; If switch is on, then not switch is off. If switch is off, then not switch is on. Assigning logical expressions to indicators can dramatically reduce the code in interactive programs: *in01 *in02 *in03 *in04 *in05 = = = = = (option (option (option (option (option = = = = = createOption ); changeOption ); copyOption ); deleteOption ); displayOption);

The logical expressions don't need to be simple like the examples above, but can be as complex as required. Most logical indicator assignments are straightforward, but here's one trick you may find useful. The following traditional code detects any error in a subfile: error = *off; readc subfile; dow not %eof( display); if item = *blank; error = *on; endif; readc subfile; enddo; You might be tempted to replace the if statement with the following: error = (item = *blank);

But if you do that, error will then contain the result of the test on the last subfile record. However, you can achieve the desired result with the following: error = (error or item = *blank); The code may look strange at first, but it is in fact logical. We have an error if this item is blank or if we have already found an error. The above tip was written by Julian Monypenny.

You might also like