IFS Stream File I/O from RPG on IBM i: Qp0lOpen, Qp0lRead, Qp0lWrite, Open Flags, CSV File Generation, and Config File Parsing in 2026

The previous post covered CL data areas and data queues on IBM i — creating and managing data areas with CRTDTAARA, CHGDTAARA, and RTVDTAARA, the local data area (LDA), using the DTAARA keyword in RPG, data area locking with ALCOBJ/DLCOBJ, creating FIFO/LIFO/keyed data queues with CRTDTAQ, and SNDDTAQ/RCVDTAQ inter-job communication patterns. This post covers IFS stream file I/O from ILE RPG on IBM i: the difference between stream files and database files, using Qp0lOpen, Qp0lRead, Qp0lWrite, and Qp0lClose UNIX-type APIs, open flag constants, CCSID encoding considerations, directory traversal with Qp0lOpen/Qp0lReaddir, exporting data as CSV to the IFS, and reading INI-style configuration files from RPG programs on IBM i in 2026.

IFS Stream Files vs. Database Physical Files

The Integrated File System (IFS) on IBM i provides a UNIX-style hierarchical file system alongside the traditional library-based object system. An IFS stream file is a flat sequence of bytes with no fixed record structure — the RPG program determines how to parse the data. This contrasts with a DB2 physical file, which has a fixed record length enforced by the OS.

AttributeDB2 Physical FileIFS Stream File
StructureFixed-length recordsFlat byte stream, no record concept
PathLIBRARY/FILE/path/to/file.csv
Access from RPGF-spec with DISK deviceUNIX-type APIs: Qp0lOpen, Qp0lRead, Qp0lWrite
Character encodingCCSID on the file objectCCSID on stream file; O_CCSID flag at open time
Best forRecord-oriented business dataCSV/JSON export, config files, web content, binary blobs

IFS stream files are the natural exchange format for CSV exports destined for Excel, JSON files for REST APIs, XML documents, configuration files read at runtime, and binary files such as PDFs or images generated by RPG programs.

UNIX-Type APIs for IFS I/O: Qp0lOpen, Qp0lRead, Qp0lWrite, Qp0lClose

IBM i provides the complete POSIX file API set via the Qp0l family of APIs. These are regular ILE service programs callable from RPG with ExtProc prototypes. The API names start with Qp0l (POSIX-compatible) rather than the bare POSIX names (open, read, write) to distinguish them as IBM i implementations.

**FREE
// ── Prototype declarations for IFS UNIX-type APIs ────────────────────

// Qp0lOpen: open a stream file, returns a file descriptor (integer)
// Returns -1 on error; errno contains the error code
Dcl-PR  Qp0lOpen      ExtProc('Qp0lOpen') Int(10);
  Path      Pointer   Const;     // NUL-terminated path string
  OFlag     Int(10)   Const;     // open flags (O_RDONLY, O_WRONLY, O_CREAT, etc.)
  Mode      Uns(10)   Const;     // permission mode (0o666 = rw-rw-rw-)
  CodePage  Uns(10)   Const Options(*NoPass);  // CCSID for text files
End-PR;

// Qp0lRead: read up to BufLen bytes from an open file descriptor
// Returns bytes actually read; 0 = EOF; -1 = error
Dcl-PR  Qp0lRead      ExtProc('Qp0lRead') Int(20);
  FD        Int(10)   Const;
  Buf       Char(65535) Options(*VarSize);
  BufLen    Uns(20)   Const;
End-PR;

// Qp0lWrite: write BufLen bytes to an open file descriptor
// Returns bytes actually written; -1 = error
Dcl-PR  Qp0lWrite     ExtProc('Qp0lWrite') Int(20);
  FD        Int(10)   Const;
  Buf       Char(65535) Const Options(*VarSize);
  BufLen    Uns(20)   Const;
End-PR;

// Qp0lClose: close a file descriptor
// Returns 0 on success; -1 = error
Dcl-PR  Qp0lClose     ExtProc('Qp0lClose') Int(10);
  FD        Int(10)   Const;
End-PR;

// errno: retrieve the last system error code
Dcl-PR  GetErrNo      ExtProc('__errno') Pointer End-PR;

