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.
| Attribute | DB2 Physical File | IFS Stream File |
|---|---|---|
| Structure | Fixed-length records | Flat byte stream, no record concept |
| Path | LIBRARY/FILE | /path/to/file.csv |
| Access from RPG | F-spec with DISK device | UNIX-type APIs: Qp0lOpen, Qp0lRead, Qp0lWrite |
| Character encoding | CCSID on the file object | CCSID on stream file; O_CCSID flag at open time |
| Best for | Record-oriented business data | CSV/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 Value | Constant Name | Meaning |
|---|---|---|
| 3401 | EACCES | Permission denied — check IFS object authority (CHGAUT) |
| 3403 | ENOENT | No such file or directory — path does not exist |
| 3404 | ENOTDIR | Path component is not a directory |
| 3405 | EEXIST | File already exists (when O_EXCL is set) |
| 3406 | EMFILE | Too many open file descriptors in this job |
| 3474 | EBADNAME | Path 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
/tmpin production —/tmpis 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 than0o666for 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.