Sometimes it's best to start from square one for a subject.
The Integraged Language Environment (ILE) has been written about many times over the years, and even I've written about it in a couple of books and training manuals myself.
But sometimes I feel a new beginning can refresh and rejuvenate and even inspire new ideas and methods.
We all seem so excited about new technologies and that is great. But for what we have to work with, and what our jobs require, I believe that taking full advantage of what has been given to us as a base for our root product (ie, RPG) will be much more fruitful that spending too much time on the latest and greatest that will only be replaced in months to come (tic).
Now, I'm not saying don't learn new things or experiment. In fact, I encourage the use of new technologies. But in the case of ILE, it's something that in the long run will not only make your job easier, but will increase your value as an IBM i programmer.
So, for this series I will be putting together a number of articles on implementing ILE into your applications starting with the most simple of functions, calculating the sum of two integers, and ending with much more complicated and useful functions.
I will also provide my insights, ideas and tricks I've developed and learned while implementing ILE for applications written not only for our software as an ISV, but many of the applications we've created for customers.
A few terms will be used interchangeably here as well. Function, Procedure and Subprocedure will all be used to describe a singular function inside of a module or service program.
On that same note, Module and Service Program may be used in place of each other as well, Module being the more generic term. When we are referring to either a Module or Service Program specifically, we'll try to make that obvious.
So, lets get started!
The environment I chose to create for a "Fresh Start" is a library named ILESAMPLE.
First, I created the library:
CRTLIB LIB(ILESAMPLE)
Next, I created the source physical files:
CRTSRCPF FILE(ILESAMPlE/QRPGLESRC) RCDLEN(112)
CRTSRCPF FILE(ILESAMPlE/QMODSRC) RCDLEN(112)
CRTSRCPF FILE(ILESAMPlE/COPYSRC) RCDLEN(112)
CRTSRCPF FILE(ILESAMPlE/QSRV
SRC) RCDLEN(112)
The four source physical files I am created are as detailed as the following:
Now if you're already confused, don't be. Things will make more sense as we go through the examples.
But, the first glaring issue would be the terms Module and Service Program. I'm sure you've heard them before, and maybe even asked about them only to be more confused as to the difference, and when to use each one. But, for now, just think of things this way:
You create a Module which contains the subprocedures (or functions). This Module can be bound "by copy" to your programs so the subprocedures within can be used. This means a physical copy of the module is added to the program.
A Service Program is created from a Module. Think of a Service Program as a "Stand Alone" copy of the Module. In other words, when you use the subprocedures within a service program it's like calling a separate program on your system.
The call to a subprocedure for a Module and a Service Program will look exactly the same. The only difference will be that the Service Program will be on it's own, and a Module must be "Bound" to the program.
Again, this will make more sense once we get into the gritty details, so lets get started with our first module.
For our first ILE module I wanted to keep things very simple. That's because I'd rather you focus HOW it is done vs WHAT it is doing.
With that said, the source for our first module is as follows:
H NOMAIN
****************************************************************
* Prototypes *
****************************************************************
D #getSumInt PR 10i 0
D Augend 10i 0 Const
D Addend 10i 0 Const
*//////////////////////////////////////////////////////////////*
* getSumInt - Return the sum of two integers *
*//////////////////////////////////////////////////////////////*
P #getSumInt B EXPORT
*--------------------------------------------------------------*
D #getSumInt PI 10i 0
D Augend 10i 0 Const
D Addend 10i 0 Const
*--------------------------------------------------------------*
/free
return (Augend + Addend);
/end-free
P #getSumInt E
I called this first source member F.MATH and placed it in the source physical file QMODSRC.
Now, you probably will notice a few things about how I personally do ILE. Not all of the methods I use are shown here, but a lot of them are:
These are not the only unique choices I've made over the years. You'll notice others as we progress, but here's a short list of additional methods I use:
But, before we get too far ahead of ourselves, let's take a look at the module we're working with.
First, we see the NOMAIN H spec. All this does is tell the compiler there is no main or entry module. In other words, we can't just CALL this module, instead we will be calling the subprocedures within.
Next we see the prototype(s) for any subprocedures in the module. In this case there is only one (so far). Normally this would be a /COPY directive, but for this example we are all inclusive.
The prototype is used in both the module and and program to describe the function and it's parameters. It's important to note that the names for the parameters aren't required, and are really "informational only". But, the data type and size declarations are required as well as any keywords such as CONST, VALUE or OPTIONS.
Next we see the Procedure Beginning (specified by the "B" following the procedure name). Also, we see an EXPORT keyword. This is used if we want to export the function to be used by any other applications other than itself. (There are actually times where you won't want to export a procedure, but we will cover that later).
Next is the Procedure Interface (PI). This is similar to the prototype and yes, seems redundant. But, this actually sets up the parameters to be usable in the procedure itself. In this case the parameter names are required as these names are how you will access the values of the parameters in the actual procedure.
Finally we have the Procedure End (specified by the "E" following the procedure name).
You may have noticed the two parameters to the procedure use the keyword CONST. This means that the values will be "read only" and cannot be changed within the procedure. It also means that you can pass the data as a "value" (ie, the number 10 instead of a variable with the value 10). If the parameters were both input and output, then you would omit this keyword and any change made to the parameter would be passed back to the calling program. You can also use the VALUE keyword which acts in a similar method as CONST. There has been much discussion about these keywords so instead of me getting into which is "better", you can decide that for your self after reading all that is available (or just as much as you want) from a internet search.
Now, there's no reason to have a procedure to find the sum of two numbers. But, hopefully the simplicity of this will allow you to focus on the procedure definition and use itself instead of what the procedure does.
To create this module we would issue the following command:
CRTRPGMOD MODULE(ILESAMPLE/F.MATH) SRCFILE(ILESAMPLE/QMODSRC)
Next, we will see how to use this module and procedure in a program.
Now that we have created our first ILE module, we will want to use it!
Below is the source code for a program named MATHTEST that we placed in QRPGLESRC:
****************************************************************
* Prototypes
****************************************************************
D #getSumInt PR 10i 0
D Augend 10i 0 Const
D Addend 10i 0 Const
****************************************************************
D Sum S 10i 0
****************************************************************
/free
Sum = #getSumInt(20:21);
*INLR = *ON;
/end-free
As you can see, this program is very short and simple. All we are doing is getting the sum of 20 plus 21.
You will see that in order to use our new function #getSumInt we need to define the prototype in the D-Spec our application. You've probably noticed that the prototype we using is exactly the same as the prototype that we used in the F.MATH module source, and you would be right. This is a case where we just copied the source from one member to another. Not the best practice, but one we are all used to. Later we'll show how to use /COPY books to update this.
Now, to compile this program will take two steps.
The first command we will run to create the MATHTEST module is as follows:
CRTRPGMOD MODULE(ILESAMPLE/MATHTEST) SRCFILE(ILESAMPLE/QRPGLESRC)
Once our module is created, we now will create a program from the two modules, MATHTEST and F.MATH.
CRTPGM PGM(ILESAMPLE/MATHTEST) MODULE(ILESAMPLE/MATHTEST ILESAMPLE/F.MATH)
What we have just done is created a program from two separate modules. This method is called "Bind By Copy". This means that a copy of the module is actually "bound" to the program. If we were to update the module, for example adding a new subprocedure, our MATHTEST program would still contain a copy of the old F.MATH module until it is recompiled at which time the new copy of the module would be bound to the program.
If we were to run the following command:
DSPPGM PGM(ILESAMPLE/MATHTEST) DETAIL(*MODULE)
We would see in the *MODULE detail the two modules that create this program:
|
We can now call our program just as we would any other program.
It's interesting to note that for a while we've been doing this same process, but only with one module. When we compile RPGLE programs using option 14 (or CRTRPGPGM) in the background the system first creates a module with the program name in library QTEMP, then creates a program with that single module. The module is then removed from the system as it is no longer needed. Try running the DSPPGM command above on a "normal" RPGLE program.
If we debug our program we can see how things work by adding a break point on the call to the #getSumInt procedure:
|
While a simple example, this should show you just how easy it is to create reusable code with RPG and ILE.
But, with every lesson that starts at the lowest level, before we start creating things for production we should start applying "best practice" methods.
I only put best practice in quotes since those can vary from person to person, but I will share what I feel are best practices and have served me well for over 15 years. And trust me, I have some code out there I wish I could go and and update, as I'm sure we all do. So this is really going to go through all my mistakes and bring us close to the point where we should be now.
Next... /COPY members for prototypes.
One thing you most likely noticed (if not, hopefully you read the mention of it) is that the prototype definitions for our first subprocedure is copied both in the module source as well as the program source where the module is used.
This is a perfect place to make use of /COPY members.
Remember in the first step we created a source physical file named QCOPYSRC? Well, this is where we're going to place our /COPY members containing the prototypes.
So, first let's simply copy the member F.MATH from QMODSRC to QCOPYSRC, but we are going to name the member P.MATH (the "P" stands for "Protoype").
CPYSRCF FROMFILE(ILESAMPLE/QMODSRC) TOFILE(ILESAMPLE/QCOPYSRC) FROMMBR(F.MATH) TOMBR(P.MATH)
Now, we can edit the P.MATH member so it contains only the following source (You'll notice that I also added a description that wasn't there before):
*//////////////////////////////////////////////////////////////*
* getSumInt - Return the sum of two integers *
*//////////////////////////////////////////////////////////////*
D #getSumInt PR 10i 0
D Augend 10i 0 Const
D Addend 10i 0 Const
We can now also go back and edit our F.MATH module to the following:
H NOMAIN
****************************************************************
* Prototypes *
****************************************************************
/COPY QCOPYSRC,P.MATH
*//////////////////////////////////////////////////////////////*
* getSumInt - Return the sum of two integers *
*//////////////////////////////////////////////////////////////*
P #getSumInt B EXPORT
*--------------------------------------------------------------*
D #getSumInt PI 10i 0
D Augend 10i 0 Const
D Addend 10i 0 Const
*--------------------------------------------------------------*
/free
return (Augend + Addend);
/end-free
P #getSumInt E
You'll see under the commented heading of "Prototypes" that we removed the prototype definition for our subprocedure and instead used the /COPY directive so that during compile time the source from the P.MATH member will be pulled in automatically.
Now, lets update or program MATHTEST as well:
****************************************************************
* Prototypes
****************************************************************
/COPY QCOPYSRC,P.MATH
****************************************************************
D Sum S 10i 0
****************************************************************
/free
Sum = #getSumInt(20:21);
*INLR = *ON;
/end-free
Again, we simply replaced the prototype definition with the /COPY directive.
Now, we can recreate the F.MATH module, create the MATHTEST module, then create the MATHTEST program:
CRTRPGMOD MODULE(ILESAMPLE/F.MATH) SRCFILE(ILESAMPLE/QMODSRC)
CRTRPGMOD MODULE(ILESAMPLE/MATHTEST) SRCFILE(ILESAMPLE/QRPGLESRC)
CRTPGM PGM(ILESAMPLE/MATHTEST) MODULE(ILESAMPLE/MATHTEST ILESAMPLE/F.MATH)
Now, both our module code and our program code is cleaned up so that it does not contain redundant prototypes.
Next we will look at prefixing the procedure names to make them more descriptive.
Before we go any further I'd like to share another practice I use.
When looking through source code and coming upon a procedure it will be obvious since I always prefix my procedure names with a hash tag (#). But, in the past few years I've also added a prefix to each procedure name so I know which module or service program it is from.
For example, our first procedure we've written is #getSumInt. This name is pretty unique as it stands, but what if we start using more generic names like #getValue or #setValue? Because each module will need to have unique procedure names, that's where the prefix comes in.
Examine the update /COPY source for our module, P.MATH:
*//////////////////////////////////////////////////////////////*
* #math_getSumInt - Return the sum of two integers *
*//////////////////////////////////////////////////////////////*
D #math_getSumInt...
D PR 10i 0
D Augend 10i 0 Const
D Addend 10i 0 Const
You'll notice we added a prefix of #math_ to our procedure name.
The same goes four our module source F.MATH:
H NOMAIN
****************************************************************
* Prototypes *
****************************************************************
/COPY QCOPYSRC,P.MATH
*//////////////////////////////////////////////////////////////*
* #math_getSumInt - Return the sum of two integers *
*//////////////////////////////////////////////////////////////*
P #math_getSumInt...
P B EXPORT
*--------------------------------------------------------------*
D #math_getSumInt...
D PI 10i 0
D Augend 10i 0 Const
D Addend 10i 0 Const
*--------------------------------------------------------------*
/free
return (Augend + Addend);
/end-free
P #math_getSumInt...
P E
And finally, our program MATHTEST:
****************************************************************
* Prototypes
****************************************************************
/COPY QCOPYSRC,P.MATH
****************************************************************
D Sum S 10i 0
****************************************************************
/free
Sum = #math_getSumInt(20:21);
*INLR = *ON;
/end-free
You can choose to do this or not. But, what I find is when I'm looking through source code and I see a procedure being called, with the appropriate prefix I'll know right away which module or service program it is being called from.
Next, we'll look into creating a Service Program from our module and look into the pros and cons of using Modules or Service Programs.
Now that we are comfortable with a simple module, lets look at it's big brother, the Service Program.
A service program can be created using a couple different ingredients, but the one thing that is required is a module. An optional ingredient is Binder Language, but we'll touch on that later. You can also include additional modules and or other service programs, but for now, lets look at creating a service program named F.MATH from our single module, also named F.MATH.
Creating a service program uses the Create Service Program (CRTSRVPGM) command. On this command you can specify a list of modules, service programs, and/or a Binding Directory which is nothing more than a list of modules and/or service programs. Our example will be quite simple though, since we are only interested in converting our F.MATH module into a service program:
CRTSRVPGM SRVPGM(ILESAMPLE/F.MATH) MODULE(ILESAMPLE/F.MATH) EXPORT(*ALL)
Examining our command we will so two obvious pieces. First is the name of the service program we are creating (F.MATH), and second is the name of a module that the service program will be created from (F.MATH).
We will also see that we are specifying EXPORT(*ALL). I'll be the first to say that in production using this is a no-no. The "why" will make more sense when we get into Binding Directories, Binder Language and Signatures but for now it will service the purpose. But please, do NOT use this keyword in production.
To understand why it's not a good idea, let's first examine what the EXPORT keyword does.
|
We see that the EXPORT keyword accepts 2 values, *SRCFILE and *ALL. What this does is tell the system which procedures we want to export to make available for use in our service program. When we say *ALL this tells the system that each procedure coded with the EXPORT keyword (remember that?) will be exported. So that means that for our service program the #math_getSumInt procedure will be exported and available in our F.MATH service program.
We can view the available procedures using the DSPSRVPGM command:
DSPSRVPGM SRVPGM(ILESAMPLE/F.MATH) DETAIL(*PROCEXP)
|
Up next, we will look at using the *SRCFILE option and Binder Language for the EXPORT keyword on the CRTSRVPGM command.
In the previous article we saw how to create a service program from a module. In this example we told the system to simply export all of the available procedures in the module.
In this article we will see how instead of using EXPORT(*ALL) we will use Binder Language to tell the system which procedures to export.
If you recall in the beginning we created a source physical file named QSRVSRC. This is where we are going to store our Binder Language. We like to name the Binder Language member the same as the source module. So in this case, it will be named F.MATH and look like the following:
STRPGMEXP
EXPORT SYMBOL(#math_getSumInt)
ENDPGMEXP
We will see that we have source that starts with STRPGMEXP (Start Program Export List) and ends in ENDPGMEXP (End Program Export List). Between these two keywords we will place the names of any procedures we wish to export from our module to be available for use in our newly created service program.
There can be more than one grouping of exported procedures for a service program between the STRPGMEXP and ENDPGMEXP keywords with different procedures being exported. Each grouping will (or should) have it's own unique Signature. This will be covered later, but just be aware that each Binder Language source member can contain from one to many different export groupings.
To create our service program we will use the following command:
CRTSRVPGM SRVPGM(ILESAMPLE/F.MATH) MODULE(ILESAMPLE/F.MATH) EXPORT(*SRCFILE) SRCFILE(ILESAMPLE/QSRVSRC) SRCMBR(*SRVPGM)
Take notice that instead of EXPORT(*ALL) we are using EXPORT(*SRCFILE). It is in this example only so we see the difference between the above, specifying EXPORT(*SRCFILE), and the previous method of creating a service program using EXPORT(*ALL).
The defaults for SRCFILE and SRCMBR also default nicely so that if we place our binder language in QSRVSRC and name it the same as the module source, we really only need to specify the following shorter command and take advantage of the system defaults:
CRTSRVPGM SRVPGM(ILESAMPLE/F.MATH) MODULE(ILESAMPLE/F.MATH)
Next we will take a look at what Service Program Signatures are and how they can be your friend, or your worst enemy.
Up until this point we've stayed away from the topic of Service Program Signatures mainly because it is something that can get confusing and interfere with learning the basics of programming with ILE.
But, before we go any further it is best we cover this subject. If you choose the route of using Service Programs in place of or along with modules, it will be something you will want to understand.
When you create a service program that service program gets what is called a Signature. A signature is a unique identifier of a service program that is created based upon the procedures that are exported from a module.
When you create a program that uses a Service Program that program will take the most current signature from the service program and store it in the program description. That way when it goes to call a procedure from that service program it can do a check on the signature.
Signatures can either be generated by the system, or you can provide your own signatures in your Binder Language on the STRPGMEXP label.
When you use the CRTSRVPGM command and specify EXPORT(*ALL) (which we won't do in production, right?) or leave the STRPGMEXP label blank without any other keywords the system will generate a signature for our service program.
If we edit our F.MATH binder language and prompt the STRPGMEXP label we will see the following keywords or parameters are available:
|
First we see the Program Level (PGMLVL) keyword which defaults to *CURRENT. This means that when we create this service program that this particular grouping of exports will be the "current" list.
Next we see the Signature Level Check (LVLCHK). This is similar to a level check we're all probably familiar with when it comes to files. This is used so that when a program uses a service program it checks the signature it was assigned when it was created to make sure it matches one of the signatures of the service program. If not, it will throw an error.
The third option is the Signature (SIGNATURE). This allows us to specify our own signature or the special value of *GEN which will let the system generate the signature. I've actually found that specifying my own signatures in the form of versions (ie, v1.0, v1.1, v2.2) as the signature tends to work well and be a little more descriptive. But, it's your choice as to how you want to do this.
Lets now take a look at the signature of our F.MATH service program and how it's referenced in our MATHTEST program.
DSPSRVPGM SRVPGM(ILESAMPLE/F.MATH) OUTPUT(*) DETAIL(*SIGNATURE)
When we execute this command we will be shown the signature(s) of our service program:
|
The signature we see here is actually a hex representation of a characters signature. We can also use the F11 function key to display the "text" version of our signature.
|
Interestingly in this case the signature is the single procedure name exported from the module, only backwards.
Let's go back into our F.MATH Binder Language source now and actually specify our own signature:
STRPGMEXP SIGNATURE('v1.0')
EXPORT SYMBOL(#math_getSumInt)
ENDPGMEXP
The signature in this case will be more descriptive and tell us that this is version 1 of our service program.
We now can recreate the service program:
CRTSRVPGM SRVPGM(ILESAMPLE/F.MATH) MODULE(ILESAMPLE/F.MATH)
And then again, view the signatures for our service program:
DSPSRVPGM SRVPGM(ILESAMPLE/F.MATH) OUTPUT(*) DETAIL(*SIGNATURE)
We now see the signature value:
|
If we press the F11 function key we will see the text representation of our new signature:
|
Next, we will update our program to use the F.MATH service program and Bind By Reference in place of the F.MATH module using Bind By Copy.
Now that we have taken the step of creating a service program from the F.MATH module, let's "bind" it to our MATHTEST program.
The first step is the same as if we were binding the F.MATH module. We create the MATHTEST module:
CRTRPGMOD MODULE(ILESAMPLE/MATHTEST) SRCFILE(ILESAMPLE/QRPGLESRC)
Next, we use the Create Program (CRTPGM) command again, but this time instead of specifying the F.MATH module, we specify the F.MATH service program created in the previous section:
CRTPGM PGM(ILESAMPLE/MATHTEST) MODULE(ILESAMPLE/MATHTEST) BNDSRVPGM(ILESAMPLE/F.MATH)
We now have a program named MATHTEST that is created from one module, and will reference our F.MATH service program instead of attaching a physical copy of it to the program.
Lets verify that using the following command:
DSPPGM PGM(ILESAMPLE/MATHTEST)
When we get to the *MODULE detail of the program object, we see that yes, just the MATHTEST module is bound to this program:
|
Now, if we view the *SRVPGM information we see that we have bound (by reference, not copy) the F.MATH service program:
|
Another thing to notice is that next to the F.MATH service program we show a signature. This signature needs to match the signature of the actual F.MATH service program object or the program will crash hard, similar to if you were using a file and there was a level check.
You're probably thinking at this point "but I like just using option 14 to compile... isn't there an easier way?"
There is an easier way! And that is by using Binding Directories which will be covered next.
Most of us on the IBM i are familiar with the concept of a Library List and how they work.
When using modules or service programs a similar concept can be used that makes compiling programs much easier. These are known as Binding Directories.
A Binding Directory is nothing more than a list of modules and/or service programs. When you compile your program and that program needs to bind to a module and/or service program you specify a binding directory (or more than one) on the CRTxxxPGM command. The system will then look through the modules and service programs listed in the binding directory for modules and service programs to bind to your program.
Lets see this in action. Lets first create a binding directory using the Create Binding Directory (CRTBNDDIR) command:
CRTBNDDIR BNDDIR(ILESAMPLE/MYBNDDIR) TEXT('My First Binding Directory')
Once the binding directory is created, we can work with it using the WRKBNDDIR command:
WRKBNDDIR BNDDIR(ILESAMPLE/MYBNDDIR)
You should be presented with a screen similar to the following:
|
If we use option 9 next to our binding directory we are presented with the following screen:
|
Because we just created our binding directory there are no entries in it. Remember, an "entry" will simply be a module or service program.
Lets add our F.MATH module to this binding directory by using option 1 on the first blank entry. Specify F.MATH for the Object, *MODULE for the Type and ILESAMPLE for the library. Once added, we should see it in the list:
|
Now that we have our F.MATH module in our binding directory, lets try creating our program MATHTEST again, but this time instead of specifying a module or service program to bind to it, let's instead specify our new binding directory. What's different is in this case we'll get to use the Create Bound RPG Program (CRTBNDRPG) command instead of the sequence of CRTMOD for each module, then CRTPGM to bind the modules into a program:
CRTBNDRPG PGM(ILESAMPLE/MATHTEST) SRCFILE(ILESAMPLE/QRPGLESRC) DFTACTGRP(*NO) BNDDIR(ILESAMPLE/MYBNDDIR)
Once this command is completed, use the DSPPGM command to display your MATHTEST program to verify that it's bound to the F.MATH module and not the F.MATH service program.
DSPPGM PGM(ILESAMPLE/MATHTEST) DETAIL(*MODULE)
Pretty neat, huh? This is much easier than issuing all the CRTMOD commands and then CRTPGM.
Now, let's add the F.MATH service program to our binding directory:
|
Now, issue the CRTBNDRPG command again to recreate our MATHTEST program and lets see what happens. Yep, that's right... it failed. Here is the messages you most likely will see in your job log:
CRTBNDRPG PGM(ILESAMPLE/MATHTEST) SRCFILE(ILESAMPLE/QRPGLESRC) DFTACTGRP(*NO) BNDDIR(ILESAMPLE/MYBNDDIR) |
What this error is telling us is that it tried to create the MATHTEST program but it found more than one reference to the #math_getSumInt() procedure. Because it was using our binding directory it would have found it both in the F.MATH module and the F.MATH service program.
So, how do we fix this?
The answer will depend on which path you choose for the type of binding you and your shop will use, Bind by Copy (using Modules) or Bind By Reference (using Service Programs). Once that decision is made, you'll remove either the module or service program from your binding directory.
For the sake of this series on ILE, let's take the Bind By Reference path and remove the F.MATH module from our binding directory. Once that is done we can issue the CRTBNDRPG command again and create our program. Then we can use the DSPPGM command to verify that the F.MATH module is not bound to our program, but the F.MATH service program is. Later in this series we will go back and look at the benefits and uses of each type of binding and where we've found it's best to use each and under which circumstances.
The last step in our adventure using Binding Directories is taking advantage of using an H-Spec to define the binding directory we want to use. We simply add one line of code to the top of our MATHTEST program:
H DFTACTGRP(*NO) BNDDIR('MYBNDDIR')
****************************************************************
* Prototypes
****************************************************************
/COPY QCOPYSRC,P.MATH
****************************************************************
D Sum S 10i 0
****************************************************************
/free
Sum = #math_getSumInt(20:21);
*INLR = *ON;
/end-free
This H-Spec defines some compiling options required when using a binding directory. This means that we can use this simplified command to create our program:
CRTBNDRPG PGM(ILESAMPLE/MATHTEST) SRCFILE(ILESAMPLE/QRPGLESRC)
You'll notice the DFTACTGRP and BNDDIR parameters can be left off as the H spec is used for those values. And yes, this means we can even use option 14 next to our program in PDM to compile!
Next, we will look at adding a new procedure to our service program and how that will affect the service program signature.
Now comes the time when we will add a new procedure (function) to our module, and then service program.
I always start with the module source itself (instead of the prototype source). This is because during the creation of the new procedure things may change and that could change the prototype definition. Here's an example:
H NOMAIN
****************************************************************
* Prototypes *
****************************************************************
/COPY QCOPYSRC,P.MATH
*//////////////////////////////////////////////////////////////*
* #math_getSumInt - Return the sum of two integers *
*//////////////////////////////////////////////////////////////*
P #math_getSumInt...
P B EXPORT
*--------------------------------------------------------------*
D #math_getSumInt...
D PI 10i 0
D Augend 10i 0 Const
D Addend 10i 0 Const
*--------------------------------------------------------------*
/free
return (Augend + Addend);
/end-free
P #math_getSumInt...
P E
*//////////////////////////////////////////////////////////////*
* #math_getDiffInt - Return the difference of two integers *
*//////////////////////////////////////////////////////////////*
P #math_getDiffInt...
P B EXPORT
*--------------------------------------------------------------*
D #math_getDiffInt...
D PI 10i 0
D Minuend 10i 0 Const
D Subtrahend 10i 0 Const
*--------------------------------------------------------------*
/free
return (Minuend - Subtrahend);
/end-free
P #math_getDiffInt...
P E
You'll see that in our updated module source we have added a new procedure named #math_getDiffInt which will return the difference of two numbers.
The next step will be updating our /COPY member (P.MATH) with the prototype of our new subprocedure. I find it's easiest to copy the top portion of the procedure source and simply remove everything except the comments (if needed) and the Procedure Interface (PI) section, then changing the PI to PR.
*//////////////////////////////////////////////////////////////*
* #math_getSumInt - Return the sum of two integers *
*//////////////////////////////////////////////////////////////*
D #math_getSumInt...
D PR 10i 0
D Augend 10i 0 Const
D Addend 10i 0 Const
*//////////////////////////////////////////////////////////////*
* #math_getDiffInt - Return the difference of two integers *
*//////////////////////////////////////////////////////////////*
D #math_getDiffInt...
D PR 10i 0
D Minuend 10i 0 Const
D Subtrahend 10i 0 Const
As you can see, the PI section of the module source and the PR section of the prototype are nearly identical. By copying the PI section for our prototype, this helps cut down on possible keying errors.
Now that we have our new procedure we need to recreate the F.MATH module:
CRTRPGMOD MODULE(ILESAMPLE/F.MATH) SRCFILE(ILESAMPLE/QMODSRC)
Now that we have a new module you may be wondering, is that going to affect anything that is using the F.MATH module or service program? At this point, the answer is no. Right now we simply have an updated F.MATH module that includes the new procedure.
If we wanted our programs to be able to use it, as before, we have two options. To bind by copy (using the new module) or bind by reference (by updating the service program).
Any program that is using the bind by copy (module) option simply needs to be recompiled so it can grab a new copy of the F.MATH module. Simple enough, right?
But, any program that is using bind by reference (service program) needs a couple extra steps before the new procedure is available:
When we update our binder language we want to make sure that when we do, we keep the old signature in tact so that any programs that use the updated F.MATH service program will continue to function. Remember that when we create a program that uses a service program, that program makes note of the current signature of that service program.
The other option (which we'll call the "brute force" method) would be to recreate the service program with only one signature (the new one that won't match any signature referenced by a program using the service program) and then recompiling every program that uses the service program to force it to get the latest signature. (whew.. that was a mouthful!) While this is an option, it isn't always the best, so lets focus on the method that will keep everything working.
Step One - Update The Binder Language
We want to update our binder language so that our newest version of our F.MATH service program will export our new procedure.
STRPGMEXP SIGNATURE('v2.0')
EXPORT SYMBOL(#math_getSumInt)
EXPORT SYMBOL(#math_getDiffInt)
ENDPGMEXP
STRPGMEXP PGMLVL(*PRV) SIGNATURE('v1.0')
EXPORT SYMBOL(#math_getSumInt)
ENDPGMEXP
The first thing we'll notice is that we have two export blocks now. The old on (v1.0) also has been changed so that the program level (PGMLVL) keyword is *PRV which will tell the system that this signature is from a previous version of the service program. The default for this keyword is *CURRENT. Remember, the current signature is the one any program created will reference. The previous signature(s) is/are there only for programs that haven't been recompiled since the update.
Step Two - Update the Service Program
When updating our service program we have two options: delete the old one and create the new one using the Create Service Program (CRTSRVPGM) command or use the Update Service Program (UPDSRVPGM) command.
Following is the command to use for the UPDSRVPGM command:
UPDSRVPGM SRVPGM(ILESAMPLE/F.MATH) MODULE(ILESAMPLE/F.MATH) EXPORT(*SRCFILE) SRCFILE(ILESAMPLE/QSRVSRC) SRCMBR(*SRVPGM)
Once this is done the first thing we'll notice is the service program now has two signatures (but, only one *CURRENT signature)
|
Step Three - Create a New Program
We had our test program MATHTEST before, but for the sake of understanding signatures I wanted to do things a little differently. So, copy your MATHTEST source to a new program named MATHTEST2 and update it so it looks like the following:
H DFTACTGRP(*NO) BNDDIR('MYBNDDIR')
****************************************************************
* Prototypes
****************************************************************
/COPY QCOPYSRC,P.MATH
****************************************************************
D Sum S 10i 0
D Diff S 10i 0
****************************************************************
/free
Sum = #math_getSumInt(20:21);
Diff = #math_getDiffInt(44:25);
*INLR = *ON;
/end-free
Once done we can create our new program.
CRTBNDRPG PGM(ILESAMPLE/MATHTEST2) SRCFILE(ILESAMPLE/QRPGLESRC)
At this point we have the following:
Next we will purposely cause a signature error just so that we are aware of what could happen if things are done wrong.
This next section will be optional since when we are done, we will end up with the same setup as with the previous post.
The purpose of this is to show you what a signature error will look like when it occurs. But, in order to do that, we need to first change our binder language and then recreate our service program so that it contains only the latest signature. In other words, we need to purposely do something wrong (which we'd never do normally).
So, first lets copy the member F.MATH in source physical file QSRVSRC to a new member called F.MATHERR (so we know this binder language will cause an error). Then, let's remove the reference to the previous signature . When we're done, the source for our F.MATHERR member should look like this:
STRPGMEXP SIGNATURE('v2.0')
EXPORT SYMBOL(#math_getSumInt)
EXPORT SYMBOL(#math_getDiffInt)
ENDPGMEXP
Now, let's update our F.MATH service program using the F.MATTERR binder language so that when done, only one signature will be available.
UPDSRVPGM SRVPGM(ILESAMPLE/F.MATH) MODULE(ILESAMPLE/F.MATH) EXPORT(*SRCFILE) SRCFILE(ILESAMPLE/QSRVSRC) SRCMBR(F.MATHERR)
Once done, if we display the details of our service program we can verify that yes, only one signature has been exported:
|
Now, if we leave things as they are, our program MATHTEST2 should run without issue since when it was created, it used the v2.0 signature. Go ahead and run that program to verify.
Program MATHTEST was compiled when only signature v1.0 was exported. What happens if we call that program now that our F.MATH service program doesn't export the v1.0 signature?
Additional Message Information |
As we can see, our program crashes and reports a signature violation error.
How could we fix this? There really are two ways:
In our case we're choosing option 1, so lets update our F.MATH service program and put it back to the state it was previously when it had exported 2 signatures:
UPDSRVPGM SRVPGM(ILESAMPLE/F.MATH) MODULE(ILESAMPLE/F.MATH) EXPORT(*SRCFILE) SRCFILE(ILESAMPLE/QSRVSRC) SRCMBR(*SRVPGM)
Once this is done we can verify both signatures exist in the service program and that both programs MATHTEST and MATHTEST2 will run without issue.
Next, we will look at the difference between Global and Local Variables when using subprocedures.