Build an ILE RPG Service Program: NOMAIN Source, Binder Language, CRTSRVPGM and UPDSRVPGM in 2026

A service program is a shared library of procedures that many programs can call. It is the ILE equivalent of a DLL or shared object, and it is the right place for logic that more than one program needs. This guide shows the full RPG path: write a NOMAIN module, control what it exports with binder language, create the service program, bind callers to it, and change it later without breaking them.

This is the hands-on build guide. For the architecture behind it (how ILE modules, programs and service programs fit together, how binding works in general, and how to design activation groups for a whole application), read IBM i ILE Binding, Activation Groups, and Service Programs.

The ILE Build Model

ILE separates compiling from binding:

  • A module (*MODULE) is compiled code that is not yet runnable. CRTRPGMOD creates one from RPG source.
  • A program (*PGM) binds one or more modules, and optionally service programs, into something you can call. CRTPGM or CRTBNDRPG create programs.
  • A service program (*SRVPGM) binds modules into a shared object whose procedures other programs import. CRTSRVPGM creates it.
/* 1. Compile the source to a module */
CRTRPGMOD MODULE(APPLIB/DATEUTIL) SRCFILE(APPLIB/QRPGLESRC) SRCMBR(DATEUTIL) DBGVIEW(*SOURCE)

/* 2. Bind it into a service program, using binder source to list the exports */
CRTSRVPGM SRVPGM(APPLIB/DATEUTIL) MODULE(APPLIB/DATEUTIL) +
          EXPORT(*SRCFILE) SRCFILE(APPLIB/QSRVSRC) SRCMBR(DATEUTIL) +
          ACTGRP(*CALLER) TEXT('Date utility service program')

Writing the Service Program Source

A service program module has no main procedure. Code it with Ctl-Opt NoMain and give each procedure callers may use the Export keyword.

**FREE
Ctl-Opt NoMain;

// Fiscal period (YYYYPP) for a calendar date. The fiscal year starts on 1 April.
Dcl-Proc FiscalPeriod Export;
  Dcl-PI *N Char(6);
    pDate Date(*ISO) Const;
  End-PI;
  Dcl-S fiscalYear Int(10);
  Dcl-S period Int(10);

  fiscalYear = %Subdt(pDate : *YEARS);
  If %Subdt(pDate : *MONTHS) < 4;
    fiscalYear -= 1;
  EndIf;
  period = %Rem(%Subdt(pDate : *MONTHS) + 8 : 12) + 1;
  Return %Char(fiscalYear) + %EditC(%Dec(period : 2 : 0) : 'X');
End-Proc;

// Number of Monday-to-Friday days from pFrom to pTo, inclusive.
Dcl-Proc WorkingDaysBetween Export;
  Dcl-PI *N Int(10);
    pFrom Date(*ISO) Const;
    pTo   Date(*ISO) Const;
  End-PI;
  Dcl-S current Date(*ISO);
  Dcl-S count Int(10);

  current = pFrom;
  DoW current <= pTo;
    // 0001-01-01 was a Monday, so 0 to 4 are Monday to Friday.
    If %Rem(%Diff(current : D'0001-01-01' : *DAYS) : 7) < 5;
      count += 1;
    EndIf;
    current += %Days(1);
  EndDo;
  Return count;
End-Proc;

// Date as text, YYYY-MM-DD.
Dcl-Proc FormatDateIso Export;
  Dcl-PI *N Varchar(10);
    pDate Date(*ISO) Const;
  End-PI;
  Return %Char(pDate : *ISO);
End-Proc;

The Export keyword makes a procedure available to the binder. It does not by itself decide what the finished service program exposes: that is controlled by the EXPORT parameter of CRTSRVPGM, covered next.

RPG exports procedure names in upper case (FISCALPERIOD) unless you override the name with ExtProc on the procedure interface. Remember this when you write binder source and prototypes.

Binder Language: Controlling the Exports and the Signature

