FOR$ - FORTRAN Support Routines
Overview
FOR$ is a call-compatible re-implementation of the OpenVMS FORTRAN run-time support library for x86 and ARM Linux. It provides the FOR$ entry points that DEC and VSI FORTRAN object code calls at run time, so migrated applications link and execute while preserving language features, execution behaviour, and system interactions. It is the runtime the compiled or translated object links against, not the compiler and not the source translator.
Two concerns dominate the library: numeric-to-text conversion through the FOR$CVT family, and record access through FOR$RAB over Sector7's RMS layer. FOR$ ships inside VX/RT and integrates with the rest of the OpenVMS run-time services on Linux, so FORTRAN applications that depend on FOR$ routines can be ported directly and keep their original behaviour.
<!-- Webflow embed (min). Source: for-api-page-technical/for-tech-body-embed-dark.html → python3 website/embeds/minify_webflow_embed.py … -->
<div class="s7-tech-embed" data-vxford-body-embed="1" data-technical-mega-embed="1" lang="en"><h2 id="architecture">Architecture and components</h2><p>
FOR$ is a linked run-time library, not a server process. There is nothing to administer:
the FOR$ entry points are archived into <code>libvxrt.a</code> and linked directly into the
migrated FORTRAN application. At run time the library owns a per-unit channel table, hands
out RMS Record Access Blocks, and formats numeric values into edited character strings. The
table maps each OpenVMS FORTRAN run-time concept to its Linux implementation.
</p><div class="table-wrapper"><table><thead><tr><th>OpenVMS concept</th><th>FOR$ implementation</th><th>Role</th><th>Status</th></tr></thead><tbody><tr><td><code>FOR$CVT_D_TE</code> (D_float to text)</td><td>Base numeric converter</td><td>Formats a double into an edited output string; the shared converter behind the CVT family.</td><td><span class="ac-live">live</span></td></tr><tr><td><code>FOR$CVT_G_TE</code> / <code>FOR$CVT_H_TE</code></td><td>G_float and H_float converters</td><td>Format G_float and H_float values to text through the shared conversion path.</td><td><span class="ac-live">live</span></td></tr><tr><td><code>FOR$RAB</code> (unit to RAB)</td><td>Record Access Block accessor</td><td>Validates a FORTRAN unit and returns its RMS Record Access Block.</td><td><span class="ac-live">live</span></td></tr><tr><td>FORTRAN unit to RMS channel table</td><td>Per-unit channel list</td><td>Per-unit channel table sized to the maximum unit count, allocated once at initialisation.</td><td><span class="ac-live">live</span></td></tr><tr><td>OPEN / CLOSE</td><td>Record-I/O runtime</td><td>Opens and closes units, builds the FAB and RAB, and honours KEEP, DELETE, PRINT, and SUBMIT dispositions.</td><td><span class="ac-live">live</span></td></tr><tr><td>READ / WRITE / REWRITE / DELETE</td><td>Record-I/O runtime</td><td>Record read, write, update-in-place, and delete over the RMS and ISAM layer.</td><td><span class="ac-live">live</span></td></tr><tr><td>BACKSPACE / REWIND / ENDFILE / INQUIRE</td><td>Record-I/O runtime</td><td>Positioning, truncation, and INQUIRE-by-unit or by-file.</td><td><span class="ac-live">live</span></td></tr><tr><td>ERRSNS</td><td>Error-state accessor</td><td>Returns the last I/O, system, status, unit, and condition information, then clears it.</td><td><span class="ac-live">live</span></td></tr><tr><td>VMS to Unix filename translation</td><td>Filename translator</td><td>Converts a VMS file specification to a Unix path when a unit is opened.</td><td><span class="ac-live">live</span></td></tr></tbody></table></div><div class="callout"><strong>No daemon to run.</strong> FOR$ is delivered as a static library inside VX/RT. The
application links it directly, so there is no separate runtime service to install or manage
on the Linux host.
</div><h6>Call lifecycle (record access path)</h6><p>
Translated FORTRAN object, or a FOR$ helper, reaches a unit's Record Access Block through the
validated lookup below. The channel table is allocated once at initialisation:
</p><style>.s7-tech-embed[data-vxford-body-embed] .guard-flow {margin: 1.5rem 0;padding: 16px;border: 1px solid var(--s7t-border);border-radius: 10px;background: rgba(255, 255, 255, 0.01);}.s7-tech-embed[data-vxford-body-embed] .guard-endpoint {display: grid;grid-template-columns: 82px minmax(0, 1fr);gap: 14px;align-items: center;padding: 14px 16px;border-radius: 8px;background: var(--s7t-surface);}.s7-tech-embed[data-vxford-body-embed] .guard-success {border: 1px solid var(--s7t-note-bd);background: var(--s7t-note-bg);}.s7-tech-embed[data-vxford-body-embed] .guard-label,.s7-tech-embed[data-vxford-body-embed] .guard-number,.s7-tech-embed[data-vxford-body-embed] .guard-pass,.s7-tech-embed[data-vxford-body-embed] .guard-fail {font-family: var(--s7t-mono);font-size: 0.76em;font-weight: 700;letter-spacing: 0.08em;text-transform: uppercase;}.s7-tech-embed[data-vxford-body-embed] .guard-label,.s7-tech-embed[data-vxford-body-embed] .guard-number,.s7-tech-embed[data-vxford-body-embed] .guard-pass {color: var(--s7t-accent);}.s7-tech-embed[data-vxford-body-embed] .guard-fail {color: #ffd84a;}.s7-tech-embed[data-vxford-body-embed] .guard-checks {display: grid;grid-template-columns: repeat(3, minmax(0, 1fr));gap: 10px;margin: 10px 0;padding: 0;list-style: none;}.s7-tech-embed[data-vxford-body-embed] .guard-check {min-width: 0;padding: 15px;border-top: 3px solid var(--s7t-accent);border-radius: 6px;background: var(--s7t-surface2);}.s7-tech-embed[data-vxford-body-embed] .guard-check .guard-number {display: block;margin-bottom: 10px;}.s7-tech-embed[data-vxford-body-embed] .guard-check strong,.s7-tech-embed[data-vxford-body-embed] .guard-check > span {display: block;}.s7-tech-embed[data-vxford-body-embed] .guard-check strong {margin-bottom: 8px;color: var(--s7t-text);}.s7-tech-embed[data-vxford-body-embed] .guard-outcome {margin-top: 6px;color: var(--s7t-muted);font-size: 0.9em;line-height: 1.45;}.s7-tech-embed[data-vxford-body-embed] .guard-endpoint strong,.s7-tech-embed[data-vxford-body-embed] .guard-endpoint span:last-child {display: block;}.s7-tech-embed[data-vxford-body-embed] .guard-endpoint strong {margin-bottom: 4px;}.s7-tech-embed[data-vxford-body-embed] .guard-endpoint span:last-child {color: var(--s7t-muted);font-size: 0.9em;}@media (max-width: 800px) {.s7-tech-embed[data-vxford-body-embed] .guard-checks,.s7-tech-embed[data-vxford-body-embed] .guard-endpoint {grid-template-columns: 1fr;}.s7-tech-embed[data-vxford-body-embed] .guard-endpoint {gap: 5px;}}</style><div class="guard-flow" role="group" aria-label="Guarded FOR$RAB record access lifecycle"><div class="guard-endpoint"><span class="guard-label">Request</span><div><strong>FORTRAN object calls <code>FOR$RAB(unit)</code></strong><span>The accessor evaluates each guard in order.</span></div></div><ol class="guard-checks"><li class="guard-check"><span class="guard-number">Guard 01</span><strong>Unit in range?</strong><div class="guard-outcome"><span class="guard-fail">No</span> Set channel-out-of-range error state and return <code>NULL</code>.</div><div class="guard-outcome"><span class="guard-pass">Yes</span> Look up <code>channel table[unit]</code>.</div></li><li class="guard-check"><span class="guard-number">Guard 02</span><strong>Unit open?</strong><div class="guard-outcome"><span class="guard-fail">No</span> Set unit-not-open error state and return <code>NULL</code>.</div><div class="guard-outcome"><span class="guard-pass">Yes</span> Continue to the channel's RAB.</div></li><li class="guard-check"><span class="guard-number">Guard 03</span><strong>RAB present?</strong><div class="guard-outcome"><span class="guard-fail">No</span> Set error state and return <code>NULL</code>.</div><div class="guard-outcome"><span class="guard-pass">Yes</span> Complete the validated lookup.</div></li></ol><div class="guard-endpoint guard-success"><span class="guard-label">All pass</span><div><strong>Return <code>RABDEF*</code></strong><span>The pointer identifies the unit's RMS Record Access Block.</span></div></div></div><p>
The FORTRAN I/O statements (OPEN, READ, WRITE, and the rest) sit on the same per-unit channel
table and delegate record storage to Sector7's RMS and ISAM layer, so record organisation and
access order match the original OpenVMS behaviour.
</p><h2 id="routines">Callable FOR$ routines</h2><p>
FOR$ exports four public entry points. Each ships in two layers in the same source module: a
C-callable core, and a FORTRAN-callable wrapper that receives string arguments as a hidden
VMS-style descriptor and then calls the core. The public <code>FOR$</code> name is attached by
an alias mechanism, so existing OpenVMS object references resolve unchanged and the FORTRAN
calling convention (a trailing hidden length argument) is preserved.
</p><div class="table-wrapper"><table><thead><tr><th>FORTRAN name</th><th>Purpose</th><th>Status</th></tr></thead><tbody><tr><td><code>FOR$CVT_D_TE</code></td><td>D_float to edited text; the base numeric converter.</td><td><span class="ac-live">live</span></td></tr><tr><td><code>FOR$CVT_G_TE</code></td><td>G_float to text.</td><td><span class="ac-live">live</span></td></tr><tr><td><code>FOR$CVT_H_TE</code></td><td>H_float (128-bit floating) to text.</td><td><span class="ac-live">live</span></td></tr><tr><td><code>FOR$RAB</code></td><td>Return the RMS Record Access Block for an open FORTRAN unit.</td><td><span class="ac-live">live</span></td></tr></tbody></table></div><h6>Numeric conversion (the CVT family)</h6><p>
The three <code>FOR$CVT</code> routines share a single call shape and a common formatting core.
They format a floating value into an edited character string, apply the OpenVMS field-overflow
convention (a truncated output field is filled with asterisks), and return a condition value:
</p>
<pre>U32 FOR$CVT_x_TE( double *inValue,
<descriptor> outStr,
long digInFrac, <span class="cmt">; digits in the fractional part</span>
long scale, <span class="cmt">; scale factor on the integer part</span>
long digInInt, <span class="cmt">; digits in the integer part</span>
long digInExp, <span class="cmt">; digits in the exponent</span>
long flags ); <span class="cmt">; conversion options</span></pre>
<p>
A successful call returns a normal status. If the edited value does not fit the caller's output
descriptor, the routine overwrites the field with asterisks and returns an output-conversion
error, matching the OpenVMS edit-descriptor behaviour rather than silently corrupting the
buffer. <code>FOR$CVT_G_TE</code> and <code>FOR$CVT_H_TE</code> currently reach text through the
same shared D_float formatting path.
</p><h6>Record access (FOR$RAB)</h6><p><code>FOR$RAB</code> returns a pointer to the RMS Record Access Block for an open FORTRAN unit,
or a null pointer on error with the condition recorded in the run-time error state. It runs a
fixed validation ladder before returning:
</p>
<pre>RABDEF *FOR$RAB( int *for_unit );</pre>
<div class="table-wrapper"><table><thead><tr><th>Check</th><th>On failure</th></tr></thead><tbody><tr><td>Unit number within the valid range</td><td>Record a channel-out-of-range condition; return NULL.</td></tr><tr><td>Unit is open (channel table slot allocated)</td><td>Record an illegal-channel-number condition; return NULL.</td></tr><tr><td>A Record Access Block exists for the unit</td><td>Record a no-RAB condition; return NULL.</td></tr><tr><td>All checks pass</td><td>Return the unit's Record Access Block.</td></tr></tbody></table></div><p>
The maximum FORTRAN unit number is 1024. Because <code>FOR$RAB</code> returns a null pointer and
stashes the condition rather than returning the status directly, calling code checks the error
state through <code>ERRSNS</code> (see below) when the pointer comes back null.
</p><h6>Real call path (banking example)</h6><p>
A migrated program opens a keyed account file and takes the unit's Record Access Block to drive
a keyed lookup, then reads the error state if the accessor returns null:
</p>
<pre><span class="cmt">C open the account master and fetch its Record Access Block</span>
OPEN (UNIT=ACCT_UNIT, FILE=<span class="str">'BANKING$DATA:ACCOUNT_MASTER.DAT'</span>,
+ ORGANIZATION=<span class="str">'INDEXED'</span>, ACCESS=<span class="str">'KEYED'</span>, STATUS=<span class="str">'OLD'</span>)
RAB = FOR$RAB (ACCT_UNIT)
IF (RAB .EQ. 0) THEN
CALL ERRSNS (IOERR, SYSERR, STAT, UNIT, COND)
<span class="cmt">C ... handle the recorded condition ...</span>
END IF
READ (UNIT=ACCT_UNIT, KEY=CUST_ID) ACCT_RECORD</pre>
<h2 id="record-io">Record access and file I/O</h2><p><code>FOR$RAB</code> is the public window onto a larger record-I/O runtime. The FORTRAN OPEN,
READ, WRITE, REWRITE, DELETE, BACKSPACE, REWIND, ENDFILE, and INQUIRE statements are all
implemented over the same per-unit channel table and delegate record storage to Sector7's RMS
and ISAM layer. The table lists the record-I/O surface behind the four public entry points.
</p><div class="table-wrapper"><table><thead><tr><th>FORTRAN statement</th><th>Runtime role</th><th>Status</th></tr></thead><tbody><tr><td>OPEN</td><td>Open a unit, build the FAB and RAB, and translate the VMS filename to a Unix path.</td><td><span class="ac-live">live</span></td></tr><tr><td>CLOSE</td><td>Close a unit with KEEP, DELETE, PRINT, or SUBMIT disposition.</td><td><span class="ac-live">live</span></td></tr><tr><td>READ</td><td>Record read via the RMS and ISAM layer.</td><td><span class="ac-live">live</span></td></tr><tr><td>WRITE</td><td>Record write, including the formatted-write helpers.</td><td><span class="ac-live">live</span></td></tr><tr><td>REWRITE</td><td>Update a record in place.</td><td><span class="ac-live">live</span></td></tr><tr><td>DELETE</td><td>Delete the current or a keyed record.</td><td><span class="ac-live">live</span></td></tr><tr><td>BACKSPACE / REWIND</td><td>Reposition one record back, or to the start of the file.</td><td><span class="ac-live">live</span></td></tr><tr><td>ENDFILE</td><td>Truncate the file and mark end of file.</td><td><span class="ac-live">live</span></td></tr><tr><td>INQUIRE</td><td>INQUIRE by unit or by file.</td><td><span class="ac-live">live</span></td></tr><tr><td>UNLOCK</td><td>Release a record lock held on a unit.</td><td><span class="ac-live">live</span></td></tr></tbody></table></div><h6>The per-unit channel</h6><p>
Each open unit has a channel record in a table allocated once at initialisation, sized to the
maximum unit count plus one. The channel carries everything the runtime needs to service the
unit, including the fields most relevant to record access:
</p><div class="table-wrapper"><table><thead><tr><th>Field</th><th>Meaning</th></tr></thead><tbody><tr><td><code>rab</code></td><td>RMS Record Access Block, the pointer <code>FOR$RAB</code> returns.</td></tr><tr><td><code>fab</code></td><td>RMS File Access Block.</td></tr><tr><td><code>vmsname</code></td><td>Original VMS file name for the unit.</td></tr><tr><td><code>location</code></td><td>Current record number or file position.</td></tr><tr><td><code>org</code></td><td>RMS organisation (sequential, relative, or indexed).</td></tr><tr><td><code>locking</code></td><td>Locking mode: automatic, manual, or exclusive.</td></tr><tr><td><code>rec_len</code> / <code>blksiz</code></td><td>Record length and buffer size.</td></tr><tr><td><code>magic</code></td><td>Integrity check confirming the slot is a live channel.</td></tr></tbody></table></div><div class="callout"><strong>Record buffers stay compatible.</strong> The runtime reads and writes fields at the
byte offsets the application's FORTRAN record declares, so record-oriented programs keep the
same on-disk layout and access semantics they had under OpenVMS.
</div><h2 id="errors">Error handling and status codes</h2><p>
FOR$ preserves OpenVMS FORTRAN error semantics. The conversion routines return standard status
codes, the record-access routines validate the unit and open state before returning, and
<code>ERRSNS</code> exposes the last error condition to the application exactly as it did under
OpenVMS. Error state is held per thread, so a multi-threaded Linux process keeps each thread's
last-error information separate.
</p><h6>The ERRSNS path</h6><p><code>ERRSNS</code> returns the last I/O, system, status, unit, and condition values, then clears
the saved condition so the next call reflects only new errors. The I/O status is the RMS status
mapped to a FORTRAN IOSTAT value; the system and status values come from the unit's Record
Access Block:
</p>
<pre><span class="cmt">/* FORTRAN: ERRSNS([IO_ERR][,SYS_ERR][,STAT][,UNIT][,COND]) */</span>
void Vxf_Errsns(U32 *io_err, U32 *sys_err, U32 *stat, U32 *unit, U32 *cond)
{
if (io_err) *io_err = Vxf_rms_equiv(Vxf_VMS_STATUS); <span class="cmt">/* RMS to IOSTAT */</span>
if (sys_err) *sys_err = Vxf_RmsStatus(Vxf_VMS_UNIT, Value); <span class="cmt">/* RAB/FAB STV */</span>
if (stat) *stat = Vxf_RmsStatus(Vxf_VMS_UNIT, Status);<span class="cmt">/* RAB STS */</span>
if (unit) *unit = Vxf_VMS_UNIT;
if (cond) *cond = Vxf_VMS_STATUS;
Vxf_VMS_STATUS = 0; <span class="cmt">/* reset saved information after each ERRSNS call */</span>
}</pre>
<h6>Run-time error codes</h6><p>The FORTRAN run-time layer reports record-access failures with a small, stable set of codes:</p><div class="table-wrapper"><table><thead><tr><th>Condition</th><th>Value</th><th>Meaning</th></tr></thead><tbody><tr><td>no error</td><td>0</td><td>No error.</td></tr><tr><td>channel out of range</td><td>1002</td><td>Unit number outside the valid range.</td></tr><tr><td>file not open</td><td>1004</td><td>Unit is not open.</td></tr><tr><td>no RAB</td><td>1006</td><td>No Record Access Block for the unit.</td></tr><tr><td>no FAB</td><td>1008</td><td>No File Access Block for the unit.</td></tr><tr><td>illegal channel number</td><td>1010</td><td>Channel table slot is not a live channel.</td></tr><tr><td>invalid record</td><td>1012</td><td>Record is not valid for the operation.</td></tr><tr><td>record overflow</td><td>1014</td><td>Record exceeds the buffer.</td></tr><tr><td>illegal filename</td><td>1016</td><td>File specification could not be translated.</td></tr></tbody></table></div><div class="callout"><strong>IOSTAT is preserved.</strong> FOR$ ships the full DEC and VSI FORTRAN IOSTAT table so
that <code>READ</code> and <code>WRITE</code> error handling driven by IOSTAT values, including
end-of-record and end-of-file, behaves the same on Linux as it did under OpenVMS.
</div><h2 id="compatibility">Compatibility and divergences</h2><p>
FOR$ is built to let OpenVMS FORTRAN applications run on Linux without source changes. A few
behaviours differ from the original VMS runtime by design, because the target platform differs;
each divergence keeps application-visible behaviour intact while adapting to Linux.
</p><div class="table-wrapper"><table><thead><tr><th>Divergence</th><th>Reason</th></tr></thead><tbody><tr><td>Error state is held per thread rather than per image.</td><td>The Linux runtime can be multi-threaded, so last-error information is kept separate for each thread.</td></tr><tr><td><code>FOR$RAB</code> returns a null pointer and records the condition separately, rather than returning the status directly.</td><td>The Linux port returns the pointer type; callers consult the error state through <code>ERRSNS</code> when the pointer is null.</td></tr><tr><td>H_float conversion is carried on the shared double-precision path.</td><td>Linux on x86 has no native 128-bit H_float type, so H_float folds onto the shared formatter.</td></tr><tr><td>Filenames pass through VMS-to-Unix translation at OPEN time.</td><td>VMS file specifications are mapped to Unix paths, which RMS handled natively on OpenVMS.</td></tr><tr><td>Open files are closed by a registered exit handler.</td><td>Reproduces OpenVMS image-rundown file cleanup on Linux through a process exit handler.</td></tr></tbody></table></div><div class="callout"><strong>Ships inside VX/RT.</strong> FOR$ is delivered as part of the VX/RT run-time, so a
FORTRAN application picks up FOR$ conversion and record-access services alongside the rest of
the OpenVMS run-time surface on x86 and ARM Linux.
</div><h2 id="quickref">Quick reference</h2><h6>Public FOR$ entry points</h6><div class="kw-grid"><div>FOR$CVT_D_TE</div><div>FOR$CVT_G_TE</div><div>FOR$CVT_H_TE</div><div>FOR$RAB</div></div><h6>Record-I/O statements serviced</h6><div class="kw-grid"><div>OPEN</div><div>CLOSE</div><div>READ</div><div>WRITE</div><div>REWRITE</div><div>DELETE</div><div>BACKSPACE</div><div>REWIND</div><div>ENDFILE</div><div>INQUIRE</div><div>UNLOCK</div><div>ERRSNS</div></div><h6>RMS file organizations</h6><div class="kw-grid"><div>sequential</div><div>relative</div><div>indexed</div></div><h6>Call shapes</h6><div class="table-wrapper"><table><thead><tr><th>Routine</th><th>Contract</th></tr></thead><tbody><tr><td><code>FOR$CVT_x_TE</code></td><td>Format a floating value to edited text; return normal status, or an output-conversion error with the field filled with asterisks on overflow.</td></tr><tr><td><code>FOR$RAB</code></td><td>Return the unit's Record Access Block, or NULL with the condition recorded for <code>ERRSNS</code>.</td></tr></tbody></table></div><h6>Key facts</h6><div class="table-wrapper"><table><thead><tr><th>Item</th><th>Value</th></tr></thead><tbody><tr><td>Maximum FORTRAN unit number</td><td>1024</td></tr><tr><td>Delivered in</td><td>VX/RT run-time, archived into <code>libvxrt.a</code></td></tr><tr><td>Target platforms</td><td>x86 and ARM Linux</td></tr><tr><td>Error state scope</td><td>Per thread</td></tr></tbody></table></div></div>