Binder language is a very simple "language" that's used to tell the Create Service Program (CRTSRVPGM) command which procedures should be callable from outside of a service program. I put "language" in quotes, because it's not really a programming language. Instead, it's a list of procedures, with a few additional options available to describe how signatures are generated.
I've been involved in several online discussions about the best way to use binder language, and for the most part, everyone agrees on how to use it. Most debates about binder language center around how signatures should be generated. After hashing things out in the debate, most people settle on a single method: hard-coded signatures. This article explains why.
When you create ILE modules, you have the option to declare subprocedures as exported or not exported. For example, in RPG, you use the EXPORT keyword on the P-spec as follows:
P doSomething B EXPORT
D doSomething PI 10i 0
D parm1 10a const
D parm2 12p 2
.
.
(code goes here )
.
.
P E
This means only that a procedure is exported from the module. It does not mean that a procedure is exported from a service program built from this module. For example, if you build a service program from two modules, you may want to have "internal" routines callable from either of the two modules of the service program but not callable from the service program's caller.
This means that each procedure in your service program should fall into one of three categories:
I hope my point is clear: The EXPORT keyword makes a procedure callable from outside of the *MODULE, whereas binder language makes it callable from outside of the *SRVPGM.
Binder language is coded in a separate source member from the rest of the service program. This is typically placed in a source file named QSRVSRC and given a member name that matches the service program's name.
Setting aside signature generation for now, the binder language should look like this:
STRPGMEXP (signature info goes here)
EXPORT SYMBOL(startSomething)
EXPORT SYMBOL(doSomething)
EXPORT SYMBOL(finishSomething)
ENDPGMEXP
Tip: Personally, I prefer not to use quotes in the SYMBOL keyword when coding RPG service programs unless I've specified ExtProc() on the prototype. When you leave off the quotes, as I did above, the names will be converted to uppercase when the service program is created—which is also what RPG does to a prototype when you don't specify ExtProc(). I find that's the most intuitive way to do it.
In this example, my service program will have three different procedures exported from it. If there are any other procedures with the EXPORT keyword specified, they will be available to other modules in the service program but not callable from the outside of the service program.
Binder source also establishes the "number" of each export. This is important because, under the covers, programs that are bound to my service program will always call the routines by number. The numbers are generated purely from the binder language source—the sequence of the routines in your source member doesn't matter at all when you're using binder language. It's purely decided upon from the sequence of your EXPORT keywords. For example:
STRPGMEXP (signature info goes here) EXPORT SYMBOL(startSomething) #1 EXPORT SYMBOL(doSomething) #2 EXPORT SYMBOL(finishSomething) #3 ENDPGMEXP
A program that calls the startSomething() procedure will do that by calling export #1. Once it's bound, it forgets that the routine is named startSomething() and thinks of it purely as #1. Likewise, a program that calls the finishSomething() procedure will call #3.
Service program signatures are designed to protect you from mistakes in calling the wrong routine. ILE signatures don't consider the parameter list at all and aren't intended to make sure your parameter list is correct. They only attempt to verify that you're calling the correct procedure. If the export number of a procedure may have changed, you want your program to detect that so you don't end up calling the wrong one.
Many people suggest that signatures are similar to the "record format level check" used on physical files. If a physical file is generated with LVLCHK(*YES), a program that uses the external definition will remember the record format level. Each time the program opens the file, it'll verify that the format level hasn't changed. Folks say that service program signatures do the same thing, except that they check that the procedure numbers haven't changed.
Unfortunately, IBM's implementation of service program signatures is not nearly as robust as that of record format levels. With signatures, the checking doesn't work as well as you might expect it to.
Let's say you're using your service program, and all is well. Now you decide to add a new procedure named doSomethingMore(). The ILE Concepts manual (here are links to the 6.1 manual and the 5.3 manual), as well as many ILE teachers, would suggest that you do the following:
STRPGMEXP PGMLVL(*CURRENT)
EXPORT SYMBOL(startSomething)
EXPORT SYMBOL(doSomething)
EXPORT SYMBOL(finishSomething)
EXPORT SYMBOL(doSomethingMore) <--- procedure that was added
ENDPGMEXP
STRPGMEXP PGMLVL(*PRV)
EXPORT SYMBOL(startSomething)
EXPORT SYMBOL(doSomething)
EXPORT SYMBOL(finishSomething)
ENDPGMEXP
The idea is that the service program will now have two signatures; both of them will be generated by the system. The *PRV group contains the original procedures and will be used to calculate the first signature. Provided that you haven't changed anything, the signature will be the same as in the previous example. Because you've specified PGMLVL(*PRV), however, ILE won't use this group to determine the export numbers, it'll use this only to calculate an additional signature.
The *CURRENT group will be used to generate the new signature and will also be used to calculate the export numbers used to call the procedures.
With this method, your service program will have two signatures, and both will be considered legal. The theory is that this protects you from mistakes (as described in the previous section). Unfortunately, you aren't as safe as you might think! Indeed, I find that this accomplishes nothing. You can use a hard-coded signature, and it'll work just as well, if not better. And that's the source of the debate—some people, at least at first, disagree with me when I tell them that the *CURRENT/*PRV method doesn't offer them any more protection from a hard-coded signature.
Consider what happens if you make a mistake:
STRPGMEXP PGMLVL(*CURRENT)
EXPORT SYMBOL(startSomething) #1
EXPORT SYMBOL(doSomething) #2
EXPORT SYMBOL(doSomethingMore) #3 <--- WHOOPS! Added it in the middle!
EXPORT SYMBOL(finishSomething) #4
ENDPGMEXP
STRPGMEXP PGMLVL(*PRV)
EXPORT SYMBOL(startSomething)
EXPORT SYMBOL(doSomething)
EXPORT SYMBOL(finishSomething)
ENDPGMEXP
In my opinion, if ILE were designed correctly, the CRTSRVPGM command would detect that finishSomething has changed from position 3 to position 4 and would warn you. But it doesn't. Existing programs that previously called finishSomething were doing "call export#3". And unless you run UPDPGM or UPDSRVPGM or re-create them completely, they will continue to do "call export#3"—the problem is, export#3 is no longer the "finishSomething" routine. Now it's "doSomethingMore", and you're calling it by mistake! WHOOPS!
You're probably thinking, "Shouldn't I get a signature violation error?" While I would happily agree that you should, the fact is that you won't.
The *PRV group will have the signature you were previously using, so it will let you call the service program without error. However, only the *CURRENT group is used to determine the export numbers. So the fact that finishSomething used to be #3 doesn't matter; the system won't detect that it has changed, and it'll let you keep calling #3, even though it's now a different routine!
Using *CURRENT/*PRV didn't protect you. I'm still wondering under what situation using *CURRENT/*PRV *would* protect you. I haven't found any situation where it would.
The only way to properly maintain compatibility so that you won't accidentally call the wrong routine is to add the export to the end of the binder source, like this:
STRPGMEXP PGMLVL(*CURRENT)
EXPORT SYMBOL(startSomething)
EXPORT SYMBOL(doSomething)
EXPORT SYMBOL(finishSomething)
EXPORT SYMBOL(doSomethingMore) <--- HURRAY! it's on the end.
ENDPGMEXP
STRPGMEXP PGMLVL(*PRV)
EXPORT SYMBOL(startSomething)
EXPORT SYMBOL(doSomething)
EXPORT SYMBOL(finishSomething)
ENDPGMEXP
This way, your existing exports aren't renumbered—finishSomething is still export #3. As long as you know this and do it consistently, this will give you backward compatibility. But if you mess it up, the signature won't catch it. So you're doing the (admittedly minor) work of copying the signature block and getting no protection for your efforts.
My problem is that after I've done this 500 or so times (I'm not exaggerating—I really do this 500+ times in some service programs), I end up with a large, unwieldy source member. That was driving me crazy. It was deterring me from adding more subprocedures. It didn't make sense to "add just one more procedure," because I'd have to copy the block, so I'd end up putting it off until I had lots of them to add so I could do it all at once and have fewer signature blocks. This situation wasn't good, because it was holding me back.
Let's look at a hard-coded signature, which is the way I recommend doing it:
Back in the original example, before the change, you have the following—the only difference now is that I've added a hard-coded signature:
STRPGMEXP SIGNATURE('SRVPGM SIG 1.0 ')
EXPORT SYMBOL(startSomething)
EXPORT SYMBOL(doSomething)
EXPORT SYMBOL(finishSomething)
ENDPGMEXP
Tip: I'm careful to make my signature 16 chars long, otherwise I get warning messages when I create my service program. It's not required—I just do it to avoid the extra warning messages.
If, whoops, I make a mistake (just as I did with *CURRENT/*PRV) and put it in the middle, I still have the same problem:
STRPGMEXP SIGNATURE('SRVPGM SIG 1.0 ')
EXPORT SYMBOL(startSomething)
EXPORT SYMBOL(doSomething)
EXPORT SYMBOL(doSomethingMore) <--- WHOOPS! it's in the middle.
EXPORT SYMBOL(finishSomething)
ENDPGMEXP
This creates the exact same problem it did with *CURRENT/*PRV. ILE won't tell me that I made a mistake. The system doesn't detect it. While I haven't gained anything by using a hard-coded signature, I also haven't lost anything. The only advantage is that I don't have to copy/paste my signature block.
Just as with the *CURRENT/*PRV method, if I want to do it correctly, I must put new procedures on the end:
STRPGMEXP SIGNATURE('SRVPGM SIG 1.0 ')
EXPORT SYMBOL(startSomething)
EXPORT SYMBOL(doSomething)
EXPORT SYMBOL(finishSomething)
EXPORT SYMBOL(doSomethingMore) <--- HURRAY! it's on the end.
ENDPGMEXP
Now I've achieved compatibility. My existing callers will work. I don't have to run UPDSRVPGM or UPDPGM, since the export numbers didn't change, and the signature didn't change. Complete backward compatibility. Exactly the same as the *CURRENT/*PRV solution, except that I don't have to copy the signature block.
Now what happens if I want to break compatibility? Let's say I'm making a change to my service program that I know won't be compatible, and I want make sure that every caller gets recreated. If it doesn't get recreated, I want it to fail with a signature error.
With *CURRENT/*PRV, you'd do that by deleting all the *PRV blocks. Simply eliminate every single *PRV block so all you have is a *CURRENT block.
STRPGMEXP PGMLVL(*CURRENT)
EXPORT SYMBOL(startSomething)
EXPORT SYMBOL(doSomething)
EXPORT SYMBOL(doSomethingMore)
EXPORT SYMBOL(finishSomething)
ENDPGMEXP
Now it doesn't matter that I put doSomethingMore() in the middle, because all callers are going to be rebound anyway! Again, the system didn't detect this for me, I had to tell it that I wanted to break compatibility by deleting the *PRV blocks. It works nicely, but in my opinion, it's not very intuitive. Would you have thought of doing this to force callers to be rebuilt?
To do the same thing with a hard-coded signature, I just change the version number in the signature.
STRPGMEXP SIGNATURE('SRVPGM SIG 1.0a ')
EXPORT SYMBOL(startSomething)
EXPORT SYMBOL(doSomething)
EXPORT SYMBOL(doSomethingMore)
EXPORT SYMBOL(finishSomething)
ENDPGMEXP
That's (again, very slightly) simpler than deleting all the *PRV blocks, and it achieves the same thing. But to me, changing a version number is more intuitive.
I'm not sure it would've occurred to me to delete all the *PRV blocks if someone hadn't told me—but changing the version I might've thought of on my own. Also, when you use DSPPGM or DSPSRVPGM on the caller, you'll be able to see in a human-readable string exactly which version it's using .
But please remember that this should come up only rarely. Whenever you can, keep the signature the same and just add new routines to the end. That will make maintaining your service program easy because you won't have to rebind the existing callers.
As far as compatibility or protection against mistakes, *CURRENT/*PRV doesn't provide any more protection than a hard-coded signature does. You can easily make the same mistakes with *CURRENT/*PRV as you would with a hard-coded signature, and the system won't detect your mistakes or protect you from them.
So you might as well use a hard-coded signature and save yourself the extra work of copying and pasting the export block. It may not seem like much of a savings at first, but when your service program has hundreds of signatures, you may change your mind.
One final point: If you look at all the IBM-supplied service programs, you'll see that they hard-code all their signatures.
Questions? Comments? Disagree? Please leave a comment, below. Thanks!