This PowerUp blog entry was written by Isaac Ramírez, a software engineer at BAC Credomatic Network. Isaac is passionate about research and development in different technologies and languages on the IBM i platform.
A few years ago, when I’d just started learning RPG, I felt the language was missing something that I was more than used to in Java and .NET: exception handling. Sure RPG has the monitor instruction, but I was looking for a way to throw exceptions and retrieve aditional data about the exceptions. This was going to be especially useful when needed to investigate bugs in the production environment.
So, after looking around, I stumbled across an IBM Redpaper titled “RPG: Exception and Error Handling” (by Gary Mullen-Schutz, Susan Gantner, Jon Paris and Paul Tuohy). This Redpaper is a must read for all RPG Developers. It introduces the QMHSNDPM and QMHRCVPM APIs, which correspondingly, can be used to “throw” and “catch” exceptions. This gave me a nice way to start. I came to realize that I needed three functionalities:
- With a given message ID and message file, throw an exception that interrupts the current procedure and goes straight to the last on-error code in the call stack
- When an exception occurred, catch it and retrieve all of the related data
- Retrieve the call stack that caused the exception
Throwing Exceptions
With a few upgrades to the code included in the paper, I created the following procedure:
p CEXCEPTION_throwNewException...
p b export
d pi n
d msgID 10a const varying
d msgFile 10a const varying
d replaceVar 3000a const varying options(*nopass)
d
d errorApi ds likeds(errorDS_Type)
d qualifiedMsgFileName...
d s 20a
d msgKey s 4a
d replaceVarTemp s 3000a
/free
clear exception;
monitor;
if %parms() > 2;
replaceVarTemp = replaceVar;
endif;
qualifiedMsgFileName = msgFile;
%subst(qualifiedMsgFileName:11) = '*LIBL';
if CEXCEPTION_saveCallStack() and stackEntryArrayCount > 0;
CEXCEPTION_saveThrowInfo();
endif;
SendProgramMessage(msgId:
qualifiedMsgFileName:
replaceVarTemp:
%len(%trimr(replaceVarTemp)):
MESSAGE_TYPE_ESCAPE:
CALL_STACK_ENTRY:
CALL_STACK_COUNTER_THROW:
MsgKey:
errorApi);
on-error;
CEXCEPTION_jobPrintf('CEXCEPTION_throwNewException: +
Error throwing exception!');
return *off;
endmon;
/end-free
p CEXCEPTION_throwNewException...
p e
This procedure allows me to simulate the functionality of the throw instruction of the Java/C++ world. The following code shows how to use it:
select;
when sqlstt = '00000';
isFound = true;
when sqlstt = '02000';
isFound = false;
other;
CEXCEPTION_throwNewException('MSG0001':'CMSGFILE');
endsl;
Catching Exceptions
The next step in the process was implementing the catch funcionality. I wanted to be able to retrieve information like the procedure that caused the exception, the line number where the exception ocurred, the error code, the message file of the error code, the job and the datetime. Using the QMHRCVPM API, I created the following procedure:
p CEXCEPTION_catchException...
p b export
d pi n
d
d recoveredMessage...
d ds likeds(RCVM0300)
d messageKey s 4 inz(*ALLx'00')
d apiError ds likeds(errorDS_Type)
d
d ptrSenderInfo s * inz(*null)
d senderInfo ds likeds(RCVM0300SndRcvInfo)
d based(ptrSenderInfo)
/free
monitor;
ReceiveProgramMessage(recoveredMessage:
%size(recoveredMessage):
FORMAT_NAME:
CALL_STACK_ENTRY:
CALL_STACK_COUNTER:
MESSAGE_TYPE:
messageKey:
WAIT_TIME:
RECEIVE_ACTION:
apiError);
if recoveredMessage.ByteAvail > 0;
ptrSenderInfo = %addr(recoveredMessage.MsgData)
+ recoveredMessage.LenReplace1
+ recoveredMessage.LenMsgReturn
+ recoveredMessage.LenHelpReturn;
if %trimr(senderInfo.SendingProcedure) <> THROW_PROCEDURE_NAME;
clear exception;
CEXCEPTION_saveCallStack();
CEXCEPTION_saveSenderInfo(senderInfo);
endif;
exception.messageId = recoveredMessage.MsgId;
exception.messageFileName = recoveredMessage.MsgFileName;
exception.messageSeverity = recoveredMessage.MsgSeverity;
exception.messageFileLibrary = recoveredMessage.MsgLibUsed;
exception.messageText = %subst(recoveredMessage.MsgData:
recoveredMessage.LenReplace1 + 1:
recoveredMessage.LenMsgReturn);
exception.date = %date(%dec(senderInfo.DateSent:7:0):*cymd);
exception.time = %time(senderInfo.TimeSent:*hms0);
exception.jobName = psds.job_name;
exception.userProfile = psds.job_user;
exception.jobNumber = %char(psds.job_num);
if (recoveredMessage.LenReplace1 > 0);
exception.messageData = %subst(recoveredMessage.MsgData:1:
recoveredMessage.LenReplace1);
endif;
return *on;
With this procedure, all I had to do is call it right after the on-error instruction, in order to access all of the exception information:
monitor;
//Some code
on-error;
CEXCEPTION_catchException(); //must be the first instruction after the on-error
//some more core
endmon;
Saving the Call Stack
By this time, all that was left was a way to call the stack information for the most recent exception (i.e., get data related with all the procedures called before the exception ocurred). To my surprise, the QWVRCSTK API let me retrieve the call stack from a specific moment. So, I included procedures to save the stack trace before throwing and after catching exceptions. You can see this in the code for CEXCEPTION_throwNewException and CEXCEPTION_catchException with the call to the CEXCEPTION_saveStackTrace procedure. With this information available, I wrote a procedure to print the stack trace in the joblog and call it right after the CEXCEPTION_catchException in the on-error structure.
The following code shows how I implemented the CEXCEPTION_saveStackTrace procedure
monitor;
dou (CSTK0100.BytesRtn >= CSTK0100.BytesAvail);
GetCallStack(CSTK0100:
sizeCSTK0100:
CALL_STACK_INFO_FORMAT:
JIDF0100:
JOB_INFO_FORMAT:
errorApi);
if (sizeCSTK0100 < CSTK0100.BytesAvail);
sizeCSTK0100 = CSTK0100.BytesAvail;
ptrCSTK0100 = %realloc(ptrCSTK0100:sizeCSTK0100);
endif;
enddo;
ptrEntry = ptrCSTK0100 + CSTK0100.Offset;
if %parms > 2;
skipEntriesUsed = skipEntries;
endif;
for index = 1 to skipEntriesUsed;
ptrEntry += stackEntry.Len;
endfor;
if CSTK0100.Count > skipEntriesUsed;
for index = skipEntriesUsed + 1 to CSTK0100.Count;
clear newArrayEntry;
newArrayEntry.stackEntryInfo = stackEntry;
if stackEntry.StmtCnt > 0 and stackEntry.StmtDisp > 0;
pStatement = ptrEntry + stackEntry.StmtDisp;
newArrayEntry.statement = %triml(statements.identifier:'0');
endif;
if (stackEntry.ProcDisp > 0 and stackEntry.ProcLen > 0);
ptrName = ptrEntry + stackEntry.ProcDisp;
newArrayEntry.procedureName = %subst(procedureName:1:
stackEntry.ProcLen);
endif;
arraySize += 1;
entryArray(arraySize) = newArrayEntry;
ptrEntry = ptrEntry + stackEntry.Len;
if arraySize > ENTRY_ARRAY_SIZE;
leave;
endif;
endfor;
endif;
Source Code
At this time, all of the functionality shown is widely used in my company and has been very helpful to create programs that handle exceptions in a more “modern way.” Obviously, a lot more code is needed to have all the functionality described in this brief post. If you want more information or the source code, just contact me to the address [email protected]. I would appreciate your feedback in this topic.
Recent Comments