With this technique, programs bound to a service program can detect the version of the service program and select procedures exported by a specific version for use at run time.
An i5/OS service program is one type of MI program object whose MI object type code is hex 02 and subtype code is hex 03. In IBM's MI documentation, a SRVPGM is referred to as a bound service program. The word "bound" means an ILE program (non-bound means an OPM program). From the MI point of view, service programs share most attributes with those of their cousins, the bound program objects, except for a few differences.
For example, a service program always has at least one signature, while an ILE program doesn't; a service program often exports procedures and data items, while an ILE program can't. From the ILE point of view, the ILE program binder binds both procedures exported by module objects and service programs into a created ILE program object. This difference exists only for binding, binding by copy, or binding by reference. From the perspective of a programmer working on various platforms, an i5/OS service program is a shared library, the same as a .DLL on Microsoft Windows or a .so on UNIX-like platforms.
Working as "shared libraries" under i5/OS, service programs share most of the advantages with their common platform counterparts. Comparing with the "bind-by-copy" approach, a service program saves a lot of storage occupied by programs bound to the service program by reference. Service programs enable newly designed programs to reuse existing program logic and contribute to the modularity of software components. But at the same, service programs also suffer the same pains as their common platform counterparts. The most well-known problem raised by using shared libraries might be the "dependency hell," much like "DLL hell" on Microsoft Windows and "JAR hell" in the Java Runtime Environment (JRE). And one of the forms of "dependency hell" is dependency conflicts between different versions of a shared library and programs linked to the shared library. As to service programs and their service-consuming programs on i5/OS, the most remarkable example of "dependency hell" is the program signature violation (hex
Different platforms provide different solutions to solve the problem of dependency conflicts, and so does i5/OS. To solve this problem, you can specify multiple export symbol lists in the binder language source for your service program, each starting with a STRPGMEXP command and ending with an ENDPGMEXP command. Each export symbol list describes symbols exported by a specific version of your service program. The top one corresponds to the most current version of your service program, with the PGMLVL parameter on STRPGMEXP command set to *CURRENT. Each export symbol list corresponding to earlier versions of your service program appears one by one after the most current one, with the PGMLVL parameter on its STRPGMEXP command set to *PRV. This technique has been discussed thoroughly by many i5/OS experts. Here is a simple example of this technique, which is extracted from chapter 5, "Program Creation Concepts," of the famous book, ILE Concepts.
FILE: MYLIB/QSRVSRC MEMBER: FINANCIAL
/* export symbol list of the most current version of service program FINANCIAL */ STRPGMEXP PGMLVL(*CURRENT) EXPORT SYMBOL('Term') EXPORT SYMBOL('Rate') EXPORT SYMBOL('Amount') EXPORT SYMBOL('Payment') EXPORT SYMBOL('OpenAccount') EXPORT SYMBOL('CloseAccount') ENDPGMEXP
/* export symbol list for a previous version of service program FINANCIAL. [1] */ STRPGMEXP PGMLVL(*PRV) EXPORT SYMBOL('Term') EXPORT SYMBOL('Rate') EXPORT SYMBOL('Amount') EXPORT SYMBOL('Payment') ENDPGMEXP |
Code notes:
[1] This allows the existing ILE programs or service programs that use the FINANCIAL service program to remain unchanged.
The above-mentioned technique does resolve the problem of dependency conflicts between different versions of a service program and programs bound to the service program. Developers of a service program can enlarge the collection of exported symbols by adding a new export symbol list to the top of the binder language source of the service program and appending newly introduced exported symbol names to the end of the most current export symbol list. This technique seems like a perfect solution to the problem of dependency conflicts. But what if the service program being maintained exported many symbols belonging to several categories and has undergone many upgrades? If you keep maintaining such a service program using the above-mentioned technique, some side effects might appear:
- The binder language source filled with duplicated EXPORT commands will be too long to read from the beginning to the end, since each newer version of the export symbol list is a superset of each one prior to it. Extra documentation must be added to the binder language source to track the change history of the export symbol list of the service program.
- It will be hard to extract exported symbols belonging to a specific category from an export symbol list. Since newly added symbols can only be appended to the end of an export symbol list, exported symbols belonging to the same category might be scattered throughout the export symbol list.
- A program previously bound to an early version of the service program is not affected by a newer version of the service program that is deployed on the same machine, but it is hard to tell from the program's source code which version of the service program is used. The burden of maintaining the dependency relationships between multiple versions of a service program and programs bound to different versions of the service program will increase over time.
- A program that uses a service program has no way to detect the version of the service program currently deployed on the machine at run time and is limited to the versions of the service program that are the same as or newer than the one to which the program has been bound. In other words, a program that uses the service program cannot cooperate with all versions of the service program by postponing the detection and selection of the version of the service program from compile time to run time.
So can these challenges be overcome? Here, I will introduce one of all possible solutions. I refer to this technique as "exporting by interfaces."
Exporting by Interfaces
The goals of the technique introduced here are:
- Decouple a service program from programs bound to the service program.
- Enable programs bound to a service program to detect the version of the service program and to select procedures exported by a specific version for use at run time.
- Improve a service program's usability by categorizing exported procedures by functions.
The key features of this technique are:
- Export by interfaces from a service program instead of directly exporting symbols of procedures. The term interface I refer to here is a structure composed of procedure pointers and pointers to other interfaces.
- Categorize procedures of different categories into different interfaces.
- Extend an existing interface by adding a new interface instead of appending exported procedure pointers to the existing interface. This makes the change history of an interface clear to either the service program or applications that use the service program. The layout of the new interface should be compatible with that of its former version so that user applications can invoke procedures exported by the former version interface via a pointer to a new version interface.
- Manage and expose interfaces to user programs through a root interface.
- Expose a negotiation method to user programs from the root interface, by which a user program can detect what interfaces are supported by the local copy of a service program at run time.
Now let's walk through a real example utilizing the "exporting by interfaces" technique step by step.
Step 1: Using the "Exporting by Interfaces" Technique
Suppose that program MARCIE is interested in behaviors of dogs, such as barking, eating, etc. Service program CBROWN is responsible for implementing what MARCIE needs. CBROWN exports only one procedure, get_root_interface, and will never export any more symbols besides that. The following is CBROWN's binder language source, cbrownexp.bndsrc:
STRPGMEXP PGMLVL(*CURRENT) SIGNATURE('Charley Brown') EXPORT SYMBOL('get_root_interface') ENDPGMEXP |
The get_root_interface procedure returns a pointer to the root interface of type iroot_t, through which MARCIE can start her cooperation with CBROWN. The following is the declaration of interface iroot_t in cbrownint.rpgleinc:
/** * interface iroot_t */
d iroot_t ds qualified d based(dummy_ptr) * procedure pointer to iroot_t.negotiate_interface() [1] d negotiate_interface... d * procptr * procedure pointer to iroot_t.release() [2] d release... d * procptr * interface pointer to idog_t [3] d dog_ptr * * reserved for future use d * dim(13) |
Code notes:
[1] Procedure pointer to procedure negotiate_interface—MARCIE invokes this procedure to detect whether an interface is supported by CBROWN or not.
[2] Procedure pointer to procedure release—MARCIE invokes this procedure to notify CBROWN to free storage occupied by interface iroot_t and interfaces contained by it.
[3] This is the pointer to interface idog_t.
Here is the declaration of interface idog_t in cbrownint.rpgleinc. Interface idog_t exposed three functional methods: bark, eat, and sleep.
/** * interface idog_t */ d idog_t ds qualified d based(dummy_ptr) * procedure pointer to idog_t.release() [1] d release * procptr d bark * procptr d eat * procptr d sleep * procptr * interface ID of idog_t [2] d iid_dog c x'00010001' |
Code notes:
[]] This procedure is exposed by interface idog_t in the form of procedure pointers.
[2] This is a 4-byte interface ID, whose high-order 2 bytes indicates main interface ID and low-order 2 bytes indicates sub-interface ID. When an existing interface is extended, the main interface ID remains unchanged, while the sub-interface ID increases by 1.
Now let's have a look at the layout of the interfaces exposed by CBROWN (Figure 1).
Figure 1: These are the interfaces exposed by CBROWN. (Click images to enlarge.)
By now, it's clear how MARCIE can use procedures exposed by CBROWN regarding behaviors of dogs. First, MARCIE obtains a pointer to interface iroot_t by invoking CBROWN's only exported procedure: get_root_interface. MARCIE then invokes procedure iroot_t.negotiate_interface to ask for a pointer to interface idog_t with interface ID iid_dog. Finally, MARCIE invokes idog_t.bark, idog_t.eat, or idog_t.sleep. To invoke procedures exposed by interface iroot_t and idog_t, MARCIE needs the prototype of those procedures. The prototypes are provided in the following ILE RPG source, cbrownh.rpgleinc.
/** * @file cbrownh.rpgleinc * * ILE RPG header for MARCIE. */
/if not defined(Charley_s_dog_and_cat) /define Charley_s_dog_and_cat
* includes interface declarations [1] [3] /copy jv1,cbrownint
/* for user applications [2]*/ d root ds likeds(iroot_t) d based(root_ptr) d root_ptr s *
d dog ds likeds(idog_t) d based(dog_ptr) d dog_ptr s *
/** * @fn iroot_t.negotiate_interface [4] * * @param[in] root_ptr, pointer to interface iroot_t * @param[in] iid, 4 bytes interface ID * * @return interface pointer, *NULL if iid is not supported * by the current version of *SRVPGM CBROWN. */ d iroot_negotiate_interface... d pr * extproc( d root. d negotiate_interface) d root_ptr * d iid
/** * @fn iroot.release [5] * * release a used root interface pointer * * @param[in] root_ptr, root interface pointer */ d iroot_release... d pr extproc(root.release) d root_ptr *
/** * @fn idog.release [6] * * @param[in] root_ptr, pointer to interface iroot_t * @param[in] dog_ptr, pointer to interface idog_t */ d idog_release... d pr extproc(dog.release) d root_ptr * d dog_ptr *
/** * @fn idog.bark [7] */ d idog_bark... d pr extproc(dog.bark)
/** * @fn idog.eat */ d idog_eat pr extproc(dog.eat) d food
/** * @fn idog.sleep */ d idog_sleep pr extproc(dog.sleep) d hours 10i 0 value
* !defined Charley_s_dog_and_cat /endif /* eof -- cbrownh.rpgleinc */ |
Note that cbrownh.rpgleinc is used only by programs that use service program CBROWN. Procedures declared in cbrownh.rpgleinc are implemented in cbrown.rpgle, which is available in Appendix A at the end of this article.
Code notes:
[1] Interfaces declared in cbrownint.rpgleinc are included.
[2] The interface (structure) root is based on space pointer root_ptr. When the addressability of root_ptr is determined, procedures iroot_negotiate_interface and iroot_release, which are declared based on procedure pointers root.negotiate_interface and root.release, become valid. Likewise, procedures such as idog_release and idog_bark become valid once dog_ptr is set to a valid interface pointer of type idog_t that is returned by procedure iroot_negotiate_interface.
[3] Procedure get_root_interface, which is declared in cbrownint.rpgleinc, is the only symbol exported directly from service program CBROWN. It allocates necessary storage for an iroot_t interface, initiates procedure pointers contained by the interface, and then returns the initiated interface pointer to the caller. Here's the source code of procedure get_root_interface that is extracted from cbrown.rpgle:
* procedure get_root_interface p get_root_... p interface b export
d root ds likeds(iroot_t) d based(root_ptr) d root_ptr s * d len s 10i 0
d get_root_interface... d pi *
/free // allocate buffer for interface iroot_t len = %size(iroot_t); root_ptr = %alloc(len); propb (root_ptr : x'00' : len);
// set PROCPTRs in root root.negotiate_interface = %paddr(iroot_negotiate_interface); root.release = %paddr(iroot_release);
return root_ptr; /end-free p get_root_... p interface e |
[4] Procedure iroot_negotiate_interface accepts an interface pointer of type iroot_t and a 4-byte interface ID, iid. If the input iid is supported by the service program, this procedure returns an interface pointer that has been initiated according to the interface type indicated by iid. Otherwise, it returns a null pointer.
[5] Procedure iroot_release should be invoked by user programs to free storage allocated for interfaces exposed by CBROWN. It is also responsible to free all interfaces contained by it.
[6] Procedure idog_release releases an interface pointer of type idog_t.
[7] Procedures idog_bark, idog_eat, and idog_sleep implement program logic consumed by user programs.
Now MARCIE can take advantages of CBROWN's knowledge in dogs. See the following source of ILE RPG program MARCIE, marcie.rpgle.
/** * @file marcie.rpgle */
/copy jv1,cbrownh
d len s 10i 0
/free
len = %size(iroot_t); root_ptr = get_root_interface();
// obtain interface pointer to idog_t dog_ptr = iroot_negotiate_interface( root_ptr : iid_dog );
if dog_ptr <> *null; idog_bark(); idog_eat('bones'); idog_release(root_ptr : dog_ptr); endif;
iroot_release(root_ptr); *inlr = *on; /end-free /* eof -- marcie.rpgle */ |
After program MARCIE is compiled and bound to service program CBROWN, you can call MARCIE and get the following output:
DSPLY doggie said Bow, wow *N DSPLY dogs eat bones and dog food |
Step 2: Exposing More Interfaces
In step 1, we established an infrastructure through which more interfaces addressing different categories can be exposed. In step 2, we will add another interface, icat_t, to service program CBROWN. Supposing that interface icat_t exposes two functional methods, eat and sleep, the following changes should be made to the existing source units:
- Declare interface icat_t in cbrownint.rpgleinc.
- Add an interface pointer of type icat_t to interface iroot_t.
- Add prototypes of procedures exposed by interface icat_t in cbrownh.rpgleinc.
- Implement procedures exposed by interface icat_t in cbrown.rpgle.
Changes to cbrownint.rpgleinc:
/** * interface iroot_t */ d iroot_t ds qualified d based(dummy_ptr) * procedure pointer to iroot_t.negotiate_interface() d negotiate_interface... d * procptr * procedure pointer to iroot_t.release() d release... d * procptr * interface pointer to idog_t d dog_ptr * * interface pointer to icat_t [1] d cat_ptr * * reserved for future use d * dim(12)
/** * interface icat_t [2] */ d icat_t ds qualified d based(dummy_ptr) * procedure pointer to idot_t.release() d release * procptr d eat * procptr d sleep * procptr * interface ID of icat_t d iid_cat c x'00020001' |
Code notes:
[1] This is the pointer to interface icat_t.
[2] This is the declaration of interface icat_t.
Changes to cbrownh.rpgleinc:
d cat ds likeds(icat_t) d based(cat_ptr) d cat_ptr s * * ... ... /** * @fn icat.release * * @param[in] root_ptr, interface pointer to iroot_t * @param[in] cat_ptr, interface pointer */ d icat_release... d pr extproc(cat.release) d root_ptr * d cat_ptr *
/** * @fn icat.eat */ d icat_eat pr extproc(cat.eat) d food
/** * @fn icat.sleep */ d icat_sleep pr extproc(cat.sleep) d hours 10i 0 value |
Implementation of icat_t.eat in cbrown.rpgle:
d icat_eat pr d food * ... ...
* procedure icat_eat p icat_eat b
d msg s
d icat_eat pi d food
/free msg = %trim(food) + ' and fish'; dsply 'cats eat' '' msg; /end-free p icat_eat e |
After the changes to CBROWN shown above, the layout of interfaces exposed by CBROWN looks like Figure 2.
Figure 2: We've added an additional interface to CBROWN, icat_t.
To test the newly added interface icat_t, we may write a simple ILE RPG program such as the following, lucy.rpgle:
/** * @file lucy.rpgle * */
/copy jan,cbrownh
d len s 10i 0
/free len = %size(iroot_t); root_ptr = get_root_interface();
// cat cat_ptr = iroot_negotiate_interface( root_ptr : iid_cat );
if cat_ptr <> *null; icat_sleep(5); icat_eat('rice'); icat_release(root_ptr : cat_ptr); endif;
iroot_release(root_ptr); *inlr = *on; /end-free /* eof -- lucy.rpgle */ |
The result of calling program LUCY is the following:
DSPLY cats eat rice and fish |
At the same time, program MARCIE is not affected by the newly added icat_t interface and works fine with the new version of service program CBROWN. From the standpoint of MARCIE, CBROWN is still the same as the one that she has been bound to. His service program signature, his export symbol list, and the layout of interface iroot_t and idog_t exposed by him are still the same.
Step 3: Extending Interface idog_t
Suppose that now a new method, chase_cat, is be added to interface idog_t. To let user programs distinguish the former idog_t interface from the extended one, it's a good idea to introduce a new interface with a new interface name and interface ID. For example, we might name the newly added interface idog2_t. At the same time, it is also necessary to reserve the compatibility in layout between idog_t and idog2_t. Here's the solution:
* cbrownint.rpgleinc [1] d idog2_t ds qualified d based(dummy_ptr)
* parent interface, in this case it is idog_t d parent_int likeds(idog_t) * PROCPTRs exposed by interface idog2_t d chase_cat * procptr * interface ID of idog2_t d iid_dog d iid_dog2_lo c x'0002'
* cbrownh.rpgleinc * overlay structure dog2 over structure dog [2] d dog ds likeds(idog_t) d based(dog2_ptr) d dog2 ds likeds(idog2_t) d based(dog2_ptr) d dog2_ptr s *
* prototype of idog_t.eat d idog_eat pr extproc(dog.eat)
d food
* prototype of idog2_t.chase_cat [3] d idog2_chase_cat... d pr extproc(dog2.chase_cat) |
Code notes:
[1] In cbrownint.rpgleinc, declaration of the interface idog_t's extended version, idog2_t is added. To reserve the compatibility with interface idog_t, structure parent_int of type idog_t is placed at the beginning of the interface. The procedure pointer to chase_cat is appended after parent_int. The interface ID of idog2_t, iid_dot2, is also added to cbrownint.rpgle, which shares the same main interface ID with idog_t and has a different sub-interface ID x'0002'.
[2] This declares structure dog2 to overlay structure dog in cbrownh.rpgleinc. Once the addressability of dog2_ptr is determined, procedures exposed by interface idog_t and idog2_t become valid.
[3] The prototype of procedure idog2_chase_cat is exposed by interface idog2_t.
Now the layout of interfaces exposed by CBROWN looks like Figure 3:
Figure 3: We've extended the interface of idog_t.
Now let's design a test program that can use both versions of interface idog_t.
/** * @file snoopy.rpgle */
/copy jan,cbrownh
d len s 10i 0 d dog_ver s 1p 0 inz(2) d msg s
/free len = %size(iroot_t); root_ptr = get_root_interface();
// try interface idog2_t first [1] dog2_ptr = iroot_negotiate_interface( root_ptr : iid_dog2 ); if dog2_ptr = *null; // [2] dog2_ptr = iroot_negotiate_interface( root_ptr : iid_dog ); if dog2_ptr <> *null; dog_ver = 1; else; dog_ver = 0; endif; endif;
// use procedures exposed by idog_t if dog_ver > 0; idog_bark(); idog_eat('bones'); endif;
// use procedures exposed by idog2_t if dog_ver > 1; idog2_chase_cat(); else; msg = 'u have to chase the cat yourself.'; dsply 'sorry' '' msg; endif;
if dog_ver > 0; idog_release(root_ptr : dog2_ptr); endif;
iroot_release(root_ptr); *inlr = *on; /end-free /* eof -- snoopy.rpgle */ |
Code notes:
[1] Program SNOOPY first negotiates with the local copy of service program CBROWN to see whether it supports interface idog2_t or not.
[2] If the local copy of service program CBROWN does not support interface idog2_t, SNOOPY requests interface idog_t to CBROWN.
When working with a copy of CBROWN that supports interface idog2_t, the output of program SNOOPY will be this:
DSPLY doggie said Bow, wow *N DSPLY dogs eat bones and dog food *N DSPLY doggie said a dog is chasing a cat. |
When working with a copy of CBROWN that doesn't support interface idog2_t, the output of program SNOOPY will be this:
DSPLY doggie said Bow, wow *N DSPLY dogs eat bones and dog food *N DSPLY sorry u have to chase the cat yourself. |
Where Are We?
In the above discussion, we implemented the "exporting by interface" technique. This technique completely decouples service providers (service programs) and service consumers (programs that use the service programs). And we also implemented a framework upon which both the methods exposed by an interface and the interfaces exposed by a service program are extensible.
Over time, people have invented numerous methods to improve the flexibility of software architectures and at the same time cut down the expense of software maintenance. But as the famous saying goes, "There's no silver bullet." A technique or method works only when it is used in the proper circumstance and in the proper way.
Appendix A: ILE RPG Source cbrown.rpgle Used in Step 1
All source files used in this article are provided in zipped file src.tgz.
Here is the ILE RPG cbrown.rpgle used in stage 1. By now, the service program has exported its first interface idog_t.
/** * @file cbrown.rpgle */
h nomain h bnddir('QC2LE')
* interface definitions /copy jv1,cbrownint
* prototypes of ILE RPG system builtins [1] /copy /usr/local/include/rpg/mih52.rpgleinc
* prototypes of internal procedures /** * @fn iroot_t.negotiate_interface * * @param[in] root_ptr, interface pointer to iroot_t * @param[in] iid, interface ID * * @return interface pointer, *NULL if iid is not supported * by the current version of *SRVPGM CBROWN. */ d iroot_negotiate_interface... d pr * d root_ptr * d iid
/** * @fn iroot_t.release * * release an used root interface pointer * * @param[in] root_ptr, root interface pointer */ d iroot_release... d pr d root_ptr *
/** * @fn idog_t.release * * @param[in] root_ptr, root interface pointer * @param[in] dog_ptr, interface pointer */ d idog_release... d pr d root_ptr * d dog_ptr *
/** * @fn idog_t.bark */ d idog_bark pr
/** * @fn idog_t.eat */ d idog_eat pr d food
/** * @fn idog_t.sleep */ d idog_sleep pr d hours 10i 0 value
* procedure get_root_interface p get_root_... p interface b export
d root ds likeds(iroot_t) d based(root_ptr) d root_ptr s * d len s 10i 0
d get_root_interface... d pi *
/free // allocate buffer for interface iroot_t len = %size(iroot_t); root_ptr = %alloc(len); propb (root_ptr : x'00' : len);
// set PROCPTRs in root root.negotiate_interface = %paddr(iroot_negotiate_interface); root.release = %paddr(iroot_release);
return root_ptr; /end-free p get_root_... p interface e
* procedure iroot_negotiate_interface p iroot_negotiate_... p interface b
d intptr s * d len s 10i 0
d root ds likeds(iroot_t) d based(root_ptr)
d dog ds likeds(idog_t) d based(dog_ptr) d dog_ptr s *
d iroot_negotiate_interface... d pi * d root_ptr * d iid
/free select; when iid = iid_dog; len = %size(idog_t); dog_ptr = %alloc(len); propb (dog_ptr : x'00' : len);
dog.release = %paddr(idog_release); dog.bark = %paddr(idog_bark); dog.eat = %paddr(idog_eat); dog.sleep = %paddr(idog_sleep); root.dog_ptr = dog_ptr;
intptr = dog_ptr; other; intptr = *null; endsl;
return intptr; /end-free p iroot_negotiate_... p interface e
* procedure iroot_release p iroot_release b
d root ds likeds(iroot_t) d based(root_ptr)
d iroot_release... d pi d root_ptr *
/free // release interface pointer contained by iroot_t if root.dog_ptr <> *null; idog_release(root_ptr : root.dog_ptr); endif;
// releas storage occupied by iroot_t dealloc root_ptr; root_ptr = *null; /end-free p iroot_release e
* procedure idog_release p idog_release b
d idog_release... d pi d root_ptr * d dog_ptr *
d root ds likeds(iroot_t) d based(root_ptr)
/free dealloc dog_ptr; dog_ptr = *null; root.dog_ptr = *null; /end-free p idog_release e
* procedure idog_bark p idog_bark b d idog_bark pr
d msg s
c 'doggie said' dsply msg p idog_bark e
* procedure idog_eat p idog_eat b
d msg s d idog_eat pi d food
/free msg = %trim(food) + ' and dog food'; dsply 'dogs eat' '' msg; /end-free p idog_eat e
* procedure idog_sleep p idog_sleep b
* sleep(), see <unistd.h> d sleep pr 10i 0 extproc('sleep') d interval 10u 0 value
d idog_sleep pi d secs 10i 0 value
/free sleep(secs); /end-free p idog_sleep e
/* eof -- cbrown.rpgle */ |
Code notes:
[1] mih52.rpgleinc is an ILE RPG system built-in header provided by the open source project i5/OS Programmer's Toolkit.
LATEST COMMENTS
MC Press Online