Let’s ‘C’ again

The previous blog posts Let’s ‘C’ and Lets ‘C’ more looked at building a couple of service programs, this post is going to take some of that functionality and use it in a program.

One of the objects I find most frustrating to use on the IBM i as a developer is the Message File! I say that because I am constantly looking for commands and tools that help me use them effectively in my development environment. I need to be able to do things like search the message file for certain messages that have some text in them, or need to copy a message description from one message file to the other and change the content slightly or even change the message ID.  I am sure that if you are developing on the IBM these are things that you have come up against in the past?

I have created a number of internal tools that I use, this post and a number which are to follow on will build a part of those tools and hopefully show some of the good/bad things you can trip over when developing in ‘C’.  With power comes responsibility and ‘C’ is a very powerful language so simple mistakes can cause some pretty hairy problems, having said that the OS does a great job of providing fences around things…

First of all I will introduce a couple of rules that I like to follow, They are not set in stone but adherence to them makes life a lot simpler when things get more complicated.
  1. Temporary Objects belong in QTEMP.
  2. Data Objects belong in the DATA library.
  3. Program objects and related artifacts belong in the program library.

Having spent many years developing software on the IBM i plus being on the backend when dealing with bad practices (providing High Availability Software) has helped us to see how proper environment management not only helps with application support it also helps with replication to other systems.

Enough of the desk banging! Lets have a look at our first program. We are going to create a program that allows us to walk through a message file and find any messages that contain a specific string, if the string is found we will prompt the command to allow changes to be made. This will introduce a number of good concepts and show some of the things that can trip us up when we develop in ‘C’. We will also take advantage of the service programs we have already created and use some of their functionality. First lets add a new member to our QCSRC File.
ADDPFM FILE(OSLIB/QCSRC) MBR(FNDMSGCNT) SRCTYPE(C) TEXT(‘Find Message Content in message file’)
ADDPFM FILE(OSLIB/QCMDSRC) MBR(FNDMSGCNT) SRCTYPE(CMD) TEXT(‘Command for Find Message File Content.’)
Now add the following code to the ‘CMD’ member.
/*===================================================================*/
/*  COMMAND name :  FNDMSGCNT                                        */
/*  Author name..:  Chris Hird                                       */
/*  Date created :  March 2018                                       */
/*                                                                   */
/*                                                                   */
/*  Purpose......:  Find message file content                        */
/*  CPP..........:  FNDMSGFCNT                                       */
/*  Revision log.:                                                   */
/*  Date     Author    Revision                                      */
/*                                                                   */
/*  @Copyright Chris Hird 2018                                       */
/*===================================================================*/

             CMD        PROMPT('Find MSGF Content')

             PARM       KWD(MSGF) TYPE(QUAL1) MIN(1) PROMPT('Message File')
             PARM       KWD(SSTR) TYPE(*CHAR) LEN(50) MIN(1) VARY(*YES *INT2) PROMPT('Search String')
             
 QUAL1:      QUAL       TYPE(*NAME) LEN(10)
             QUAL       TYPE(*NAME) LEN(10) DFT(*LIBL) SPCVAL((*LIBL)) PROMPT('Library name:')        


This is for the ‘C’ member
//
// Copyright (c) 2018 Chris Hird
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//
// Disclaimer :
// This code is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

