X-Developer Cross-platform development in C++, Flight Simulation

21Mar/12Off

Compile-time checked to not fly a pretzel

The CRJ 1.4.5 update is uploading to the publishers now, and you will see it on your machines this week.

The biggest advance is what I call the no-more-pretzels-technology. The CRJ was notorious for doing awkward things when trying to enter a holding pattern. Entering a holding in what was supposed to be a parallel entry looked more like flying a pretzel.

If you are not interested in software development, you can stop reading here. Just believe me it works great now. If you want to know why, read on.

How does no-more-pretzels work?
It is all about treating object-oriented design as type-design. Just like Scott Meyers says in Effective C++ (i take it for granted you all have a copy of this book besides your bed. If not, buy it now!) you're not doing object-oriented design right unless you treat class design as type design.

The key to get the holdings right was to solve the confusion I was having about the four types of "course" there are:

  • True Track
  • Magnetic Track
  • True Heading
  • Magnetic Heading

If you are passing around courses as numbers (floats), you are in the constant state of wondering "wait, is this magnetic? And what about wind here anyway? Is the wind direction magnetic or true? Can I add a correction with regard to magnetic wind on this true course?".
Believe me, you will screw that big times. My git log shows I touched the holding and holding entry code a gazillion times correcting such problems.

Now types to the rescue: A true track is not a float! A true track is a type! And a true course is a different type!

So here's how it works: You can't compare a true track to a magnetic heading. Because there is no operator for that. You can't add a magnetic wind correction on a true heading. No operator for that.

Compare this code:

float crs = 315.f; // is this 315 true or magnetic ???
crs += mag_var; // did we just correct from true to mag or the other way?
crs += wca; // did we just convert track to heading or heading to track???

with this code:

// MagenticTrack holding_inbd = 315._degm;  
/* You can do this if your compiler supports C++11 user-defined 
literals, which for now only a gcc 4.7 pre-release does */
MagenticTrack holding_inbd = MagneticTrack::fromDegrees(315); 
WindCorrection wca = WindCorrection::fromTrackAndWind(holding_inbd, 
                                                      wind,
                                                      groundspeed);
// operator+ overloading ftw!
MagneticHeading crs_to_steer = holding_inbd + wca;

More verbose, certainly, but compile-time checked to make sense.

Compile-time checked to not fly pretzels.

  • del.icio.us
  • Digg
  • Facebook
  • Reddit
  • RSS
  • Twitter
  • Add to favorites
  • email
  • HackerNews
  • LinkedIn
  • Slashdot
Tagged as: , Comments Off
1Dec/11Off

IPv6 … isn’t!

Yesterday we shipped the 1.3 update for the CRJ. It included the much-awaited remote CDU functionality, which is basically a web server running in the plugin, serving the CDU screen in HTML format via Ajax.
The webserver in the plugin is a very basic HTTP server built with boost.asio, an incredibly powerful cross-platform asynchronous network library.

Thinking progressive as I always do, I of course built the webserver with IPv6 support, and configured it to always start in dual-stack mode, that means it serves the CDU both via IPv4 and IPv6.
I thought I could relax on that basis, and not touch this code again until the IPv6 address space is full.

I was so wrong!
Reports from customers kept coming in, that they couldn't start the CRJ any more. A very helpful customer showed me a gdb stacktrace of what was wrong. Guess what: it couldn't open a listening socket on 0::0, the "any" IP address in IPv6.
No, this guy was not using Windows 98. He was using the latest and greatest Ubuntu Linux. SRSLY, WTF?! It is 2011 and Ubuntu 11.10 has IPv6 support disabled by default?
Next customer with this problem was using Windows XP. Still the most popular MS OS these days. Guess what, no IPv6 enabled by default!

After instructing those guys how to enable IPv6 on their machines, I coded a patch for the server, to fallback to IPv4 only when dual-stack is unavailable. This is released as the 1.3.1 hotfix now.

I wonder how this planet is going to make the transition to IPv6 any time soon, when about 25% of my customers just can't use it without browsing through cryptic knowledgebase articles...

  • del.icio.us
  • Digg
  • Facebook
  • Reddit
  • RSS
  • Twitter
  • Add to favorites
  • email
  • HackerNews
  • LinkedIn
  • Slashdot
5May/11Off

Analyzing how X-Plane propagates your position

No, this is not a "it's been a while"-post. You probably all know what I've been working on.

