Table of Contents
- Why this matters
- The core idea: a structure is a view, not the bytes
- Pointers and the 4-byte token
- Storage classes = how long memory lives
- BASED — overlaying a structure on existing memory
- DEFINED — a fixed overlay at an offset
- What you'll observe (and how to diagnose it)
- What each field looks like in the bytes
- Summary
- Related articles
Structures, pointers, Global Work Areas (GWA), and the BASED / DEFINED relationship.
Audience: developers and support engineers working with PL/I applications migrated to Java with Heirloom.
Why this matters
On the mainframe, PL/I memory is just addresses into a contiguous region of storage: a structure is a layout, a pointer is an address, and BASED / DEFINED variables are simply windows onto bytes that live somewhere else.
The Heirloom PL/I Runtime preserves these exact semantics on the JVM — but the JVM has no raw addresses, so the runtime models them faithfully with a small, consistent set of building blocks. Understanding that model explains the behavior you see, and turns symptoms like "a field is blank" or "memory keeps growing" into a clear root cause.
1. The core idea: a structure is a view, not the bytes
There are two distinct things in the runtime:
-
The template / view — the PL/I structure (
DCL 1 REC ...). It carries the layout (which field is at which offset, and how wide) and a current offset, but it does not own bytes. The same template object is reused, re-pointed at different storage over its life. - The memory — a backing byte buffer. This is where the actual data lives.
Think of the structure as a stencil and the buffer as the paper. The stencil tells you where each field sits; the paper holds the ink. Laying the same stencil over different sheets of paper reads/writes different data through the identical field layout.
When you read or write a field, the runtime computes buffer + field-offset and reads/writes the bytes there. This is why a migrated program behaves exactly like the original: field positions are honored byte-for-byte.
A practical consequence: a sub-structure or a BASED view does not have its own private memory — it shares the buffer of whatever it currently points at. Writing through one view is immediately visible through every other view of the same bytes, just as two PL/I pointers to the same area see each other's changes.
2. Pointers and the 4-byte token
A PL/I POINTER holds an address. The JVM can't store a Java object reference inside a byte buffer, so the runtime stores a 4-byte token in the bytes instead, and keeps the live pointer object in a lookup table:
-
Writing a pointer (
POINTERassignment,ADDR(x)) puts a token into the bytes and registers the pointer under that token. - Reading a pointer reads the 4-byte token back and looks up the registered pointer.
**The token in the bytes is the stored pointer value.** Two parts of a program that read the same memory read the same token, and therefore resolve to the same pointer — exactly the aliasing PL/I guarantees.
The runtime keeps two registries:
| Registry | Holds | Keyed by |
|---|---|---|
| Standalone pointer registry | Pointers not embedded in a structure | the token |
| Structure-field registry |
POINTER fields inside a structure |
structure + token |
You don't manage these directly, but they explain a common trace line: when a pointer is resolved, the runtime is looking the token up in one of these tables. A miss (the token is no longer registered) is the signature of storage that was reclaimed earlier than the program expected.
3. Storage classes = how long memory lives
PL/I storage isn't all equal. What the runtime keeps vs. reclaims follows the storage class of the memory, not a property of the pointer pointing at it:
| PL/I / CICS source | Storage class | Lifetime | Runtime scope |
|---|---|---|---|
EXTRACT EXIT GASET — a Global Work Area
|
region/global | lives for the application | APPLICATION |
GETMAIN SHARED |
shared storage | until an explicit FREEMAIN
|
APPLICATION |
plain GETMAIN
|
task storage | the task / transaction | TRANSACTION |
AUTOMATIC, ADDR(local var), working storage |
stack / per-call | the current block / transaction | TRANSACTION |
-
APPLICATION-scoped memory (a GWA, or
GETMAIN SHARED) is genuinely long-lived: it must survive across transactions and is only released when the program explicitly frees it. The runtime keeps it. - TRANSACTION-scoped memory is task storage. The runtime reclaims it at the end of the transaction so that long-running, pooled JVM threads don't accumulate dead work areas.
This single rule — retention follows the storage class of the memory — is the heart of how the runtime stays memory-stable under sustained transaction load.
Why this is occasionally subtle
The honest signal for "is this application storage?" is captured when the memory is first allocated/registered, not read live later. A field inside a structure may be created against ordinary task storage and only later be re-pointed at a Global Work Area. If the runtime judged scope by where the field currently points, it would mistake a short-lived duplicate for long-lived storage and keep it forever. By trusting the scope captured at allocation time — plus the explicit "shared" marker that GETMAIN SHARED / EXTRACT EXIT set — the runtime keeps exactly the real Global Work Areas and reclaims the rest.
4. BASED — overlaying a structure on existing memory
A PL/I BASED variable is a declaration that says "I have no storage of my own; read me through this pointer."
DCL 1 ORDER BASED(P),
3 NEXT POINTER,
3 DATA CHAR(100);
In the runtime this is the overlay operation: the ORDER template is pointed at the buffer that P addresses. From that moment, reading ORDER.DATA reads the 100 bytes at the pointer's target, through ORDER's layout.
Key points:
- The same
BASEDtemplate is re-overlaid on every access, because aBASEDreference may legitimately point somewhere new each time (a different list node, a different commarea). The runtime does not cache the target — it re-resolves the pointer so the view is always correct. -
BASEDintroduces aliasing: the overlaid structure and the original owner of the buffer are the same bytes. This is intended and matches PL/I. - When the pointer's target is APPLICATION-scoped (a GWA), the view inherits that scope — so the structure overlaid on a Global Work Area is correctly treated as long-lived.
5. DEFINED — a fixed overlay at an offset
A PL/I DEFINED variable overlays another variable at a known position:
DCL FULL CHAR(8);
DCL PART CHAR(2) DEFINED FULL POSITION(3); /* bytes 3-4 of FULL */
Where BASED follows a pointer (the target can change), DEFINED is pinned to a fixed offset within a named base. The runtime records the defined position and resolves PART as "the base's buffer, starting at the defined offset." (PL/I positions are 1-based; the runtime translates them to the correct byte offset.)
BASED vs DEFINED at a glance:
| BASED | DEFINED | |
|---|---|---|
| Target | wherever a pointer currently points | a fixed offset in a named base variable |
| Changes at runtime? | yes — re-resolved each access | no — the offset is fixed |
| Typical use | list/queue traversal, commareas, dynamic storage | reinterpreting part of a record, byte-level overlays |
Both share one principle with everything above: no copy is made — the defined/based variable is a window onto the base's bytes.
6. What you'll observe (and how to diagnose it)
Because memory is shared by design, most issues are about a window being open on the wrong bytes, or storage being released too early or kept too long.
| Symptom | Likely cause |
|---|---|
| A field reads blank / a screen renders empty | A Global Work Area or shared area was treated as transaction storage and reclaimed; the view now points at an empty buffer. |
| "Buffer too small" / TOOSMALL on a cross-program call | A pointer passed through a commarea/parameter was resolved after its storage was reclaimed (a token miss). |
| Memory grows steadily under load | Short-lived work areas are being retained as if application-scoped. |
| Two variables unexpectedly change together | Intended aliasing — they are BASED/DEFINED views of the same bytes. |
To diagnose, enable the runtime tracer (see JVM Options & Runtime Configuration):
java -Dheirloom.trace.file=/tmp/trace.csv -jar my-application.jar
The CSV records, for every pointer, whether it was kept or reclaimed and why (its storage scope). A pointer miss on read, paired with a reclaim row for the same token, pinpoints which storage was released before a later program needed it.
7. What each field looks like in the bytes
Within that one buffer, each field occupies a fixed number of bytes at a fixed position, encoded the same way it was on the mainframe — so your record layouts are preserved byte-for-byte. Values are stored big-endian (mainframe order), and fields are packed contiguously (no gaps): a structure's total size is the sum of its fields.
| PL/I type | Size in the buffer | How it's stored |
|---|---|---|
CHARACTER(n) |
n bytes |
Character data, space-padded to n
|
CHARACTER(n) VARYING |
n bytes |
Fixed width, space-padded; trailing blanks are trimmed when read |
FIXED BINARY |
2, 4, or 8 bytes | Signed whole number (two's-complement), big-endian; very large precision uses an extended form |
FIXED DECIMAL(p,s) |
⌈(p+1)/2⌉ bytes |
Packed decimal — two digits per byte, sign in the last nibble |
PICTURE |
one byte per picture position | Display/zoned format following the picture |
BIT(n) |
⌈n/8⌉ bytes |
Bits packed left-to-right (IBM bit ordering) |
POINTER / OFFSET
|
4 bytes | A token that identifies the target (not a raw machine address) |
COMPLEX |
real + imaginary parts | The two parts stored consecutively |
| nested structure | the inner structure's size | Laid out inline at the field's position |
| array | element size × number of elements | Elements stored back-to-back |
Why it matters: because the byte layout matches the original, data read from a file, a commarea, an MQ message, or a database column lands in exactly the positions your program expects — no re-mapping, and no surprises when a record is shared across programs.
Summary
- A structure is a view (layout + offset); the buffer holds the bytes.
- A pointer's value is a token in the bytes that resolves to a live pointer.
- Retention follows the storage class of the memory: Global Work Areas and
GETMAIN SHAREDare kept (application lifetime); task/transaction storage is reclaimed at the transaction boundary. - BASED overlays via a pointer (target can move); DEFINED overlays at a fixed offset. Neither copies — both are windows onto shared bytes.
- Each field has a fixed size and position, encoded big-endian and packed contiguously, so record layouts are preserved byte-for-byte.
Related articles
- JVM Options & Runtime Configuration — how to enable the tracer referenced above.
- How Transactions Are Managed (ETP + PL/I Runtime) — when transaction-scoped storage is reclaimed.
0 Comments