Blocking I/O and select()
Imported from X-Plane.org and filed under "plain C hacks".
Hi folks,
a recent entry in the Plugin Developers Forum here on the .org made me remember the last year when I was facing the very same problem.
So why does X-Plane lock up completely when you try to read data from the serial port or a network socket? Basically you call a function "get me data from there!" and the function dutifully looks at the specified place (e.g. the serial port) for fresh bytes. If there is nothing to fetch it just sits there and waits... as long as it takes until data arrives.
Now it is crucial to understand that all function calls you do in a plugin happen synchronously. Nothing can be done as long as you are waiting for the read function to return.
Moreover, not only your plugin has to wait, but since the plugin processing is called from X-Plane again in a synchronous way, the whole simulation stops while the socket function waits for data.
This behavior is what we call "blocking I/O".
So what do you do about it?
There are two ways to handle it: You can check if there is really data available BEFORE you call the read function. This way you can guarantee your plugin can immediately continue with its work, either with fresh data or without.
The other way is turn the blocking IO effectively into non-blocking IO by putting the blocking function in a separate thread the runs concurrent to X-Plane (and your plugin).
The first way is pretty straightforward:
You have to enclose your processing in another if-clause that checks if the IO is ready
1 2 3 4 5 | if (ready_read(your_file_descriptor) == 1) { blocking_read(your_file_descriptor); decode_input_and_do_whatever(); } |
if there is nothing to fetch, the blocking read function will never be called. So how do you implement "ready_read(file)" ?
There is a function defined by the POSIX standard that is called select(). Select takes a few arguments, among them the file_descriptor and a struct that represents a timeout. What select() does is either return immediately, if there is data at the file_descriptor, or wait for a maximum timeout that you specify.
Select() is part of the Socket-System and available on all UNIX-platforms, that means on Linux and MacOS. But you can also use it under Windos, because Microsoft in there rare moments of reason had the good idea to implement something compatible to UNIX sockets, and that is called winsock2.
So all you need for a working select() is the correct header files for your operating system, and on windos a library called "ws2_32.lib" that you have to link statically.
Here is how you code it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | int ready_read(int descriptor) { int res; fd_set sready; struct timeval nowait; FD_ZERO(&sready); FD_SET((unsigned int)descriptor,&sready); nowait.tv_sec = 0; // specify how many seconds you would like to wait for timeout nowait.tv_usec = 0; // how many microseconds? If both is zero, select will return immediately res = select(descriptor+1,&sready,NULL,NULL,&nowait); if( FD_ISSET(descriptor,&sready) ) return 1; else return 0; } |
To compile this, you need the headers
1 2 3 4 5 | #include <unistd.h> #ifdef WIN32 #include <winsock2.h> #else #include <sys/socket.h> |
And in your makefile, you specify that if you are on Windos, to define the WIN32 symbol and link against ws2_32
So that was the easy way to do it.
The more sophisticated way is to "outsource" the blocking operation to another thread and define a callback that is called asynchronously whenever data arrives.
If you are familiar with the "reactor pattern" and know how to utilize pthreads that shouldn't be too much of a challenge. But wait! You cannot always write your data to X-Plane, it is crucial that you obey to the regulations for plugins - because you may well crash X-Plane when you write to a dataref when the sim is not in a consistent state. So it is absolutely necessary to read the technote on multithreading in plugins.
Happy coding to all plugin-devs out there and always remember to reset your wild pointers!
Phil