Triton RPG compiles RPG source into standard JVM .class files, making compiled programs and service modules callable from any JVM language - Java, Kotlin, Scala, or anything else that targets the JVM. This chapter covers the surfaces a Java caller sees: method names, parameter conventions, error handling, metadata annotations, conversational state management, and native Java object interop from RPG.

Procedure Names (Calling Exported Procedures from Java)

The exported procedures of a --lib-compiled service module are callable directly from Java. Each becomes a public method spelled exactly as the source declares it - the compiler adds no prefix or suffix and does not change the letter case. So a module STRUTIL that exports FormatPhone is generated with a FormatPhone method (not a mangled or upper-cased one):

// STRUTIL.rpgle  (compiled with --lib)
Dcl-Proc FormatPhone Export;
  Dcl-Pi FormatPhone Char(13);
    digits Char(10) Const;
  End-Pi;
  // ...
End-Proc;
// Generated method on class STRUTIL - call it by the same name:
//     public RpgString FormatPhone(RpgString digits)
STRUTIL util = new STRUTIL();
RpgString formatted = util.FormatPhone(digits);

The procedure's parameter and return types map to their usual Java forms (see Type Mappings below); the point here is that the method name is the procedure's own name.

The method name is the procedure's declared spelling (its DCL-PROC name, or the P-spec name in fixed format). Because RPG identifiers are case-insensitive, the same procedure may be referenced in several letter cases across the source; every reference resolves to the one procedure, and the Java method carries the declared spelling regardless of how any call site spelled it. (This is the JVM-facing surface. A native build, by contrast, folds an exported procedure's external signature to upper case unless EXTPROC('...') quotes it - that folding does not apply here.)

Only exported procedures compiled with --lib appear on the Java surface; non-exported (internal) procedures are private.

In/Out Parameters (RpgTypeRef<T>)

RPG passes a parameter by reference by default (unless you mark it VALUE or CONST), so the procedure can write back to the caller's storage. Java has no by-reference call convention, so each scalar by-reference parameter is presented as a mutable holder, ltd.whitehorn.rpg.runtime.RpgTypeRef<T>. The caller seeds the holder with the value going in, the procedure reads and writes that same cell, and the caller reads the result back with get() after the call:

// COUNTER.rpgle  (compiled with --lib)
Dcl-Proc Bump Export;
  Dcl-Pi *N;
    value Int(10);          // by reference (the default)
  End-Pi;
  value += 1;
End-Proc;
// Generated method:  public void Bump(RpgTypeRef<Long> value)
COUNTER mod = new COUNTER();
RpgTypeRef<Long> n = new RpgTypeRef<>(5L);   // value in
mod.Bump(n);
long result = n.get();                       // value out → 6

The generic type argument is recorded in the method's signature, so IDEs and reflection see RpgTypeRef<Long>, not a raw holder.

Reserved procedure names

A generated program class inherits a small set of built-in methods from the Triton runtime (for example run, dump, commit). If an exported procedure's name matches one of these (case-insensitively), it cannot be exposed under its source name - a Java method of that name would clash with the inherited one. The compiler keeps the procedure fully functional inside RPG (bound and CALLB calls to it work as usual) but does not add the Java-facing method, and reports:

TRN2050 (warning) - Exported procedure 'Run' collides with a reserved runtime method name; it is not exposed to Java callers under its source name.

This is only ever a warning - the program still compiles. To make the procedure callable from Java, rename it to a non-colliding identifier.

Catching RPG Errors from Java

When you call compiled RPG from Java, any RPG-level runtime failure that the program does not handle itself (no MONITOR, (E) extender, or error indicator) surfaces as an exception. Every such exception has a single common root - ltd.whitehorn.rpg.types.RpgException - so a Java caller can handle all of them with one catch:

try {
    result = strutil.FormatPhone(digits);
} catch (RpgException e) {
    // any RPG-level failure: halt, cancel, file/status error, decimal overflow
}

RpgException extends RuntimeException, so it is unchecked - you are never forced to declare or wrap it. The hierarchy beneath it lets you narrow when you need the detail:

