Chapter 2. The Basics of Stream Files

Table of Contents
2.1. Opening Files with the open() API
2.2. Closing a file with the close() API
2.3. Writing streams with the write() API
2.4. Reading a stream file with the read() API
2.5. Example of writing and reading data to a stream file
2.6. Error handling
2.7. Our last example with error handling added
2.8. Example of writing raw data to a stream file

2.1. Opening Files with the open() API

2.1.1. Creating a header member

More than half of the code in our "Hello World" program was prototypes and other definitions. If we were C programmers, we wouldn't have to worry about this part of the coding, because IBM ships "header members" with OS/400 and ILE C/400 that C programmers can simply "include" at the top of their programs.

Since IBM doesn't do that for us as RPG programmers, what we'll do is create our own header member. We'll call it "IFSIO_H" which stands for "Integrated File System I/O Header Member," in that member, we'll put our prototypes, constants, etc. After we've done that, we'll no longer need to type those prototypes into every program. One /copy and we're done!

The first prototype that we'll add will be for the open() API.

2.1.2. Prototyping the open() API

The UNIX-type APIs manual shows us a prototype (written in C) for the open() API. It looks like this:

     int(1) open(2)(const char *path(3), int oflag(4), ...(5));
  
(1)
The "int" here signifies what type of value the procedure returns. The "int" data type in C is identical to a "10I 0" variable in RPG IV.
(2)
The word "open" here signifies the name of the sub-procedure that's being called. Like all things in C, procedure names are case-sensitive. This is important to us because, in RPG they are not case-sensitive! In fact, if we don't do anything special, the RPG compiler will convert the procedure name to all uppercase before binding. Therefore, when we make our RPG prototype, we'll use the EXTPROC() keyword to refer to open as an all-lowercase procedure name.
(3)
This is where we specify the name of a path that we want to access. The "char" means character, and the "*" means pointer. So, what this procedure needs from us is a pointer that points to a character variable. You see, in C, character strings are implemented by specifying a starting point in memory, and then reading forward in memory until a x'00' (called a "null") is encountered.

Fortunately, RPG's options(*string) keyword, and the %str() built-in-function, make it easy for us to convert to and from C's string format.

(4)
This is an integer ("int") which defines the flags that are used by the open() API. As I mentioned, the C "int" data type is equivalent to the "10I 0" data type in RPG.
(5)
These three periods signify that any number of optional parameters may follow. This is difficult for us, since RPG prototypes need to know in advance what the data types of the parameters are.

Fortunately, if you read IBM's documentation, you find out that there are only two optional parameters that follow, and they are both unsigned integers. One is for specifying the mode, which is used when O_CREAT is passed as a flag, the other is for specifying a code page, which we will talk more about in chapter 5.

Now that we know how the C prototype works, we can write our own RPG prototype. Here's what I came up with:

    D open            PR            10I 0 extproc('open')                   
    D   path                          *   value options(*string)            
    D   oflag                       10I 0 value                             
    D   mode                        10U 0 value options(*nopass)            
    D   codepage                    10U 0 value options(*nopass)        
   

Please add that prototype to your IFSIO_H member now, unless of course, you've decided to just install mine, in which case you already have it.

The first thing that you might notice is that all of the parameters are passed by value. That's because C is expecting to receive a pointer, an integer, an unsigned integer and another unsigned integer. If we passed these parameters by reference, it would actually receive 4 memory addresses, rather than receiving the actual data.

As a general rule, when a C program expects to receive something by value, it will simply list the data type followed by the variable. If it wants to receive the address of the variable, it will ask for a pointer to that variable by putting a "*" in front of it. For example "int oflag" is a request for an integer, passed by value, whereas "int *oflag" would be the same integer passed by reference.

Wait a minute! Wouldn't that mean that the "char *path" should be passed by reference, instead of by value?! Yes, that's true. In fact, we could've coded path as:

    D   path                      1024A               
   

However, if we did that, we'd have to assign a length to "path", and the C version allows path to be of any length. The trick is, passing a pointer by value is the same thing as passing a variable by reference. In either case, what actually gets passed from procedure to procedure is an address in memory. But, if we use a pointer by value, and we use "options(*string)", the RPG compiler will automatically allow any length string, and will automatically convert it to C's format by adding the terminating "null" character at the end. This saves us some work.

Finally, you'll notice that the mode and codepage parameters are set up with "options(*nopass)". This means that they're optional, and we only need to pass them when we need them.

2.1.3. The path parameter

path is pretty self-explanatory. It is the path to the file in the IFS that we want to open. In the IFS, the "/" character is used to separate the different components of the path. The first component may be a "/" to signify the root directory. Thereafter, each section of the path refers to a directory name until we reach the last component, which specifies the filename.

For example, consider the following path name:

       /ifsebook/chapter2/examples/myfile.txt
   

The leading "/" means to start at the root of the IFS. If it was not specified, the path name would actually start at whatever our current working directory was, and continue from there. But since it has a "/" we're telling it that the path to the file actually starts at the root of the system.

The word "ifsebook" refers to a directory. The word "chapter2" refers to a sub-directory that's inside the "ifsebook" directory. The word "examples" refers to another sub-directory, this one is inside the "chapter2" directory, and finally, "myfile.txt" refers to the object that's in the "examples" directory.

Let's try another, somewhat more familiar, example:

       /QSYS.LIB/qgpl.lib/qrpglesrc.file/proof.mbr
   

