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
  • email
  • Facebook
  • Google Buzz
  • Identi.ca
  • MisterWong
  • MySpace
  • Orkut
  • Print
  • Reddit
  • RSS
  • Slashdot
  • StumbleUpon
  • Technorati
  • Twitter
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
  • email
  • Facebook
  • Google Buzz
  • Identi.ca
  • MisterWong
  • MySpace
  • Orkut
  • Print
  • Reddit
  • RSS
  • Slashdot
  • StumbleUpon
  • Technorati
  • Twitter
7Sep/11Off

Fun with clang or: How to build Qt 4.8 with clang 3 without going insane

Given that XCode 4 on Mac ships with the the clang frontend for LLVM now, I wanted to give this new compiler a try and see if it can replace gcc for me.

To compile the CRJ plugin, I need Qt of course, and given this entry in the Qt blogs I thought it would be fairly easy to get this to work.

I was wrong.

First try: I checked out the Qt 4.8 branch from gitorious, and configured with -platform unsupported/macx-clang but I soon ran into a compile error complaining about an undeclared identifier "SetRect".

Second try: I checked out the specific git revision mentioned in the blog post, and re-configured. Build stops with the same error.

Thrid try: Given that the most recent clang is the 3.0 branch and that Apple ships 2.1, I decided to get myself a more recent clang. I checked out the llvm sources via svn, then the clang sources in the appropriate subdirectory, configured for a release-optimzed build (to boldly go...) and make'd. Some 10 or 20 minutes later I had a clang 3.0. I edited the mkspecs/common/clang.conf to point to my freshly compiled clang in /usr/local/bin and reconfigured Qt (I checked out master/HEAD before).  Now the built took longer before it stopped, this time everything of QtCore compiled, but when linking, I got an "undefined symbol for architecture i386: ___eprintf". Oh well.

Fourth try: Google pointed me to this thread which indicated I must built the compiler runtime also. So I did another svn checkout (compiler-rt into llvm/projects/), make clean'd, re-configured and built llvm/clang/compiler-rt again. The built aborted mocking about some missing headers for ARM.

Fifth try: I reconfigured llvm with --enable-architecture=x86 instead of the default --enable-architecture=all, which would built all kinds of backend. The next built took somewhat longer and perhaps 30 minutes later I was ready to try again: with the freshly installed clang I went ahead to compile Qt again. This time it actually linked QtCore, but when compiling QtGui it stopped again with a compiler error:

src/gui/kernel/qt_cocoa_helpers_mac_p.h:218:10: error:
cannot initialize return object of type 'NSString *'
with an rvalue of type 'const NSString *'

Apparently, clang doesn't like Cocoa.

Sixth try: I make confclean'd and re-configured Qt to build with -carbon. Unfortunately, the next make stopped at exactly the same location, at the Cocoa-helpers. Apparently this header is pulled in every time regardless whether we actually use Cocoa or not.

At this point, I decided to give up on OSX and try my luck on Linux.

I svn co'd llvm, clang and compiler-rt to my Suse, configured for optimzed built and started make. Short time after that *KABOOOM!*. No, not a compile error. GCC crashed. My compiler trapped during compiling. Seriously, WTF? I was first confused by the print output of GCC, that referred to a source file that I wasn't compiling at all, until it occurred to me that the indicated source was a source of GCC, a GCC function that crashed.

Oh the humanity! I went to the #clang channel on irc.freenode.net and luckily some helpful soul indicated what I should do. GCC would break regularly during optimized clang compiles. The way to go would be to build clang with clang

  1. First, build clang in debug mode without optimizations with gcc
  2. Then, use the un-optimized clang to build an optimized clang

Luckicly, this guy was spot on. I had encountered this error that prevents GCC 4.5.0 to build clang with -ftree-vectorize. So the non-optimized built succeeded, and then I was able to bootstrap the optimized clang from it.

I checked out Qt master again and configured with -platform unsupported/linux-clang. Build was uneventful, and my minimal testing-Qt (without webkit, Javascript, QtScript, QDeclarative, etc...) was ready within some 30 or 45 minutes.

Configuring Qt-Creator 2.3 to recognize clang as compiler and display its warnings was straightforward.

Now I took a deep breath and compiled my airnav library on -Wall, which is a lot more pedantic than the -Wall of gcc. And bingo!!
The -Wall -Wextra compile however showed some minor warnings that I've never seen with gcc - it complained about an explicit template instantiation outside the namespace of the template. This was fixed changing two lines of code, and after that I got it through -Werror -Wall -Wextra. W00t!

Lessons learned:

  • clang is not yet ready to be used on a regular day-to-day basis and still requires lots of fiddling
  • Qt's codebase is relatively clean, as it compiles with clang with relatively few warnings
  • the Cocoa headers of the 10.7 SDK however are not
  • It is great to have yet another reference compiler to check for code-cleanliness

So I reached at least one of my goals: I have another reference compiler I can test my code against: I regularly compile my whole codebase with Visual C++ 10 and gcc 4. Now I have another testing point that reveals more potential issues (portability, unsafe assumptions).

I won't ship clang-compiled plugins to end-users soon, but I will definitely use clang-build plugins myself now for testing purposes.

  • del.icio.us
  • Digg
  • email
  • Facebook
  • Google Buzz
  • Identi.ca
  • MisterWong
  • MySpace
  • Orkut
  • Print
  • Reddit
  • RSS
  • Slashdot
  • StumbleUpon
  • Technorati
  • Twitter
Tagged as: , , No Comments
11May/11Off

snprintf is not _snprintf_s_WTF_SNAFU !