Exception Raised by
RpgHaltException A halt indicator (H1 - H9) ended the program
RpgCancelException A *PSSR error subroutine ended with *CANCL
RpgStatusException An operation raised an RPG status; .status() returns the %STATUS code
  RpgFileException A file operation error (file status 01xxx) - a subtype of RpgStatusException
  RpgEscapeException A SND-MSG *ESCAPE; .messageId() returns the message ID
  RpgDecimalOverflowException Numeric (decimal) overflow - a subtype of RpgStatusException

Because the subtypes share the RpgException root, catch (RpgException) catches all of them, while catch (RpgStatusException) catches all the status-bearing ones - RpgFileException, RpgEscapeException and RpgDecimalOverflowException - so a narrower catch for any of those three must come first.

RPG Metadata Annotations

Generated classes are self-describing: by default the compiler emits runtime-retained annotations (defined in ltd.whitehorn.rpg.runtime.meta) that record RPG detail the erased JVM signature cannot express. Java reflection and tooling can use them to discover a module's true interface - enumerate its exports, and for each parameter recover the passing mode, declared RPG type, and source name.

Annotation Placed on Records
@RpgProgram(name, kind) the class The canonical (source-spelled) name of the RPG module and the kind: PROGRAM or SERVICE_MODULE. This is the module's own name - --class-name renames the Java class, not the module, so the two can differ
@RpgExport(name) an exported procedure's method Marks the true export set and carries the source-spelled procedure name (reliable even where the JVM method name had to be escaped)
@RpgParm(name, mode, rpgType, nopass) a parameter Its source name, passing mode (VALUE / CONST / BY_REF), declared RPG type (e.g. PACKED(9:2)), and whether it is OPTIONS(*NOPASS)

Reflecting over an exported procedure gives back what the erased JVM signature loses - which argument is really a by-reference output, and each parameter's declared RPG type and source name:

Method m = STRUTIL.class.getMethod("FormatPhone", RpgString.class, PackedDecimal.class);
m.getAnnotation(RpgExport.class).name();   // "FormatPhone"
// parameter annotations: RawNumber CHAR(10) CONST, AreaCode PACKED(3:0) VALUE

An exported procedure is reachable under two method names: its source-spelled name (FormatPhone) and the internal proc_FORMATPHONE alias. Both carry the same annotations, so that a procedure whose name collides with a reserved runtime method - and therefore has no source-spelled method at all (see TRN2050) - still reports its metadata. To enumerate a module's exports, collect them by name and prefer the source-spelled method:

Map<String, Method> exports = new LinkedHashMap<>();
for (Method m : STRUTIL.class.getDeclaredMethods()) {
    RpgExport exp = m.getAnnotation(RpgExport.class);
    if (exp == null) continue;                              // not an export
    Method chosen = exports.get(exp.name());
    if (chosen == null || m.getName().equals(exp.name())) { // prefer FormatPhone over proc_FORMATPHONE
        exports.put(exp.name(), m);
    }
}

exports.forEach((name, m) -> {
    System.out.println("export: " + name);
    for (Annotation[] pa : m.getParameterAnnotations()) {
        for (Annotation a : pa) {
            if (a instanceof RpgParm p) {
                System.out.println("  " + p.name() + " " + p.rpgType() + " " + p.mode());
            }
        }
    }
});

The metadata is inert - it never affects runtime behaviour. It is emitted by default; pass --disable-annotations to omit it entirely (the generated bytecode is otherwise identical).

Conversational State - Snapshot and Restore

On IBM i, a program that returns with *INLR off stays activated: its variables and indicators keep their values, so the next call resumes where the last one left off. A program that sets *INLR on reinitializes on the next call. This is how a conversational transaction carries a job forward across several calls.

When you embed a compiled program in a host that cannot keep the program instance alive between requests - a stateless HTTP endpoint, for example - you reproduce that behavior by snapshotting the program's state at the end of one request and restoring it into a fresh instance at the start of the next. Every compiled program exposes two methods for this:

RpgProgramState captureState();          // snapshot logical state
void            restoreState(RpgProgramState state);   // hydrate a fresh instance

captureState() returns a snapshot of the program's logical state - its RPG fields, indicators (including *IN, *INLR, level and halt indicators), operation-error state (%ERROR / %STATUS), and the program message queue (the messages SND-MSG and QMHSNDPM have queued, with their types, their send order, and the keys QMHRMVPM removes them by - so a message subfile paints the same messages after hydration, and a key issued before the snapshot still selects its message afterwards). Live resources are not part of the snapshot: the JDBC connection and commit boundary (held by the activation group), the screen handler, and open files are re-acquired on the fresh instance, not carried in the state. The snapshot is a true point-in-time image - mutating the program after capturing does not change a snapshot already taken.

