The code

Updated: April 19, 2023

Here's the code that addresses all the above points. We'll go through it step by step in the discussion that follows.

/*
 * io_read1.c
*/

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/neutrino.h>
#include <sys/iofunc.h>

// our data string
char    *data_string = "Hello, world!\n";

int
io_read (resmgr_context_t *ctp, io_read_t *msg, iofunc_ocb_t *ocb)
{
    int     sts;
    int     nbytes;
    int     nleft;
    int     off;
    int     xtype;
    struct _xtype_offset *xoffset;

    // 1) verify that the device is opened for read
    if ((sts = iofunc_read_verify (ctp, msg, ocb, NULL)) != EOK) {
        return (sts);
    }

    // 2) check for and handle an XTYPE override
    xtype = msg -> i.xtype & _IO_XTYPE_MASK;
    if (xtype == _IO_XTYPE_OFFSET) {
        xoffset = (struct _xtype_offset *) (&msg -> i + 1);
        off = xoffset -> offset;
    } else if (xtype == _IO_XTYPE_NONE) {
        off = ocb -> offset;
    } else {   // unknown, fail it
        return (ENOSYS);
    }

    // 3) how many bytes are left?
    nleft = ocb -> attr -> nbytes - off;

    // 4) how many bytes can we return to the client?
    nbytes = min (nleft, msg -> i.nbytes);

    // 5) if returning data, write it to client
    if (nbytes) {
        /* the first nbytes value, the status parameter, 
        tells the client how many bytes we're giving them */
        MsgReply (ctp -> rcvid, nbytes, data_string + off, nbytes);

        // 6) set up POSIX stat() "atime" data
        ocb -> attr -> flags |= IOFUNC_ATTR_ATIME |
                                IOFUNC_ATTR_DIRTY_TIME;

        // 7) advance the lseek() index by the number of bytes
        // read if not _IO_XTYPE_OFFSET
        if (xtype == _IO_XTYPE_NONE) {
            ocb -> offset += nbytes;
        }
    } else {
        /* 8) no more data to return, tell the client we have 0 
        bytes left through the status parameter */
        MsgReply (ctp -> rcvid, 0, NULL, 0);
    }

    // 9) indicate we already did the MsgReply to the library
    return (_RESMGR_NOREPLY);
}
Step 1
Here we ensured that the client's open() call had in fact specified that the device was to be opened for reading. If the client opened the device for writing only, and then attempted to perform a read from it, it would be considered an error. In that case, the helper function iofunc_read_verify() would return EBADF, and not EOK, so we'd return that value to the library, which would then pass it along to the client.
Step 2
Here we checked to see if the client had specified an xtype-override—a per-message override (e.g., because while the device had been opened in non-blocking mode, this specifies for this one request that we'd like blocking behavior). Note that the blocking aspect of the “xtype” override can be noted by the iofunc_read_verify() function's last parameter—since we're illustrating a very simple example, we just passed in a NULL indicating that we don't care about this aspect.

More important, however, is to see how particular “xtype” modifiers are handled. An interesting one is the _IO_XTYPE_OFFSET modifier, which, if present, indicates that the message passed from the client contains an offset and that the read operation should not modify the “current file position” of the file descriptor (this is used by the function pread(), for example).

If the _IO_XTYPE_OFFSET modifier is not present, then the read operation can go ahead and modify the “current file position.” We use the variable xtype to store the “xtype” that we received in the message, and the variable off to represent the current offset that we should be using during processing. You'll see some additional handling of the _IO_XTYPE_OFFSET modifier below, in step 7.

If there is a different “xtype override” than _IO_XTYPE_OFFSET (and not the no-op one of _IO_XTYPE_NONE), we fail the request with ENOSYS. This simply means that we don't know how to handle it, and we therefore return the error up to the client.

Steps 3 & 4
To calculate how many bytes we can actually return to the client, we perform steps 3 and 4, which figure out how many bytes are available on the device (by taking the total device size from ocb->attr->nbytes and subtracting the current offset into the device). Once we know how many bytes are left, we take the smaller of that number and the number of bytes that the client specified that they wish to read. For example, we may have seven bytes left, and the client wants to read only two. In that case, we can return only two bytes. Alternatively, if the client wanted 4096 bytes, but we had only seven left, we could return only seven bytes.
Step 5
Now that we've calculated how many bytes we're going to return to the client, we need to do different things based on whether or not we're returning data. If we are returning data, then after the check in step 5, we reply to the client with the data. Notice that we use data_string + off to return data starting at the correct offset (the off is calculated based on the xtype override).

Also notice the second parameter to MsgReply()—it's documented as the status argument, but in this case we're using it to return the number of bytes. This is because the implementation of the client's read() function knows that the return value from its MsgSendv() (which is the status argument to MsgReply(), by the way) is the number of bytes that were read. This is a common convention.

Step 6
Since we're returning data from the device, we know that the device has been accessed. We set the IOFUNC_ATTR_ATIME and IOFUNC_ATTR_DIRTY_TIME bits in the flags member of the attribute structure. This serves as a reminder to the stat I/O function handler that the access time is not valid and should be fetched from the system clock before replying. If we really wanted to, we could have stuffed the current time into the atime member of the attributes structure and cleared the IOFUNC_ATTR_DIRTY_TIME flag. But this isn't very efficient, since we're expecting to get a lot more read() requests from the client than stat() requests. However, your usage patterns may dictate otherwise.
Note: So which time does the client see when it finally does call stat()? The iofunc_stat_default() function provided by the resource manager library will look at the flags member of the attribute structure to see if the times are valid (the atime, ctime, and mtime fields). If they are not (as will be the case after our read I/O function handler has been called and returned data), the iofunc_stat_default() function will update the time(s) with the current time. The real value of the time is also updated on a close(), as you'd expect.
Step 7
Now we advance the lseek() offset by the number of bytes that we returned to the client, only if we are not processing the _IO_XTYPE_OFFSET override modifier. This ensures that, in the non-_IO_XTYPE_OFFSET case, if the client calls lseek() to get the current position, or (more importantly) when the client calls read() to get the next few bytes, the offset into the resource is set to the correct value. In the case of the _IO_XTYPE_OFFSET override, we leave the ocb version of the offset alone.
Step 8
Contrast steps 5 through 7 with this step. Here we only reply to the client while using the status parameter to tell it that we have 0 bytes left for it (read() will return this 0 indicating End Of File, EOF); we don't perform any other functions. Notice also that there is no data area specified to the MsgReply(), because we're not returning data.
Step 9
Finally, in step 9, we perform processing that's common regardless of whether or not we returned data to the client. Since we've already unblocked the client via the MsgReply(), we certainly don't want the resource manager library doing that for us, so we tell it that we've already done that by returning _RESMGR_NOREPLY.