The last three days were filled up with collecting all the customer reports and preparing the first service pack for the CRJ. Luckily, no real showstopper showed up, but one thing was strange:

I was fixing the display of 25kHz spaced COM-frequencies, where the CRJ 1.0.0 software had a stupid rounding error. I implemented the fix on my Linux Notebook, and was satisfied with the result. I pushed the fix to my git repository, pulled on my Windows PC and compiled again. Starting up the CRJ I had an instant crash with the dreaded "X-Plane.exe has stopped working"-window.

What had happened? To display the value on the small RTU display, I use a well-known C99 function called "snprintf()". It is the safe counterpart of sprintf() (Stream print formatted) that takes an additional argument, the maximum number of characters to be written to the destination buffer. What is great about snprintf() is that it guarantees the output buffer is null-terminated, even if when the output is truncated. And that was precisely what I wanted: The output had to be truncated to 7 characters, 3 characters the frequency before the decimal, then the decimal point, then two digits after the decimal, then the null-terminator. If I put in the frequency as 136.375 it should be truncated so the string reads 136.37\0

Fair enough, now why was the CRJ crashing so violently on Windows? The answer is: Visual C++ doesn't have an snprintf() function. There exist the following two "equivalents": _snprintf() and sprintf_s(). The former was what I used - and guess what - the Microsoft _snprintf() does NOT guarantee the null-terminator!!1!

So the frequency on the RTU read something like 136.375!$Dg=73*+#?§ with some random garbage behind the 7th character.

snprintf() is a C99 standardized function, for god's sake! It's not a GNU extension or a Torvalds memorial function. It's flipping C99 AND C++0x!

Google for the rescue! Apparently quite a lot of people on MSDN have already stumbled over the retarded _snprintf function and there is a workaround that behaves consistent to the C99 requirements:

#ifdef _MSC_VER
#include <cstdarg>
inline int my_snprintf(char* str, size_t size, const char* format, ...)
{
    size_t count;
    va_list ap;
    va_start(ap, format);
    count = _vscprintf(format, ap);
    _vsnprintf_s(str, size, _TRUNCATE, format, ap);
    va_end(ap);
    return count;
}
#define snprintf my_snprintf
#endif

Now that's a kludgy workaround that keeps the CRJ flying...

  • del.icio.us
  • Digg
  • email
  • Facebook
  • Google Buzz
  • Identi.ca
  • MisterWong
  • MySpace
  • Orkut
  • Print
  • Reddit
  • RSS
  • Slashdot
  • StumbleUpon
  • Technorati
  • Twitter
23Jan/11Off

C++ bei Chaosradio Express

Note to my english readers: This is a link to a german podcast.

Endlich bin ich dazu gekommen, mir die vielversprechende Folge über C++ anzuhören. Chaosradio Express steht seit etwa einem Jahr bei mir ganz oben auf der Liste der abzugrasenden Informationsquellen. Ich habe aufgehört zu zählen, wie viele Stunden ich schon damit verbracht habe, mir Folgen dieses hochinformativen Podcasts reinzuziehen. Jede einzelne war eine Erleuchtung.

So auch diese (schon recht alte) Folge über das "Programmieruniversum" C++. C++ ist mehr als eine Sprache, es ist eine Art zu Denken. Das wird hier sehr gut herausgearbeitet, vorallem der Kontrast zwischen old-school Programmierer (Pavel, Gast) und Scriptsprachen-Jungspund (Tim, Moderator) sorgt für eine gute Spannung. Abzug in der B-Note gibt es für Pavel's Diktus - er spricht sehr langsam mit vielen "Füllwörtern". Im Zweifel einen Mediaplayer verwenden, wo man mit 1,3facher Geschwindigkeit abspielen kann. Dann ist es perfekt.

http://chaosradio.ccc.de/cre063.html

Wer einen Einblick in das C++ Universum braucht, kommt voll auf seine Kosten. Anhören! Sofort!

  • del.icio.us
  • Digg
  • email
  • Facebook
  • Google Buzz
  • Identi.ca
  • MisterWong
  • MySpace
  • Orkut
  • Print
  • Reddit
  • RSS
  • Slashdot
  • StumbleUpon
  • Technorati
  • Twitter
Tagged as: , , No Comments
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
  • email
  • Facebook
  • Google Buzz
  • Identi.ca
  • MisterWong
  • MySpace
  • Orkut
  • Print
  • Reddit
  • RSS
  • Slashdot
  • StumbleUpon
  • Technorati
  • Twitter
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
  • email
  • Facebook
  • Google Buzz
  • Identi.ca
  • MisterWong
  • MySpace
  • Orkut
  • Print
  • Reddit
  • RSS
  • Slashdot
  • StumbleUpon
  • Technorati
  • Twitter
22Oct/10Off

Knowledgebase for cross-platform X-Plane plugin development

Hi folks,

this is my blog about the development of X-Plane plugins, about the beauty of C++ code, about X-Plane and aviation in general.

The goal is to bundle all the advice on cross-platform development I scattered on the X-Plane.org forum in the last year.
So I will try to sum up everything you need to know to successfully build X-Plane plugins.

I will start by migrating blog entries from the X-Plane.org blog here, and then organize my various posts related to cross-platform plugin development to build some kind of knowledgebase for plugin developers.

Comments and suggestions are highly welcome.

Philipp

  • del.icio.us
  • Digg
  • email
  • Facebook
  • Google Buzz
  • Identi.ca
  • MisterWong
  • MySpace
  • Orkut
  • Print
  • Reddit
  • RSS
  • Slashdot
  • StumbleUpon
  • Technorati
  • Twitter