RpgProgramState is java.io.Serializable, so it can be written to a session store, a cache, or a database between requests and read back later.

The hydration order is: construct the program, inject its resources (setActivationGroup, _initSql, setScreenHandler as applicable), then restoreState(...), then run():

// End of request N - snapshot and persist.
RpgProgramState snapshot = program.captureState();
session.put("rpgState", snapshot);   // Serializable - store anywhere

// Start of request N+1 - fresh instance, re-acquired resources, restored state.
OrderEntry program = new OrderEntry();
program.setActivationGroup(group);   // live resources are injected, not restored
program._initSql(connection);
program.restoreState((RpgProgramState) session.get("rpgState"));
program.run();

A program whose logic ends by leaving *INLR off is the one you carry forward this way; if it sets *INLR on, capture a fresh snapshot (or none) so the next instance reinitializes, exactly as a re-activated program would on IBM i.

Java Interop (CLASS / OBJECT / EXTPROC *JAVA)

Triton RPG supports RPG's Java interop model: declare OBJECT-typed variables with CLASS(*JAVA:'classname'), prototype Java methods with EXTPROC(*JAVA:'class':'method'), and call them like RPG procedures.

Declaring Java object variables:

// Free-format: class name inside OBJECT type
DCL-S intObj OBJECT(*JAVA:'java.lang.Integer');
     D intObj          S               O   CLASS(*JAVA:'java.lang.Integer')

In fixed-format, O in column 40 is the OBJECT data type and CLASS(*JAVA:'classname') in the keyword area specifies the Java class.

Prototyping Java methods:

// Constructor - returns a new instance
DCL-PR newInteger OBJECT(*JAVA:'java.lang.Integer')
       EXTPROC(*JAVA:'java.lang.Integer':*CONSTRUCTOR);
  val INT(5) VALUE;
END-PR;

// Instance method - caller passes the object as the first argument
DCL-PR intValue INT(5)
       EXTPROC(*JAVA:'java.lang.Integer':'intValue');
END-PR;

Calling Java methods:

intObj = newInteger(42);           // constructor call
result = intValue(intObj);         // instance method (intObj is implicit first arg)

For instance methods, the object reference is passed as the first argument in the call but is not declared in the prototype's parameter list. Since Triton RPG compiles to JVM bytecode, OBJECT fields are native JVM object references - no JNI bridge is needed.

Type Mappings

The following table shows how RPG data types map to their JVM representations in compiled output. These are the types a Java caller sees on method signatures for VALUE and CONST parameters, return values, and public fields.

RPG Type JVM Representation Notes
PACKED(p:s) PackedDecimal Wraps BigDecimal; IBM i intermediate-precision rules
ZONED(p:s) ZonedDecimal
CHAR(n) RpgString (fixed-length) Blank-padded to declared length
CHAR(n) VARYING / VARCHAR(n) String Native Java String
INT(3) / INT(5) int
INT(10) long 4-byte signed on IBM i, stored as long
INT(20) long
UNS(3) / UNS(5) int
UNS(10) / UNS(20) long
FLOAT(4) float
FLOAT(8) double
IND (indicator / N) boolean
DATE RpgDate
TIME RpgTime
TIMESTAMP RpgTimestamp
BINDEC(p:s) PackedDecimal Binary decimal
OBJECT(*JAVA:'class') Object reference Native JVM reference

For by-reference parameters (the RPG default), each scalar type is wrapped in RpgTypeRef<T> so the procedure can write back to the caller's storage:

RPG Type By-Value JVM Type By-Reference Wrapper
PACKED / ZONED / BINDEC PackedDecimal / ZonedDecimal RpgTypeRef<PackedDecimal>
CHAR RpgString / String RpgTypeRef<RpgString> / RpgTypeRef<String>
INT / UNS int / long RpgTypeRef<Integer> / RpgTypeRef<Long>
FLOAT float / double RpgTypeRef<Float> / RpgTypeRef<Double>
IND boolean RpgTypeRef<Boolean>