Instead, this is a "fancy engineering stuff" post.

Let's discuss briefly how X-Plane knows where your planes is. X-Plane, as we all know, calculates the forces that act on your airplane. The force that the engine applies to your airframe, for example. When you think back to your 9th grade physics class, you will recall that force is mass times acceleration. Or in other words, if you assume the mass of the airplane constant, force is a direct measure for the acceleration of your airplane.

Now recall that when you integrate acceleration over time, you get the speed. So by knowing the forces that X-Plane calculates based on the effect of the air on your plane, X-Plane knows the acceleration on your plane, and if you step forward a little discrete timestep ("one frame") and integrate this acceleration over time, X-Plane knows the speed of your plane.

Position (or, more accurately, distance) is velocity integrated over time. So when you progress another little discrete timestep and integrate the speed, you get the distance the plane has travelled. Add this distance to the point where your flight started and voila, X-Plane knows where your plane is now.

Okay, why do I care about this? When developing plugins for simulating, say, an FMC, I come across the need to know several parameters all the time, and these are, among others, position, groundspeed and true track (in X-Plane's jargon called "hpath"). The question I was asking myself was "I can get this out of the datarefs, of course. But could I possibly get this data like the real units do, from accelerometers and gyros?"

So what I tried to do is: Get the position out of X-Plane using ONLY a simulated accelerometer. The accelerometer is easy: X-Plane provides three datarefs to get the three local accelerations (sim/flightmodel/position/local_ax, .../local_ay, .../local_az).

So starting with nothing but an initial position and heading, I set up a flightloop callback that is called every frame, and in each callback I did what I know X-Plane must also do: Integrate the local accelerations twice and propagate from the last known position.

The first approach was like this:

  • get speed by explicit Euler integration of the acceleration
  • get distance by another explicit Euler integration of the speed
  • calculate the Place/Bearing/Distance point from the last known location (in Lat/Lon) by using the last known hpath and the distance I just calculated
  • Set this PBD point as the new position and start over

Now this proved to be a bad idea: Position of X-Plane and my calculated position diverged rather quickly. After a minute of flight, I was more than a mile off.

I decided to scratch all the fancy great circle maths (needed for the Lat/Lon calculation), and stay in X-Plane's OpenGL coordinate system as long as I could.

My new approach was calculating the position entirely in this coordinate system and use XPLMLocalToWorld() only as a finalizer when displaying the coordinates.

Much better! I was down to like a mile per 10 minutes of flight. Completely useless if you want to cross the pond based on this navigation, but hey, I'm learning here!

Now I began messing with the integration methods. Explicit Euler is a very rough approximation. I briefly thought about using some pre-calculations and using a Runge-Kutta integration, but then I thought I could just save the last acceleration data in a list and use a multistep method. Adams-Bashfort is the most straightforward to use.

So now I had my setup:

  • push the acceleration into the list of acceleration history (and drop the oldest out of the history)
  • get speed by an Adams-Bashfort integration of the acceleration history
  • push this speed into the speed history and drop the oldest speed
  • do another Adams-Bashfort integration, this time on the speed history, which gives the travelled distance
  • add the travelled distanced onto the local OpenGL coordinates
  • transform to world coordinates and write those to datarefs

The result is w00t! I flew the little Cirrus jet in bumpy weather for like an hour, before calculating the difference between my position and X-Plane's. The difference was less than 500 meters. Now this is a navigation system you can fly cross-the-pond with!

So what is all this about? It's the proof that you can simulate navigation in X-Plane WITHOUT using all the data that X-Plane gives you, but you don't have in real life applications. An FMS in the real world can't ask the plane for the groundspeed (unless you have a GPS receiver, of course), it can only ask accelerometers, laser gyros, air data sensors, etc.
What my little experiment shows, is that you can do this in X-Plane and simulate an Inertial Reference Unit that allows you to navigate based solely on the output of the three datarefs, our simulated accelerometer.

However, there is one small problem you have to catch: X-Plane changes it's openGL reference frame as you travel (everytime you see new scenery being loaded, there also occurs a shift in the coordinate reference). So I need a little trick: Watch the datarefs for the reference point, and if they change, do an XPLMWorldToLocal() transform of my estimated position and overwrite my integrated position with these new local coordinates. Note that this does NOT reset the position to the "true" position, it just changes the reference frame in which my second integration is running.

So now on to even more advanced FMS simulations for X-Plane!

  • del.icio.us
  • Digg
  • Facebook
  • Reddit
  • RSS
  • Twitter
  • Add to favorites
  • email
  • HackerNews
  • LinkedIn
  • Slashdot
12Dec/10Off

LeanAssist, a plugin for flying recips in X-Plane

Did you ever wonder how to lean your engine properly? How to get the maximum power out of it, when in climb? Or howto squeeze the last NM of range out of your fuel on board?

LeanAssist is a small plugin that helps you with leaning the engine. While you lean, it detects peak EGT and can then guide you to either best power or best economy mixture.

LeanAssist works with all airplanes having a fuel-injected, normally aspirated piston engine. As opposed to real-life,  in X-Plane all engines have perfect mixture distribution, so you can all run them lean-of-peak.

To install LeanAssist, simply unzip to your X-Plane main folder. To finish installation, you have to assign a key command or a joystick button in X-Plane. This is explained in the video.

To use lean assist, press the assigned key or joystick button and start leaning the engine slowly. A small screen overlay will appear, reading "looking for peak". Lean slowly until the indication changes to "Peak detected".

  • For best power mixture, enrich again. The indication will display the ROP EGT delta. At 70°F ROP, the display will signal "best power". Stop enriching. The indication will disappear after a few seconds.
  • For best economy mixture, lean further. The indication will display the LOP EGT delta. At 50°F LOP, the display will signal "best economy". Stop leaning. The indication will disappear after a few seconds.

Also explained in the video:

If you want to learn more about leaning and what happens inside your engine when you adjust your mixture, read this excellent article:  http://www.avweb.com/news/pelican/182084-1.html

Download at X-Plane.org:

On Windows, please make sure you have the latest Visual C++ 2010 redistributable package installed. Download it here: http://www.microsoft.com/downloads/en/details.aspx?familyid=A7B7A05E-6DE6-4D3A-A423-37BF0912DB84&displaylang=en

  • del.icio.us
  • Digg
  • Facebook
  • Reddit
  • RSS
  • Twitter
  • Add to favorites
  • email
  • HackerNews
  • LinkedIn
  • Slashdot
24Nov/10Off

Byte datarefs and refcon-pointers to heap data

Designing a cockpit in Plane-Maker you probably came across the "byte datarefs" that allow you to show text on generic instruments, like the generic text LED readout.

In a discussion on the .org forum a plugin programmer was wondering how to publish a byte dataref himself from an X-Plane plugin and promptly fell prey to "wild pointers" and memory corruption, because strings in C are not as easy as Strings in other programming languages like Java.

So here is the bullet-proof solution for publishing string data refs from a plugin:

1. The basics:
How do you tell X-Plane that you serve a string?

XPLM_API XPLMDataRef          XPLMRegisterDataAccessor(
                                   const char *         inDataName,
                                   XPLMDataTypeID       inDataType,
                                   int                  inIsWritable,
                                   XPLMGetDatai_f       inReadInt,
                                   XPLMSetDatai_f       inWriteInt,
                                   XPLMGetDataf_f       inReadFloat,
                                   XPLMSetDataf_f       inWriteFloat,
                                   XPLMGetDatad_f       inReadDouble,
                                   XPLMSetDatad_f       inWriteDouble,
                                   XPLMGetDatavi_f      inReadIntArray,
                                   XPLMSetDatavi_f      inWriteIntArray,
                                   XPLMGetDatavf_f      inReadFloatArray,
                                   XPLMSetDatavf_f      inWriteFloatArray,
                                   XPLMGetDatab_f       inReadData,
                                   XPLMSetDatab_f       inWriteData,
                                   void *               inReadRefcon,
                                   void *               inWriteRefcon);

This monster is the function for registering what the X-Plane SDK calls "owned" datarefs. "Owned" as in "my plugin owns this data. If it's not there, the data won't be available". But also "owned" as in "X-Plane crashed by memory corruption when reading array data. 0wned!"

The important parameters of the function are

XPLMGetDatab_f   inReadData, XPLMSetDatab_f   inWriteData

representing the function pointers that will be invoked when some instance tries to read from or write to your dataref.

So everytime X-Plane wants to update, say, your generic LED readout, it calls the funtion you registered as the inReadData function.

Let's have a look at the signature:

typedef long (* XPLMGetDatab_f)(
                                   void *               inRefcon,
                                   void *               outValue,    /* Can be NULL */
                                   int                  inOffset,
                                   long                 inMaxLength);

As we all know, calling the string read with outValue==NULL should return the number of characters in the string. Otherwise, the content of the string starting at inOffset to the end or until reaching inMaxLength shall be copied to outValue.

2. Strings in C are pointers !
The crucial point here is that strings in C are not values like and int or a float: A string in C is a pointer to some characters sitting in the memory, the last of them being 0x0 to indicate that the string ends. If this memory goes out of scope, the pointer will still be there, pointing to some random garbage in your memory.

This is the error most newbies trying to implement a byte-dataref fall prey to.
The most obvious solution of course would be to allocate the string on the heap and store the pointer in a global variable.

This approach has a downside: Raw string handling in C is s a pain in the neck.

3. Using std::string instead of char*
The string from the C++ standard library is great: You don't have to worry about pointers, scope, manual memory management, resizing, reallocations, ...
The std::string can be converted to a c-string and vice-versa. Let's implement the read function using a global C++ std::string:

#include  <cstring> // for strcpy
#include  <string>  // for std::string
 
std::string global_string = "hello world";
 
long readFuncStr(void* inRefCon, void* outValue, int inOffset, long inMaxLength)
{
    // we were asked to return the length of the string
    if (outValue == NULL)
        return global_string.length();
    // we have to copy the characters from offset to maxlength into outValue
    // c_str() is the conversion function from std::string to const char*
    strcpy(static_cast<char* >(outValue), global_string.substr(inOffset,inMaxLength).c_str());
    return inMaxLength;
}

now we don't need to worry about pointers to invalid data anymore.
The write funtion is about just as easy:

void writeFuncStr(void* inRefCon, void* inValue, int inOffset, long inMaxLength)
{
    char* str = static_cast<char* >(inValue);
    // replace global string with required substring of inValue
    // note that the std::string c'tor is overloaded to take a char*
    global_string = std::string(str).substr(inOffset, inMaxLength);
}

Okay, now suppose we have to two strings in our plugin. What do we do now? Create two sets of read- and write-functions? No, of course not.

First, lets register the global strings as refcon-pointers to our functions. Looks like this:

long readFuncStr(void* inRefCon, void* outValue, int inOffset, long inMaxLength)
{
    std::string* p_data = static_cast<std::string* >(inRefCon);
    if (outValue == NULL)
        return p_data->length() + 1;
    strcpy(static_cast<char* >(outValue), p_data->substr(inOffset,inMaxLength).c_str());
    return inMaxLength;
}
 
void writeFuncStr(void* inRefCon, void* inValue, int inOffset, long inMaxLength)
{
    std::string* p_data = static_cast<std::string* >(inRefCon);
    char* str = static_cast<char* >(inValue);
    *p_data = std::string(str).substr(inOffset,inMaxLength);
}

We now register our read and write function and assign the pointers accordingly:

XPLMDataRef my_data_ref = XPLMRegisterDataAccessor(
    "my/plugin/data/textdata" , // dataref
    xplmType_Data,  // data type
    1,   // writeable
    NULL, NULL, // no int functions
    NULL, NULL, // no float functions
    NULL, NULL, // no double functions
    NULL, NULL, // no int-array functions
    NULL, NULL, // no float-array functions
    &readFuncStr, // our read function
    &writeFuncStr, // our write function
    &global_string, // refcon-pointer for read function
    &global_string  // refcon-pointer for write function
);

4. Using a class to manage the strings
As said before, globals are evil. One possible solution is to store the string itself in a class, that calls XPLMRegisterDataAccessor() in its c'tor and XPLMUnregisterDataAccessor() in its d'tor. This way it is assured that the string you use lives exactly as long as the dataref is available to X-Plane.
The readFunc and writeFunc then become static functions in this class, and the refcon-pointer is the this-pointer of your instance.

long readFuncStr(void* inRefCon, void* outValue, int inOffset, long inMaxlength)
{
    OwnedData* p_owned_data = static_cast<OwnedData* >(inRefCon);
    long length = p_owned_data->value().length();
    if (outValue == NULL)
        return length;
    long maxlen = std::min(inMaxLength,length);
    strcpy(static_cast<char* >(outValue), p_owned_data->value().substr(inOffset, maxlen).c_str());
    return inMaxLength;
}
 
void writeFuncStr(void* inRefCon, void* inValue, int inOffset, long inMaxLength)
{
    OwnedData* p_owned_data = static_cast<OwnedData* >(inRefCon);
    char* str = static_cast<char* >(inValue);
    p_owned_data->setValue(std::string(str).substr(inOffset,inMaxLength));
}
 
class OwnedData{
public:
 
    OwnedData(const std::string& identifier):
        m_data_ref_identifier(identifier),
        m_data_ref(0),
        m_value(std::string(""))
    {
        m_data_ref = XPLMRegisterDataAccessor( m_data_ref_identifier.c_str(), xplmType_Data, 1,
                                                 NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
                                                 NULL, NULL, readFuncStr, writeFuncStr, this, this );
    }
 
    ~OwnedData()
    {
        if (m_data_ref)
        {
            XPLMUnregisterDataAccessor(m_data_ref);
            m_data_ref = 0;
        }
    }
 
    std::string value() const { return m_value; }
 
    void setValue(const std::string&amp; val) { m_value = val; }
 
private:
        friend long readFuncStr(void* inRefCon, void* outValue, int inOffset, long inMaxLength);
        friend void writeFuncStr(void* inRefCon, void* inValue, int inOffset, long inLength);
 
    XPLMDataRef m_data_ref;
    std::string m_value;
};

As always: beware of bugs in the above code :)

  • del.icio.us
  • Digg
  • Facebook
  • Reddit
  • RSS
  • Twitter
  • Add to favorites
  • email
  • HackerNews
  • LinkedIn
  • Slashdot