// Open flag constants (O_ values for IBM i IFS)
Dcl-C  O_RDONLY    1;        // Read-only
Dcl-C  O_WRONLY    2;        // Write-only
Dcl-C  O_RDWR      4;        // Read-write
Dcl-C  O_CREAT     8;        // Create if not exists
Dcl-C  O_TRUNC     64;       // Truncate to zero length on open
Dcl-C  O_APPEND    256;      // Append: all writes go to end of file
Dcl-C  O_CCSID     4194304;  // Use CCSID specified in CodePage parameter

Writing a CSV File to the IFS from RPG

This is the most common IFS write pattern: an RPG batch program fetches data from DB2 for i and writes it as a comma-separated values file in the IFS, where downstream systems (SFTP jobs, web services, Excel) can consume it.

**FREE
Ctl-Opt DftActGrp(*No) ActGrp('CSVGRP') Option(*SrcStmt);

// Include the IFS API prototypes defined above
/Copy QSYSINC/QRPGLESRC,IFS_PROTO   // Or inline the DCL-PRs as shown above

Dcl-S  Fd          Int(10);
Dcl-S  PathStr     Varchar(512);
Dcl-S  PathPtr     Pointer;
Dcl-S  CsvLine     Varchar(2000);
Dcl-S  NewLine     Char(2)    Inz(X'0D25');  // CRLF in EBCDIC
Dcl-S  BytesWrt    Int(20);
Dcl-S  OFlags      Int(10);

// Build the file path as a NUL-terminated string
PathStr = '/home/batch/exports/custlist_' +
          %Char(%Date() : *ISO0) + '.csv' + X'00';  // NUL-terminated
PathPtr = %Addr(PathStr) + 2;   // Skip the 2-byte varchar length prefix

// Open flags: write-only, create if not exists, truncate existing content
OFlags  = O_WRONLY + O_CREAT + O_TRUNC;

// Open the file with CCSID 1208 (UTF-8) — correct for modern CSV consumers
Fd = Qp0lOpen(PathPtr : OFlags : 438 : 1208);   // 438 = 0o666 permissions
If Fd = -1;
  // Check errno for the specific error
  Dcl-S  ErrnoPtr  Pointer;
  Dcl-S  ErrNo     Int(10)  Based(ErrnoPtr);
  ErrnoPtr = GetErrNo();
  Dsply ('IFS open failed, errno=' + %Char(ErrNo));
  *InLR = *On;
  Return;
EndIf;

// Write CSV header row
CsvLine = 'CustNo,Name,City,Province,Balance,LastOrderDate' + NewLine;
BytesWrt = Qp0lWrite(Fd : CsvLine : %Len(%TrimR(CsvLine)));

// Read customer data from DB2 and write each row
Dcl-S  CustNo     Char(7);
Dcl-S  CustName   Varchar(50);
Dcl-S  City       Varchar(30);
Dcl-S  Province   Char(2);
Dcl-S  Balance    Packed(11:2);
Dcl-S  LastOrdDt  Date(*ISO);
Dcl-S  SqlCode    Int(10);

Exec SQL
  DECLARE C_CUST CURSOR FOR
    SELECT CUST_NO, CUST_NAME, CITY, PROVINCE,
           BALANCE, LAST_ORDER_DATE
    FROM SALESLIB/CUSTMST
    WHERE ACTIVE_FLAG = 'Y'
    ORDER BY CUST_NO;

Exec SQL OPEN C_CUST;

DoU SqlCode = 100;
  Exec SQL
    FETCH NEXT FROM C_CUST
    INTO :CustNo, :CustName, :City, :Province, :Balance, :LastOrdDt;
  SqlCode = SQLCODE;

  If SqlCode = 0;
    // Build CSV line — quote fields containing commas
    CsvLine = %TrimR(CustNo) + ',' +
              '"' + %TrimR(CustName) + '",' +
              '"' + %TrimR(City) + '",' +
              %TrimR(Province) + ',' +
              %Char(Balance) + ',' +
              %Char(LastOrdDt : *ISO) + NewLine;

    BytesWrt = Qp0lWrite(Fd : CsvLine : %Len(%TrimR(CsvLine)));
  EndIf;
EndDo;

Exec SQL CLOSE C_CUST;
Qp0lClose(Fd);

*InLR = *On;

Reading a Configuration File from the IFS