This tells us to go back to the "/" root directory, then look at the QSYS.LIB directory (which is also known as the QSYS library) and that within that directory is a directory called the qgpl.lib directory (which is also known as the QGPL library) and within that, there's a file called QRPGLESRC which contains a member called "PROOF".

2.1.4. The oflag parameter

oflag is where we specify the options we want to use when opening the file. What we're actually passing here is a string of 32 bits, each of which specifies a different option. The rightmost bit specifies "Read only", then moving one bit to the left, that bit specifies "Write only", and the next bit specifies "reading and writing" and the next bit specifies "create the file if it doesn't exist," etc.

In order to make our lives easier, rather than actually specifying the bits manually, we define a series of flags, that when added together, will turn on the bits that we desire.

For example, we use the number 8 to signify "create if the file doesn't exist" and the number 2 to signify "write only". This makes more sense if you convert those numbers to binary. The decimal number 8 is 1000 in binary. The decimal number 2 is 10 in binary. So you see, when we specify the number 8, we actually specify that we want the 4th bit (counting from the right) to be turned on. When we specify 2, we are specifying that the 2nd bit be turned on. If we add those two numbers together, 8+2=10. If you convert the decimal 10 to binary you get 1010 (bits 4 and 2 are both on). Because each of these numbers turns on a single bit, we refer to them as "flags", and they supply us with a convenient way to which options we want to pass to the open() API.

So that we don't need to mess with the bit values later in this book, let's add those flags to our IFSIO_H member now. This is what we need to add:

     D**********************************************************************
     D*  Flags for use in open()
     D*
     D* More than one can be used -- add them together.
     D**********************************************************************
     D*                                            Reading Only
     D O_RDONLY        C                   1
     D*                                            Writing Only
     D O_WRONLY        C                   2
     D*                                            Reading & Writing
     D O_RDWR          C                   4
     D*                                            Create File if not exist
     D O_CREAT         C                   8
     D*                                            Exclusively create
     D O_EXCL          C                   16
     D*                                            Truncate File to 0 bytes
     D O_TRUNC         C                   64
     D*                                            Append to File
     D O_APPEND        C                   256
     D*                                            Convert text by code-page
     D O_CODEPAGE      C                   8388608
     D*                                            Open in text-mode
     D O_TEXTDATA      C                   16777216
   

2.1.5. The mode parameter

mode is used to specify the access rights that this file will give to users who want to work with it. Like the "oflag" parameter, this parameter is treated as a series of bits. The rightmost 9 bits are the ones that we're concerned with, and they're laid out like this:

      user:       owner    group    other                        
      access:     R W X    R W X    R W X                        
      bit:        9 8 7    6 5 4    3 2 1                        
   

These bits specify Read, Write and Execute access to 3 types of users. The first is the file's owner, the second is users with the same group profile as the file's owner, and the third is all other users.

For example, if I wanted to specify that the owner of the file can read and write to the file, that people in his group can only read the file, and that everyone else has no access at all, I'd specify the following bits: 110 100 000. If you look at those bits as the binary number 110100000, and convert it to decimal, you'd get 416. So, to assign those permissions to the file, you'd call open() with a 3rd parameter of 416.

Just as we did for the "oflags" parameter, we'll also specify bit-flags for the mode, which we can add together to make our programs easier to read. Please add these to your IFSIO_H member:

     D**********************************************************************
     D*      Mode Flags.
     D*         basically, the mode parm of open(), creat(), chmod(),etc
     D*         uses 9 least significant bits to determine the
     D*         file's mode. (peoples access rights to the file)
     D*
     D*           user:       owner    group    other
     D*           access:     R W X    R W X    R W X
     D*           bit:        8 7 6    5 4 3    2 1 0
     D*
     D* (This is accomplished by adding the flags below to get the mode)
     D**********************************************************************
     D*                                         owner authority
     D S_IRUSR         C                   256
     D S_IWUSR         C                   128
     D S_IXUSR         C                   64
     D S_IRWXU         C                   448
     D*                                         group authority
     D S_IRGRP         C                   32
     D S_IWGRP         C                   16
     D S_IXGRP         C                   8
     D S_IRWXG         C                   56
     D*                                         other people
     D S_IROTH         C                   4
     D S_IWOTH         C                   2
     D S_IXOTH         C                   1
     D S_IRWXO         C                   7
   

Now, instead of specifying "416", we can simply add together S_IRUSR+S_IWUSR+S_IRGRP, which specifies "read access for user", "write access for user" and "read access for group", respectively.

2.1.6. The codepage parameter

If you specify the O_CODEPAGE flag in the oflag parameter, you must use this parameter to specify which code page the file will be assigned.

We will talk about that more in Chapter 5, in our discussion of text files.

2.1.7. The return value of the open() API

The return value of the open() API is a "file descriptor". It is an integer that we will pass to all of the other IFS APIs that we call so that they know which file we are referring to. If something goes wrong, and the system is not able to open the file that we requested, it will return a value of -1 instead of a file descriptor. So, whenever we call open() we will check for this, and treat -1 as an error.

2.1.8. Code snippet showing the use of the open() API

Here's a code snippet that uses the open() API:

     c                   eval      path = '/QIBM/UserData/OS400/DirSrv' +
     c                                 '/slapd.conf'
     c                   eval      flags = O_WRONLY + O_CREAT + O_TRUNC

     c                   eval      mode =  S_IRUSR + S_IWUSR
     c                                   + S_IRGRP

     c                   eval      fd = open(%trimr(path): flags: mode)
     c                   if        fd < 0
     c                   goto      bomb_out
     c                   endif