13Nov/10Off

Fat plugins, aircraft plugins, and how to install them

Looking at various postings in X-Plane forums I came to the conclusion that the two new concepts of plugin handling introduced with X-Plane 9 are still very little known. At least to end users.

in X-Plane 8, the world was simple: plugins could only be installed in the X-Plane/Resources/plugins folder and hat to be named *.xpl.
As X-Plane is a multiplatform software, this implied that only plugins matching the platform could be installed. Developers had to provide three different versions of their plugins (one for Windows, one for Linux, one for Mac) and the user had to download the appropriate one and put it into his plugins folder.

Since we all know no one of us reads manuals and we all want ease of installation, with X-Plane 9 the mechanism of "fat plugins" was introduced.

Now developers can put all three versions of their plugin into a folder and the user just drops the whole folder to his Resources/plugins directory. X-Plane will then select the right version of the plugin, based on the name. The plugins now have to be named win.xpl, mac.xpl and lin.xpl. The specific name of the plugin is now the folder name.

This makes installation easier for the user, since he just downloads one archive and drops one folder to his X-Plane installation. Furthermore, it makes migrating of a whole X-Plane installation from one OS to another easier.

The second new concept is aircraft plugins. Often plugins are needed to improve functionality of just one aircraft. In this case, it is unnecessary to keep them loaded all the time, but just when the aircraft is selected.

