Q: I have a topic that's very confusing to me, and I bet it gives a lot of other programmers problems as well. The topic is the use of the VARYING and *VARSIZE keywords for subprocedures. When do I use one or the other, and what techniques are needed to make them work?
I've tried to use these keywords to make a wrapper for the QCMDCHK API, but nothing I've tried works. The following code should give you an idea of what I'm trying to accomplish:
D ChkCmd Pr n
D CmdString 32767a Options(*VarSize)
D Prompt 1a Const Options(*Nopass)
D sTestString S 2000a
D sDspReply S 1a
D sDspQueue S 10a Inz('*EXT')
/Free
// Command to prompt
sTestString = 'sbmjob';
// Execute subprocedure and show results
If ChkCmd(sTestString:'Y');
Dsply %SubSt(sTestString:1:50) sDspQueue sDspReply;
Else;
Dsply 'Command failed.' sDspQueue sDspReply;
Endif;
*InLr = *On;
/End-Free
P ChkCmd B Export
D ChkCmd Pi N
D CmdString 32767a Options(*VarSize)
D Prompt 1a Const Options(*NoPass)
D QCmdChk PR ExtPgm('QCMDCHK')
D CmdString 32767a Options(*VarSize)
D sCmdLen 15 5 Const
D sDftPrompt s 1
D sDftLength s 10i 0
/Free
If %Parms >= 2 and
Prompt <> *Blanks;
sDftPrompt = Prompt;
Else;
sDftPrompt = 'N';
EndIf;
// THIS LINE DOES NOT WORK! IT RETURNS
// 32767 EVEN THOUGH THE INPUT IS 2000!
sDftLength = %Len(CmdString);
// THIS CODE CAN OVERWRITE MEMORY THAT DOESN'T
// BELONG TO CMDSTRING:
If sDftPrompt = 'Y';
CmdString = '?' + %Subst(CmdString:1:sDftLength);
Else;
CmdString = %Subst(CmdString:1:sDftLength);
EndIf;
// THE FOLLOWING CODE PRODUCES UNPREDICTABLE
// RESULTS BECAUSE THE LENGTH THAT GETS PASSED
// IS TOO LARGE!
Monitor;
QCmdChk(CmdString:sDftLength);
Return *On;
On-Error;
Return *Off;
Endmon;
/End-Free
P ChkCmd E
A: Yes, I can see why you're confused. The terms "Varsize" and "Varying" are so similar that it's easy to confuse the two. Before I present a solution to errors in the subprocedure, I'll explain what each keyword does.
VARYING describes a type of variable -- or, at least, that's the way I've always thought of it. I find it counter-intuitive that IBM decided to make it a keyword instead of a data type! Just as "packed" and "zoned" are two different numeric data types, I consider "alphanumeric" and "varying" to be two different character data types.
Consider the following code:
D myString s 100A
c eval myString = 'Alpha Beta Gamma'
How many characters does this statement assign to myString? If you think that 16 characters are assigned, you're wrong. That statement sets 100 characters! The first 16 characters are the words "Alpha Beta Gamma," but there's also an additional 84 blanks that this statement implicitly sets. This is done because myString is a fixed-length string. It's always 100 characters long, no matter what you do to it!
The VARYING keyword tells the compiler that a variable doesn't have a fixed length. When you add the VARYING keyword, it changes the data type from standard alphanumeric to one that varies in length.
D myString s 100A VARYING
c eval myString = 'Alpha Beta Gamma'
In this example, the variable is declared as varying. It's true, I still coded "100A", but now that number specifies the maximum length of the string. The actual length can vary. In this example, myString only gets assigned the 16 characters. No blanks are added to the end.
On the other hand, *VARSIZE is an option that may be specified in the OPTIONS D-spec keyword. It has nothing to do with the data type of the variable. All it does is disable the compiler's validity checking of the length.
You see, normally when you pass a variable to a subprocedure, that variable must be the same length or larger than the one that's declared in the prototype. For example, the following code will not work:
D myProto pr
D Parm1 19A
D myVar s 15A
** This will generate an error because myVar
** is shorter than Parm1!
c callp myProto(myVar)
Sometimes, in order to make routines as generic as possible, you want to accept any size variable, no matter how large or small. The options(*VARSIZE) keyword disables the compiler's internal validity checking, so that the following code will compile:
D myProto pr
D Parm1 19A options(*varsize)
D myVar s 15A
** This will NOT generate an error.
** Warning: Do not use positions 16-19 of PARM1 in the
** subprocedure!
c callp myProto(myVar)
The tricky thing about options(*varsize) is that you've disabled a safeguard that was there for a reason! The declaration of myVar causes the system to reserve 15 bytes of memory to store the variable's data into. When you call the subprocedure in the example above, it will view that same spot in memory as a 19A variable! If you mistakenly write anything to positions 16-19 of that variable, you'll overwrite memory that may be used for another purpose. Sometimes, it'll cause the program to crash. Other times, it'll overwrite memory used by other variables, which may be in the same program or may be in another program. You might even overwrite memory that's not in use, so you might not even notice the problem!
When you use options(*varsize), you must be very careful to only touch the bytes that were passed. It's up to you to determine how many bytes were passed and to make sure that the remaining bytes do not get touched.
As you can see, VARYING and *VARSIZE are two very different things! VARYING is a data type where the data in a string varies, and options(*VARSIZE) is a keyword that disables a safeguard.
When you specify VARYING on a prototype, you're not telling the system that the length of the parameter can vary. You're telling it the data type of that parameter.
To summarize:
*
* Pass a 32k parameter to SomeProcedure. The problem here is
* that it's too rigid! The length always has to be 32767!
*
D SomeProcedure PR
D parm1 32767A
*
* Pass a 32k parameter to SomeProcedure. This is every bit as
* rigid as the previous example. The only difference is the
* parameter has to be a VARYING variable.
*
D SomeProcedure PR
D parm1 32767A varying
*
* This example is not so rigid. The data can be any length,
* but now I have to worry about overflowing the space that's
* been reserved!
*
D SomeProcedure PR
D parm1 32767A options(*varsize)
What you may be thinking of when you suggest using these options to count the length of a string for you is that RPG will automatically convert from one data type to another when the CONST keyword is specified. This can be used to calculate the length of a fixed-length string because when RPG converts from fixed-length to VARYING, it sets the length of the data in the VARYING variable to the length of the fixed-length field.
For example, the following will return 15:
H DFTACTGRP(*NO)
D CalcSize PR 10I 0
D myParm 1000A const varying
D myVar s 15A
D size s 10I 0
/free
size = CalcSize(myVar);
*inlr = *on;
/end-free
P CalcSize B
D CalcSize PI 10I 0
D myParm 1000A const varying
/free
return %len(myParm);
/end-free
P E
This type of logic is not helpful in the QCMDCHK example, however, since QCMDCHK is not looking for a VARYING field, and you can't use CONST because the API needs to be able to change the data in the string.
The easiest way to make QCMDCHK work is to eliminate the subprocedure completely and use the %SIZE BIF to calculate the length. That way, you don't need to write a special subprocedure. Instead, code it as follows:
D QCmdChk PR ExtPgm('QCMDCHK')
D CmdString 32767A options(*varsize)
D sCmdLen 15 5 const
D sTestString s 2000A
D sDspReply s 1A
D sDspQueue s 10A inz('*EXT')
/free
sTestString = '?sbmjob';
callp(e) QCmdChk(sTestString: %size(sTestString));
if not %error;
Dsply %subst(sTestString:1:50) sDspQueue sDspReply;
else;
Dsply 'Command failed.' sDspQueue sDspReply;
endif;
*inlr = *on;
/end-free
If you do want to put this into a subprocedure, then you have to consider how to pass the variable-length parameter. VARYING does not make sense here, since QCmdChk doesn't understand that data type. Clearly, options(*VARSIZE) is what's needed.
Remember the caveat that I discussed above! You have to be very careful to only touch the length that was passed. You could do that by passing an additional parameter to indicate the length of the variable, as follows:
H DFTACTGRP(*NO)
D ChkCmd PR N
D CmdString 32767A options(*varsize)
D sCmdLen 10I 0 value
D Prompt 1A const options(*nopass)
D QCmdChk PR ExtPgm('QCMDCHK')
D CmdString 32767A options(*varsize)
D sCmdLen 15 5 const
D sTestString s 2000A
D sDspReply s 1A
D sDspQueue s 10A inz('*EXT')
/free
sTestString = 'sbmjob';
if ChkCmd(sTestString: %size(sTestString): 'Y');
Dsply %subst(sTestString:1:50) sDspQueue sDspReply;
else;
Dsply 'Command failed.' sDspQueue sDspReply;
endif;
*inlr = *on;
/end-free
P ChkCmd B
D ChkCmd PI N
D CmdString 32767A options(*varsize)
D sCmdLen 10I 0 value
D Prompt 1A const options(*nopass)
D sDftPrompt s 1
/free
if %parms >= 2 and
Prompt <> *blanks;
sDftPrompt = Prompt;
else;
sDftPrompt = 'N';
endif;
if sDftPrompt = 'Y';
%subst(CmdString:1:sCmdLen) =
'?' + %subst(CmdString:1:sCmdLen);
endif;
monitor;
QCmdChk(CmdString: sCmdLen);
return *on;
on-error;
return *off;
endmon;
/end-free
P E
Note the use of the %SUBST BIF where it adds the prompt character. The following code will NOT work, because it overwrites the entire CmdString! If you did it this way, it'd set the first 2001 bytes of CmdString to the value passed in and set the remaining 30766 bytes to blanks! That would overwrite memory that you don't own and cause all sorts of headaches!
// USE %SUBST, DON'T DO IT THIS WAY!
If sDftPrompt = 'Y';
CmdString = '?' + %Subst(CmdString:1:sDftLength);
EndIf;
Although the example that I posted above will work nicely, it doesn't do the job of calculating the size of the input variable for you. You have to pass it as a parameter. You could use RPG's support for operational descriptors to solve that problem. When you use operational descriptors, additional information about each parameter is made available through some APIs.
The following code demonstrates the use of operational descriptors to calculate the length of the variable for you:
H DFTACTGRP(*NO)
D ChkCmd PR N opdesc
D CmdString 32767A options(*varsize)
D Prompt 1A const options(*nopass)
D QCmdChk PR ExtPgm('QCMDCHK')
D CmdString 32767A options(*varsize)
D sCmdLen 15 5 const
D sTestString s 2000A
D sDspReply s 1A
D sDspQueue s 10A inz('*EXT')
/free
sTestString = 'sbmjob';
if ChkCmd(sTestString: 'Y');
Dsply %subst(sTestString:1:50) sDspQueue sDspReply;
else;
Dsply 'Command failed.' sDspQueue sDspReply;
endif;
*inlr = *on;
/end-free
P ChkCmd B
D ChkCmd PI N opdesc
D CmdString 32767A options(*varsize)
D Prompt 1A const options(*nopass)
*
* CEEGSI(): Get String Information from the
* operational descriptor.
*
D CEEGSI PR
D parmno 10I 0 const
D datatype 10I 0
D currlen 10I 0
D maxlen 10I 0
D sDftPrompt s 1
D StringType s 10I 0
D CurrentLen s 10I 0
D MaximumLen s 10I 0
/free
if %parms >= 2 and
Prompt <> *blanks;
sDftPrompt = Prompt;
else;
sDftPrompt = 'N';
endif;
CEEGSI( 1
: StringType
: CurrentLen
: MaximumLen );
if sDftPrompt = 'Y';
%subst(CmdString:1:MaximumLen) =
'?' + %subst(CmdString:1:CurrentLen);
endif;
monitor;
QCmdChk(CmdString: MaximumLen);
return *on;
on-error;
return *off;
endmon;
/end-free
P E