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:
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:
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:
Or:
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)