To achieve this, plugins can now also reside in the plugins/ subfolder of every aircraft, and are automatically loaded when the user selects this aircraft.

Also this makes installation of third-party aircraft easier to the end-user. Instead of having to download and aircraft, install it to the aircraft folder, than download a plugin and install it to the plugins folder, the user now downloads one archive, drops it to the aircraft folder and is done.

It is worth noticing that these technologies are orthogonal: You could have an aircraft-plugin that is non-fat. While this would work, it totally contradicts the "ease of installation" paradigm.

Bottom line for users: If a plugin comes as a folder, don't tear it apart and drop an individual file to your plugins folder as you did it in X-Plane 8. Leave the folder intact. X-Plane will select the right plugin for you.

Bottom line for developers: Create "fat" plugins. Always. No exception. Makes the installation easier for the user and helps keeping the plugins folder nice and organized.

  • del.icio.us
  • Digg
  • Facebook
  • Reddit
  • RSS
  • Twitter
  • Add to favorites
  • email
  • HackerNews
  • LinkedIn
  • Slashdot
30Oct/10Off

Passing an object to XPWidgetFunc_t

Programming with the old-school C-API of X-Plane, we got used to passing objects via the void* refCon parameters that are available for every callback function (like flightloop callbacks XPLMRegisterFlightloopCallback() or plugin-owned-data callbacks XPLMRegisterDataAccessor() ).