#include <H/MSGFUNC>                  // Message headers
#include <H/SPCFUNC>                  // User Space functions
#include <H/COMMON> // Common Header #include <qmhrtvm.h> // retrieve message api header // program is passed 2 parameters, argv_1 = message file, argv_2 = search string int main(int argc, char **argv) { int i = 0; // counter int found = 0; // string found int Rep_Dta_Len = 0; // length of replacement data passed short int len; // length of search string char UsrSpc[20] = "RTVMSG QTEMP "; // Userspace returned data char Rep_Dta[16] = {'0'}; // replacement data char Rep_Sub_Vals[10] = "*NO "; // replace substitution values char Ret_Fmt_Ctl[10] = "*NO "; // return format control chars char Rtv_Opt[10] = "*FIRST "; // retrieval option char Msg_Id[7] = {' '}; // returned message ID char search[51] = {'\0'}; // search string char cmd[8192]; // command str char str1[8192]; // str buffer char str2[8192]; // str buffer char *space; // pointer to userspace char *search_str; // search string char *tmp1; // temp ptr char *tmp2; // temp ptr Qmh_Rtvm_RTVM0300_t *MsgInfo; // pointer to returned structure Os_EC_t Error_Code; // error Code // set up the error code structure used by the API Error_Code.EC.Bytes_Provided = sizeof(Error_Code); // get a pointer to the user space for the returned data if(Get_Spc_Ptr(UsrSpc,&space,_4MB) != 1) return -1; // get the length of the string passed len = *(short int *)argv[2]; //printf("Len = %d\n",len); // null terminate the string we are looking for search_str = (char *)argv[2]; search_str += sizeof(short int); memcpy(search,search_str,len); //printf("Search string = %s\n",search); // retrieve all messages QMHRTVM(space, _4MB, "RTVM0300", Msg_Id, argv[1], Rep_Dta, Rep_Dta_Len, Rep_Sub_Vals, Ret_Fmt_Ctl, &Error_Code, Rtv_Opt, 0, 0); if(Error_Code.EC.Bytes_Available > 0) { snd_error_msg(Error_Code); exit(-1); } MsgInfo = (Qmh_Rtvm_RTVM0300_t *)space; // read through all of the messages in the file if(MsgInfo->Bytes_Available > 0) { memcpy(Rtv_Opt,"*NEXT ",10); do { // flag if message contains string, set to 0 (NO) each loop found = 0; // message text search //printf("Length = %d Offset = %d\n",MsgInfo->Length_Message_Returned,MsgInfo->Offset_Message_Returned); if(MsgInfo->Length_Message_Returned > 0) { tmp1 = space; tmp1 += MsgInfo->Offset_Message_Returned; memcpy(str1,tmp1,MsgInfo->Length_Message_Returned); memset(&str1[MsgInfo->Length_Message_Returned],'\0',1); //look for the text string if(strstr(str1,search)) found++; } // help text search //printf("Help Length = %d Offset = %d\n",MsgInfo->Length_Help_Returned,MsgInfo->Offset_Help_Returned); if(MsgInfo->Length_Help_Returned > 0) { tmp2 = space; tmp2 += MsgInfo->Offset_Help_Returned; memcpy(str2,tmp2,MsgInfo->Length_Help_Returned); memset(&str2[MsgInfo->Length_Help_Returned],'\0',1); if(strstr(str2,search)) found++; } //printf("found = %d Msg = %.7s\n",found,MsgInfo->Message_ID); if(found > 0) { sprintf(cmd,"?CHGMSGD MSGID(%.7s) MSGF(HAMN10SRC/HAMNMSGF11) MSG('%s')",MsgInfo->Message_ID,str1); if(memcmp(str2,"*NONE",5) != 0) { strcat(cmd," SECLVL('"); strcat(cmd,str2); strcat(cmd,"')"); } //printf("%s\n",cmd); system(cmd); } // get the next message memcpy(Msg_Id,MsgInfo->Message_ID,7); //printf("%.7s\n",Msg_Id); QMHRTVM(space, _4MB, "RTVM0300", Msg_Id, argv[1], Rep_Dta, Rep_Dta_Len, Rep_Sub_Vals, Ret_Fmt_Ctl, &Error_Code, Rtv_Opt, 0, 0); if(Error_Code.EC.Bytes_Available > 0) { snd_error_msg(Error_Code); exit(-1); } }while(MsgInfo->Bytes_Available > 0); } return 1; }
Ok so now lets break things down a little and what we are doing. First of all the Command, we are going to pass in 2 parameters to the program, the first is the message file name. The API we are going to use requires a 20 character string to be passed in so creating the message file parameter as a qualified parameter helps make sure we get it all in a single 20 character array. The next parameter is going to be varying length so we have created the command in a way to not only pass in the character array but also pass in the length (I think that is a nice feature and we take full advantage of it), as we have decided the maximum size is 50 characters a short int (*INT2) will be more than enough to store that size information.