Binder language is a small source language, kept in a source member (conventionally in QSRVSRC), that lists the symbols a service program exports. It has three statements:

  • STRPGMEXP starts an export list.
  • EXPORT SYMBOL('NAME') lists one exported procedure or data item.
  • ENDPGMEXP ends the list.
STRPGMEXP PGMLVL(*CURRENT) SIGNATURE('DATEUTIL_V1')
  EXPORT SYMBOL('FISCALPERIOD')
  EXPORT SYMBOL('WORKINGDAYSBETWEEN')
  EXPORT SYMBOL('FORMATDATEISO')
ENDPGMEXP

Why bother, instead of EXPORT(*ALL)? Because of the signature. A service program has a signature, which is the binder’s record of its export interface. A program that binds to the service program stores the signature it was bound with, and at run time the system checks that the service program still supports it. With EXPORT(*ALL) the binder generates the signature from the full export list, so adding or reordering an export changes the signature and every caller must be rebound before it will work. With binder source you control the signature and can keep earlier ones valid.

STRPGMEXP takes the parameters PGMLVL, SIGNATURE and LVLCHK. SIGNATURE('text') sets a value you choose (16 bytes); with the default, SIGNATURE(*GEN), the binder generates one from the export list. Exactly one export list may be PGMLVL(*CURRENT).

Adding a procedure without breaking callers

Add the new export at the end of the *CURRENT list, give that list a new signature, and keep the old list as a PGMLVL(*PRV) block that still carries the old signature:

STRPGMEXP PGMLVL(*CURRENT) SIGNATURE('DATEUTIL_V2')
  EXPORT SYMBOL('FISCALPERIOD')
  EXPORT SYMBOL('WORKINGDAYSBETWEEN')
  EXPORT SYMBOL('FORMATDATEISO')
  EXPORT SYMBOL('ADDWORKINGDAYS')
ENDPGMEXP

STRPGMEXP PGMLVL(*PRV) SIGNATURE('DATEUTIL_V1')
  EXPORT SYMBOL('FISCALPERIOD')
  EXPORT SYMBOL('WORKINGDAYSBETWEEN')
  EXPORT SYMBOL('FORMATDATEISO')
ENDPGMEXP

Programs bound to the old signature keep working because the service program still supports it. New programs bind to DATEUTIL_V2. Rules that keep this safe:

  • Never remove or reorder symbols in an export list that a signature is already published for.
  • Add new exports only at the end of the *CURRENT list.
  • Keep every published signature as a PGMLVL(*PRV) block for as long as callers might use it.
  • Removing a procedure, or changing a parameter list so that old callers break, needs a new service program name, not a new signature.

Check the signatures a service program supports with:

DSPSRVPGM SRVPGM(APPLIB/DATEUTIL) DETAIL(*SIGNATURE)
DSPSRVPGM SRVPGM(APPLIB/DATEUTIL) DETAIL(*PROCEXP)

*SIGNATURE lists the signatures; *PROCEXP lists the exported procedures.

Changing the Implementation: UPDSRVPGM

When only the internals change and the export interface stays the same, recompile the module and replace it inside the existing service program:

CRTRPGMOD MODULE(APPLIB/DATEUTIL) SRCFILE(APPLIB/QRPGLESRC) SRCMBR(DATEUTIL) DBGVIEW(*SOURCE)
UPDSRVPGM SRVPGM(APPLIB/DATEUTIL) MODULE(APPLIB/DATEUTIL)

UPDSRVPGM replaces the named module or modules in the service program without recreating it. It does not rebind the programs that call it. If you do change the export interface in a way that creates a new signature, callers bound to the old signature must be rebound; that is what the *PRV block above avoids.

Calling the Service Program from RPG

A caller needs a prototype for each procedure it imports. Because RPG exports names in upper case, either leave ExtProc off the prototype, as below, or use the same ExtProc value on both sides.

**FREE
Ctl-Opt DftActGrp(*No) ActGrp('ORDGRP') BndDir('APPLIB/APPBNDDIR');