A common modernization pattern is externalising configuration — server addresses, API keys, processing thresholds — from hardcoded RPG constants into an INI-style text file in the IFS. The RPG program reads the file at startup and parses key=value pairs.

**FREE
// Read an INI-style config file from the IFS
// Config file format:
//   SERVER_ADDR=api.ordersys.internal
//   PORT=8443
//   TIMEOUT=30
//   DEBUG=N

Dcl-S  Fd          Int(10);
Dcl-S  PathStr     Varchar(256);
Dcl-S  PathPtr     Pointer;
Dcl-S  ReadBuf     Char(8192);
Dcl-S  BytesRead   Int(20);
Dcl-S  FullContent Varchar(8192);
Dcl-S  LineStart   Int(10);
Dcl-S  LineEnd     Int(10);
Dcl-S  OneLine     Varchar(256);
Dcl-S  KeyPart     Varchar(50);
Dcl-S  ValPart     Varchar(200);
Dcl-S  EqPos       Int(10);

// Config values to populate
Dcl-S  ServerAddr  Varchar(100);
Dcl-S  PortNum     Int(10)    Inz(8080);
Dcl-S  TimeoutSec  Int(10)    Inz(30);
Dcl-S  DebugFlag   Char(1)    Inz('N');

PathStr = '/etc/appconfig/ordersys.ini' + X'00';
PathPtr = %Addr(PathStr) + 2;

Fd = Qp0lOpen(PathPtr : O_RDONLY : 0 : 819);  // CCSID 819 = ISO 8859-1
If Fd = -1;
  // Config not found — use defaults already set via Inz
  Return;
EndIf;

BytesRead = Qp0lRead(Fd : ReadBuf : %Size(ReadBuf));
Qp0lClose(Fd);

If BytesRead  %Len(FullContent);
  // Find next newline character (LF = X'15' in EBCDIC)
  LineEnd = %Scan(X'15' : FullContent : LineStart);
  If LineEnd = 0;
    LineEnd = %Len(FullContent) + 1;
  EndIf;

  OneLine = %SubSt(FullContent : LineStart : LineEnd - LineStart);
  OneLine = %TrimR(OneLine);

  // Skip blank lines and comment lines starting with #
  If %Len(OneLine) > 0 And %SubSt(OneLine : 1 : 1)  '#';
    EqPos   = %Scan('=' : OneLine);
    If EqPos > 0;
      KeyPart = %TrimR(%SubSt(OneLine : 1         : EqPos - 1));
      ValPart = %TrimL(%SubSt(OneLine : EqPos + 1));

      Select;
        When KeyPart = 'SERVER_ADDR';
          ServerAddr  = ValPart;
        When KeyPart = 'PORT';
          PortNum     = %Int(ValPart);
        When KeyPart = 'TIMEOUT';
          TimeoutSec  = %Int(ValPart);
        When KeyPart = 'DEBUG';
          DebugFlag   = %SubSt(ValPart : 1 : 1);
      EndSl;
    EndIf;
  EndIf;

  LineStart = LineEnd + 1;
EndDo;

Directory Traversal with Qp0lOpendir and Qp0lReaddir

To process all files matching a pattern in an IFS directory — for example, all .csv drop files in an inbound folder — use the POSIX directory APIs Qp0lOpendir, Qp0lReaddir, and Qp0lClosedir.

**FREE
Dcl-PR  Qp0lOpendir   ExtProc('Qp0lOpendir') Pointer;
  DirPath   Pointer   Const;
End-PR;

// Directory entry structure returned by Qp0lReaddir
Dcl-DS  DirEntDS  Qualified;
  Ino     Uns(10);             // Inode number
  Reclen  Uns(5);              // Record length of this entry
  NamLen  Uns(5);              // Length of d_name
  DType   Char(1);             // File type: '8'=regular, '4'=directory
  Name    Char(640);           // NUL-terminated file name
End-DS;

Dcl-PR  Qp0lReaddir   ExtProc('Qp0lReaddir') Pointer;
  DirHandle  Pointer  Const;
End-PR;

Dcl-PR  Qp0lClosedir  ExtProc('Qp0lClosedir') Int(10);
  DirHandle  Pointer  Const;
End-PR;