Now for the ‘C’ source, some of this follows on from the previous posts so we will keep it pretty simple. Any text that has ‘//’ proceeding it is a comment and the compiler will treat any text after that to be a comment until the next line. You can use the /* … */ style of comments which work fine for multi-line comments but we chose to just use ‘//’. First important lines are the lines which start #include, these are including the header files so the compiler can check our parameters are correct etc. We have 3 that we have created plus a new one which is for the API we are going to use.

Next we have the main entry point to our program (called main surprise surprise and no! you cannot call it anything else), we are stating that we expect 2 parameters, first is an integer argc, this is the number of parameters passed into the program and next its an array of character *, these point to the parameters. When we address these parameters we will use the format argv[n] to argv[…].

As with all ‘C’ programs you have to declare the parameters you are going to use up front, so the next few lines declare a number of variables we are going to use. A note to bring up here is how that relates to the memory the compiler will assign and how it will be filled when it is assigned. The compiler likes things to be on known boundaries so it will ‘pad’ the memory locations unless we tell it not too (that’s for a later post) so while we have said this is what we want it does not mean that is what the compiler will allocate for us. This is important to know when dealing with pointers etc. many time you will fall into problems where the address you expect data to be at is not actually where it is! Also the content of the memory will reflect what you tell the compiler to do when it is initialised, if you don’t pass in a value the compiler simply says ‘hey here is some memory to hold a variable of this type’, it could be set to any value of any type (its just 0’s and 1’s), we like to ensure we ‘initialise’ the content so we can verify its content.

You will notice some variables are not initialised particularly pointers, many times you get caught by uninitialised pointers so make sure you set them to the correct address before you try to use them.

Important Note! Strings are NULL terminated character arrays, when a string function is called it ‘expects’ the content to be a NULL terminated character array. Passing it a character array with no NULL termination simply causes the function to process from the starting memory location right up until the first NULL it finds, that can be anywhere. The ‘C’ compiler does not do bounds checking! That means if you do something which overwrites its neighbour it aint going to complain!  You the BOSS.. Consider memcpy, you give it a start position to copy to, a start position to copy from and how many bytes to copy. Copying 20 bytes into a memory location that’s expecting to hold 10 bytes can cause lots of debug mayhem… We will cover some of the issues related to sprintf later.

Before the call to the API we set the Error_Code structure length, if its set to 0 the OS will just default to sending the message directly to the user and not allow you to capture the message and possibly take some action. Next we use one of our Service Program functions to get a pointer to a *USRSPC. You will notice that we pass the pointer as ‘&space’, this is simply passing in the address of the pointer (its declared as a char *) so the function can set it to the address of the user space. Also take note, we create the user space in QTEMP (see rule number 1). This user space is only for use by this program and we need to make sure its content is ONLY set by this program!

We need to interpret what string the user is requesting so we move it to a location which we know will be NULL terminated. you will notice we have created the variable search to be 51 characters (the max length is 50 so need to add the NULL on the end) and we initialise it with NULL values ({‘\0’} means fill with ‘\0’ for its full length). Then we copy the parameter to it, first we have to get the length so we can copy it plus skip over that counter to the start of the string. From this point on you will notice a number of printf() lines which have been commented out, we do this so you can debug when you get issues, simply un-comment the lines, recompile and you should see the content displayed.

The API call comes next, it is called using the required parameters and as we initialised them, the message file is argv[1] it expects just 20 characters so as you can see the command plays nicely into that. The data is returned to a User Space (the API expects a user space so allocating memory and passing that does not work) we simply pass in the pointer we retrieved from the Service Program and tell it how big it is (_4MB described in the COMMON header file). Read up on the QMHRTVM API if you want to know what all of the parameters are. 

If we made an error such as passing in the wrong message file name etc the error_code would be flagged and the program will end after the message is sent courtesy of the other Service Program. But what if the message file existed but it had no messages? Well we check for that next as we only want to check the messages content if that are some to check. then we go into a do loop, we know its got at least 1 message so we need to do it once, then we will check again before we decide with we want to ‘do’ it again.. As part of the API call we pass in the MSGID plus the relationship to the next message at the start this is set to BLANKS and ‘*FIRST’, now we want to set the variable to ‘*NEXT’ before we do anything else.  Another important check is did the message description actually contain the search string, we simply use 0 = No 1 = Yes, before we check we set to 0.