But the widget callback function

int widgetCallback(XPWidgetMessage, XPWidgetID, long, long);

doesn't have this parameter ! So how do you get an object in there?

Well, the way officially suggested by Supnik and Barbour is a really dirty hack: Store a pointer to the object as a widget property. How? By doing a reinterpret_cast<> on the pointer and setting it as an integer parameter. This is of course a really ugly kludge, but seems to work on all platforms where sizeof(long) == sizeof(void*).

Here's how you do it:

/* When setting up the widget */
XPWidgetID my_widget  = // whatever
YourWidgetClass object = // whatever you invented to handle widgets OO-style
XPSetWidgetProperty(my_widget, xpProperty_Object, reinterpret_cast<long>(&object));
 
/* In the callback function */
int widgetCallback(XPWidgetMessage inMessage, XPWidgetID inWidget, long param1, long param2)
{
    YourWidgetClass* widget = reinterpret_cast<YourWidgetClass*>(XPGetWidgetProperty(inWidget, xpProperty_Object, NULL));
    if (widget)
    {
        // do something with *widget
    }
    return 0;
}

Remember: This is not type-safe. We are lying to the compiler, so if something goes wrong here, we're on our own. No std::bad_cast exception if we screwed it up, only UB.

  • del.icio.us
  • Digg
  • Facebook
  • Reddit
  • RSS
  • Twitter
  • Add to favorites
  • email
  • HackerNews
  • LinkedIn
  • Slashdot