Dcl-PR FiscalPeriod Char(6);
  pDate Date(*ISO) Const;
End-PR;

Dcl-PR WorkingDaysBetween Int(10);
  pFrom Date(*ISO) Const;
  pTo   Date(*ISO) Const;
End-PR;

Dcl-S orderDate Date(*ISO) Inz(D'2026-07-28');
Dcl-S shipDate  Date(*ISO) Inz(D'2026-08-05');

Dsply ('Fiscal period: ' + FiscalPeriod(orderDate));
Dsply ('Working days: ' + %Char(WorkingDaysBetween(orderDate : shipDate)));
*InLR = *On;

Put the prototypes in a /COPY member shared by every caller, so that they cannot drift from the real interface.

Binding Directories for Service Program Imports

A binding directory is a list of service programs (and modules) the binder searches when it resolves imports. It saves you naming every service program on every compile.

CRTBNDDIR BNDDIR(APPLIB/APPBNDDIR) TEXT('Application service programs')
ADDBNDDIRE BNDDIR(APPLIB/APPBNDDIR) OBJ((APPLIB/DATEUTIL *SRVPGM))
DSPBNDDIR BNDDIR(APPLIB/APPBNDDIR)
RMVBNDDIRE BNDDIR(APPLIB/APPBNDDIR) OBJ((APPLIB/OLDUTIL *SRVPGM))

CRTBNDRPG PGM(ORDLIB/PRCORDER) SRCFILE(ORDLIB/QRPGLESRC) SRCMBR(PRCORDER) +
          DFTACTGRP(*NO) ACTGRP(ORDGRP) BNDDIR(APPLIB/APPBNDDIR)

The binding directory can also be named in the source with Ctl-Opt BndDir('APPLIB/APPBNDDIR'), as in the example above. Binding to a service program is static binding: the link is resolved at bind time, which is faster than a dynamic program call. The ILE architecture guide covers where binding directories fit in the wider ILE binding model.

Choosing an Activation Group for a Service Program

An activation group holds the run-time resources of an ILE application, such as static storage, open files and, depending on settings, its commitment control definition. For a service program, CRTSRVPGM accepts:

  • ACTGRP(*CALLER), the default: the service program runs in the activation group of whoever called it, so it shares that program’s open files and commitment control.
  • ACTGRP(name): the service program runs in its own named group.

*NEW is not a valid value for a service program. Use *NEW only when creating a program with CRTPGM/CRTBNDRPG.

  • For a general-purpose utility such as DATEUTIL, ACTGRP(*CALLER) is usually correct.
  • Use a named group when the service program needs its own state, files or commitment scope.
  • Do not run ILE programs in the default activation group (*DFTACTGRP); that is the OPM compatibility mode, and CRTBNDRPG needs DFTACTGRP(*NO) to use service programs and activation groups.

For activation group design across a whole application (*NEW, *CALLER, named groups, commitment control scope, and debugging), see the ILE architecture guide.

Lookup pages for the parts of this build: RPGLE Service Programs Reference, RPGLE Binder Language and Signatures Reference, RPGLE Binding Directories Reference and RPGLE Activation Groups Reference. For command parameters: CRTRPGMOD, CRTSRVPGM and UPDSRVPGM and DSPSRVPGM.

Practical Checklist

  • One source member for the module and one binder source member per service program.
  • Start binder source with explicit SIGNATURE values from the first version.
  • Add exports at the end only; keep every published signature.
  • Keep prototypes in one /COPY member.
  • Use UPDSRVPGM for internal changes and a new signature for new exports.
  • Name binding directories after the application and keep them in a shared library.
  • Test callers with DSPPGM PGM(...) DETAIL(*SRVPGM) after any interface change to see the service programs and signatures they are bound to.

1 thought on “Build an ILE RPG Service Program: NOMAIN Source, Binder Language, CRTSRVPGM and UPDSRVPGM in 2026”

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top