Message descriptions contain 2 descriptive sections, the message text plus the help text, we will need to check each element for the string. The function we are going to use to do the string search is ‘strstr()’ it will look for a string within a string, notice here we says string which is a NULL terminated character array. The API returns the message text and help text in a couple of structures which are character arrays NOT strings! If we do not make them into ‘strings’ the API does not work! it may work but the results will be very confusing. To overcome this we will copy the message text into a buffer and set the NULL terminator at the correct position. All character arrays are 0 offset, that means the first character will be at array[0]. so when you have a 10 character array its offsets are [0]..[9] we use that to allow us to set the NULL terminator at array[x] when x is the length the API told us the message length is. We will do that twice if the help text is available (its not always) and if we find the search string in either text buffer we will set the found flag to 1.

if the message contained the search string we have created a command that will prompt the command so you can do what you want with it! The system function takes a command string and runs it, putting a ? in front means the command will be prompted.
Notice we look for ‘*NONE’ in the help text, this is the default so if its set we know we do not need to add it to the command.

At this point I will make something clear, the system command is like throwing your request over the fence, you only get told if it ran correctly or not, no reasons or messages, just 0 or 1 returned to your program. In a future post we will develop our own ‘system()’ like function to allow commands to be issued and we have control should things go wrong.  

Finally we need to set things up to determine of we fall out of the do while loop. The message ID from the last request is used as the marker for the ‘*NEXT’ request and the API is called again, if it returns some data we go back through the do loop again otherwise we simply fall out and go to the end.

We do not clean up the User Space object, if you wanted to do that you could use the QUSDLTUS API before the return statement, we may call this program a number of times so leaving it in QTEMP allows the Get_Spc_Ptr() request to just return a pointer instead of creating it all again. The content does not matter as the first time it get used is after the call to the API to fill it and all of the previous data will be lost (not lost but the offsets used will probably not point to the same positions). Once we end our job (sign off) the system will clean up QTEMP as well!

So that’s a lot to take in, its a simple program but its got lots of critical points which could causes problems if not understood and managed correctly.  So lets build the program and command and take it for a spin. Before you run these commands make sure you have OSLIB and OSPGM in your library list.
CRTCMOD MODULE(OSLIB/FNDMSGCNT) SRCFILE(OSLIB/QCSRC) SRCMBR(FNDMSGCNT)  DBGVIEW(*SOURCE) REPLACE(*YES) TGTRLS(V7R1M0)

CRTPGM PGM(OSPGM/FNDMSGCNT) MODULE(OSLIB/FNDMSGCNT) BNDDIR(OSLIB/OS) TGTRLS(V7R1M0)
Now create the command
CRTCMD CMD(OSPGM/FNDMSGCNT) PGM(*LIBL/FNDMSGCNT) SRCFILE(OSLIB/QCMDSRC) SRCMBR(FNDMSGCNT) REPLACE(*YES)

I used the following request to find any messages with HA4i in the text from our HA4i (Multi-Node) message file.  
FNDMSGCNT MSGF(HAMN10/HAMNMSGF) SSTR(HA4I)

(IMPORTANT NOTE: it is case sensitive at this point so the text has to match in content and case for it to be found). In this sample the second level text caused the flag to be set not the message text which contains ‘HA4I’STS. 
 
Command test

Test of Command


I hope that you are following the flow and its explained enough for you, if not send me a message and I will try to clear up and questions you have.

In this post we have :
  1. Called functions defined in our Service Programs.
  2. Used lots of pointers
  3. Talked about strings versus character arrays
  4. Implemented a dowhile loop
  5. Created a command to front the program
  6. Discussed a few good practices
  7. Mentioned memory management and the compilers effects on that
  8. Used an IBM i API…
  9. Built something that is useful

That’s all for today, I know its a bit long and it needs some reading to get through but I hope it all makes sense? We will continue to add to this in future posts where we will attempt to add more functionality to the Service Programs and how that affects the signature management. We will look at how to open and use files and read through them, this will include some performance management by pre loading the data etc.

Happy ‘C’ coding.

Chris…