Dcl-S  DirPath    Varchar(256);
Dcl-S  DirPtr     Pointer;
Dcl-S  EntPtr     Pointer;
Dcl-S  EntryDS    LikeDS(DirEntDS) Based(EntPtr);
Dcl-S  FileName   Varchar(640);
Dcl-S  NulPos     Int(10);
Dcl-S  FullPath   Varchar(900);

DirPath = '/home/batch/inbound/' + X'00';
DirPtr  = Qp0lOpendir(%Addr(DirPath) + 2);

If DirPtr = *Null;
  // Could not open directory — check errno
  Return;
EndIf;

DoU EntPtr = *Null;
  EntPtr = Qp0lReaddir(DirPtr);
  If EntPtr = *Null;
    Leave;    // No more entries
  EndIf;

  // Extract NUL-terminated file name
  NulPos   = %Scan(X'00' : EntryDS.Name);
  If NulPos > 1;
    FileName = %SubSt(EntryDS.Name : 1 : NulPos - 1);
  EndIf;

  // Skip . and .. entries; process only .csv files
  If FileName  '.' And FileName  '..'
     And %SubSt(FileName : %Len(FileName) - 3) = '.csv';
    FullPath = '/home/batch/inbound/' + FileName;
    // Call processing program with this file path
    // CallP ProcessCsvFile(%TrimR(FullPath));
  EndIf;
EndDo;

Qp0lClosedir(DirPtr);

Error Handling: errno and POSIX Error Codes

All UNIX-type IFS APIs return -1 on failure and set the global errno variable to a numeric error code. The most important errno values for IFS work on IBM i:

errno ValueConstant NameMeaning
3401EACCESPermission denied — check IFS object authority (CHGAUT)
3403ENOENTNo such file or directory — path does not exist
3404ENOTDIRPath component is not a directory
3405EEXISTFile already exists (when O_EXCL is set)
3406EMFILEToo many open file descriptors in this job
3474EBADNAMEPath contains invalid characters or is too long
**FREE
// Robust open with error reporting
Dcl-Pr  Qp0lStrerror  ExtProc('Qp0lStrerror') Pointer;
  ErrNum  Int(10)  Const;
End-PR;

Dcl-S  FD         Int(10);
Dcl-S  ErrnoPtr   Pointer;
Dcl-S  ErrNo      Int(10)  Based(ErrnoPtr);
Dcl-S  MsgPtr     Pointer;
Dcl-S  MsgText    Varchar(256) Based(MsgPtr);

FD = Qp0lOpen(PathPtr : O_WRONLY + O_CREAT + O_TRUNC : 438 : 1208);
If FD = -1;
  ErrnoPtr = GetErrNo();
  MsgPtr   = Qp0lStrerror(ErrNo);
  SndPgmMsg('IFS open error ' + %Char(ErrNo) + ': ' + MsgText);
  *InLR = *On;
  Return;
EndIf;

Security Considerations for IFS Stream File I/O

IFS stream files have their own authority model separate from the library-based *PUBLIC/*AUTL system. A few key practices:

  • Do not write to /tmp in production/tmp is world-writable and files there are deleted on IPL. Use application-specific directories such as /home/batch/exports/ with controlled ownership.
  • Set restrictive creation permissions — use 0o640 (owner read-write, group read-only, others none) as the mode parameter to Qp0lOpen rather than 0o666 for files containing sensitive data.
  • Always close file descriptors — each IBM i job has a finite number of open file descriptors (default 50 per process in PASE, higher in ILE). A program that exits without closing FDs leaks them for the job’s lifetime.
  • Validate path inputs — if the path string comes from a user, a data area, or a database field, validate it to prevent path traversal attacks (../../etc/ patterns). Check for .. components before calling Qp0lOpen.
  • Use CCSID 1208 (UTF-8) for modern file exchange — downstream systems expecting UTF-8 will misread EBCDIC stream files. Specify the CCSID at open time with the O_CCSID flag and the CodePage parameter to Qp0lOpen.

Next post: building a REST API server on IBM i with Node.js and Express in PASE — connecting to DB2 for i with the odbc package, defining CRUD route handlers, returning JSON responses, adding API-key authentication middleware, handling errors, and running the Express server as a persistent IBM i batch job submitted via SBMJOB.

Leave a Comment

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

Scroll to Top