Translating messages to devctl() or _IO_MSG

So the question becomes, how do you perform control operations? The easiest way is to use the devctl() call.

Our CLID library example (above) now becomes:

/*
 *  CLID_AddSingleNPANXX (npa, nxx)
*/

int
CLID_AddSingleNPANXX (int npa, int nxx)
{
    struct  clid_addnpanxx_t    msg;

    checkAttach ();  // keep or delete, style issue

    msg.npa = npa;
    msg.nxx = nxx;
    return (devctl (clid_pid, DCMD_CLID_ADD_NPANXX, &msg, 
                    sizeof (msg), NULL));
}

As you can see, this was a relatively painless operation. (For those people who don't like devctl() because it forces data transfers to be the same size in both directions, see the discussion below on the _IO_MSG message.) Again, if you're maintaining source that needs to run on both operating systems, you'd abstract the message-passing function into one common point, and then supply different versions of a library, depending on the operating system.

We actually killed two birds with one stone:

  1. Removed a global variable, and assembled the messages based on a stack variable—this now makes our code thread-safe.
  2. Passed only the correct-sized data structure, instead of the maximum-sized data structure as we did in the previous (QNX 4) example.

Note that we had to define DCMD_CLID_ADD_NPANXX—we could have also kludged around this and used the CLID_MsgAddSingleNPANXX manifest constant (with appropriate modification in the header file) for the same purpose. I just wanted to highlight the fact that the two constants weren't identical.

The second point that we made in the list above (about killing birds) was that we passed only the “correct-sized data structure.” That's actually a tiny lie. You'll notice that the devctl() has only one size parameter (the fourth parameter, which we set to sizeof (msg)). How does the data transfer actually occur? The second parameter to devctl() contains the device command (hence “DCMD”).

Encoded within the top two bits of the device command is the direction, which can be one of four possibilities:

  1. “00”—no data being transferred
  2. “01”—transfer from driver to client
  3. “10”—transfer from client to driver
  4. “11”—transfer bidirectionally

If you're not transferring data (meaning that the command itself suffices), or if you're transferring data unidirectionally, then devctl() is fine.

The interesting case is when you're transferring data bidirectionally, because (since there's only one data size parameter to devctl()) both data transfers (to the driver and back) will transfer the entire data buffer! This is okay in the sub-case where the “input” and “output” data buffer sizes are identical, but consider the case where the data buffer going to the driver is a few bytes, and the data coming back from the driver is large.

Since we have only one size parameter, we're effectively forced to transfer the entire data buffer to the driver, even though only a few bytes were required!

This can be solved by “rolling your own” messages, using the general “escape” mechanism provided by the _IO_MSG message.

The _IO_MSG message is provided to allow you to add your own message types, while not conflicting with any of the “standard” resource manager message types—it's already a resource manager message type.

The first thing that you must do when using _IO_MSG is define your particular “custom” messages. In this example, we'll define two types, and model it after the standard resource manager messages—one data type will be the input message, and one will be the output message:

typedef struct
{
    int     data_rate;
    int     more_stuff;
} my_input_xyz_t;

typedef struct
{
    int     old_data_rate;
    int     new_data_rate;
    int     more_stuff;
} my_output_xyz_t;

typedef union
{
    my_input_xyz_t  i;
    my_output_xyz_t o;
} my_message_xyz_t;

Here, we've defined a union of an input and output message, and called it my_message_xyz_t. The naming convention is that this is the message that relates to the xyz service, whatever that may be. The input message is of type my_input_xyz_t, and the output message is of type my_output_xyz_t. Note that “input” and “output” are from the point of view of the resource manager—“input” is data going into the resource manager, and “output” is data coming from the resource manager (back to the client).

We need to make some form of API call for the client to use—we could just force the client to manually fill in the data structures my_input_xyz_t and my_output_xyz_t, but I don't recommend doing that. The reason is that the API is supposed to “decouple” the implementation of the message being transferred from the functionality. Let's assume this is the API for the client:

int
adjust_xyz (int *data_rate,
            int *odata_rate,
            int *more_stuff);

Now we have a well-documented function, adjust_xyz(), that performs something useful from the client's point of view. Note that we've used pointers to integers for the data transfer—this was simply an example of implementation. Here's the source code for the adjust_xyz() function:

int
adjust_xyz (int *dr, int *odr, int *ms)
{
    my_message_xyz_t    msg;
    int                 sts;

    msg.i.data_rate = *dr;
    msg.i.more_stuff = *ms;
    sts = io_msg (global_fd, COMMAND_XYZ, &msg, 
                  sizeof (msg.i),
                  sizeof (msg.o));
    if (sts == EOK) {
        *odr = msg.o.old_data_rate;
        *ms = msg.o.more_stuff;
    }
    return (sts);
}

This is an example uses io_msg() (the user-defined message I/O function handler, which we'll define shortly—it's not a standard QNX-supplied library call!). The io_msg() function does the magic of assembling the _IO_MSG message. To get around the problems that we discussed about devctl() having only one “size” parameter, we've given io_msg() two size parameters, one for the input (to the resource manager, sizeof (msg.i)) and one for the output (from the resource manager, sizeof (msg.o)). Notice how we update the values of *odr and *ms only if the io_msg() function returns an EOK.

This is a common trick, and is useful in this case because the passed arguments don't get modified unless the actual command succeeded. (This prevents the client program from having to maintain copies of its passed data, just in case the function fails.)

One last thing that I've done in the adjust_xyz() function, is that I depend on the global_fd variable containing the file descriptor of the resource manager. Again, there are a number of ways that you could handle it:

Here's the source for io_msg():

long
io_msg (int fd, int cmd, void *msg, int isize, int osize)
{
    io_msg_t    io_message;
    iov_t       rx_iov [2];
    iov_t       tx_iov [2];
    int         sts;

    // set up the transmit IOV
    SETIOV (tx_iov + 0, &io_msg.o, sizeof (io_msg.o));
    SETIOV (tx_iov + 1, msg, osize);

    // set up the receive IOV
    SETIOV (rx_iov + 0, &io_msg.i, sizeof (io_msg.i));
    SETIOV (rx_iov + 1, msg, isize);

    // set up the _IO_MSG itself
    memset (&io_message, 0, sizeof (io_message));
    io_message.type = _IO_MSG;
    io_message.mgrid = cmd;

    return (MsgSendv (fd, tx_iov, 2, rx_iov, 2));
}

Notice a few things.

The io_msg() function used a two-part IOV to “encapsulate” the custom message (as passed by msg) into the io_message structure.

The io_message was zeroed out and initialized with the _IO_MSG message identification type, as well as the cmd (which will be used by the resource manager to decide what kind of message was being sent).

The MsgSendv() function's return status was used directly as the return status of io_msg().

The only “funny” thing that we did was in the mgrid field. QNX Software Systems reserves a range of values for this field, with a special range reserved for “unregistered” or “prototype” drivers. These are values in the range _IOMGR_PRIVATE_BASE through to _IOMGR_PRIVATE_MAX that you can use for your resource manager. In our example above, we assume that COMMAND_XYZ is something based on _IOMGR_PRIVATE_BASE:

#define COMMAND_XYZ (_IOMGR_PRIVATE_BASE + 0x0007)