25Oct/10Off

The PLANE_LOADED gotcha

With the X-Plane interplugin messaging API you can have your plugin notified when a new plane is loaded. That is, you can react to either the user loading a new plane or a new plane being introduced via multiplayer.

The callback for receiving interplugin messages is

PLUGIN_API void XPluginReceiveMessage(XPLMPluginID, long inMessage, void* inParam)

and the documentation says

#define XPLM_MSG_PLANE_LOADED 102

This message is sent to your plugin whenever a new plane is loaded. The parameter is the number of the plane being loaded; 0 indicates the user's plane.

So when receiving a callback for the XPLM_MSG_PLANE_LOADED message, you have to check the parameter to be 0 if you want to react to a change of the user's plane.

Unfortunately, the documentation is inconsistent here

The parameter is the number of the plane being loaded

The param being a void* one might think that with this message, this pointer points to an int that holds the plane number, so you can get the behaviour you want as follows:

/* warning: DOING IT WRONG !!!1! */
PLUGIN_API void XPluginReceiveMessage(XPLMPluginID, long inMessage, void* inParam)
{
    if( inMessage == XPLM_MSG_PLANE_LOADED )
    {
        int* planeNo = (int*)(inParam);
        if ( *planeNo == 0) /* BOOOM! SEGFAULT!! */
        {
            // do some stuff
        }
    }
}

I tripped into this pitfall two times and spend almost a day figuring out why X-Plane segfaulted when my plugin was loaded.

Instead what really happens here is that they pass the integer ITSELF as the void*. So you have to check

inParam == 0   /*doing it right*/

instead of

*inParam == 0 /* doing it wrong*/

This is one of the reasons why I strongly encourage the use of the new style C++ casts instead of the old C-casts.

If you write this as C++ code with a C++-style cast, it becomes totally clear what happens here. It is a reinterpret_cast going on, not a static cast!

int planeNo = (*(static_cast<int*>(inParam))); /* wrong */
int planeNo = reinterpret_cast<int>(inParam);   /* right */

Bottom line: If you use void* for passing parameters, the documentation should be very specific on what is going on. The compiler won't help you.

  • del.icio.us
  • Digg
  • Facebook
  • Reddit
  • RSS
  • Twitter
  • Add to favorites
  • email
  • HackerNews
  • LinkedIn
  • Slashdot
23Oct/10Off

Failure datarefs: The failure enum

Because it is missing in the original documentation, these are the values you get from an integer dataref flagged "failure enum":

0 = always working
1 = mean time until failure
2 = exact time until failure
3 = fail at exact speed KIAS
4 = fail at exact altitude AGL
5 = fail if CTRL f or JOY
6 = inoperative
  • del.icio.us
  • Digg
  • Facebook
  • Reddit
  • RSS
  • Twitter
  • Add to favorites
  • email
  • HackerNews
  • LinkedIn
  • Slashdot
22Oct/10Off

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

  • del.icio.us
  • Digg
  • Facebook
  • Reddit
  • RSS
  • Twitter
  • Add to favorites
  • email
  • HackerNews
  • LinkedIn
  • Slashdot