Maintaining Binary Compatibility

Binary Compatibility in 3 Easy Steps!

An Introduction

In the early days of the OpenBeOS project, a debate raged concerning one of the projects primary goals: maintaining binary compatibility with BeOS R5. The idea was that the only way an effort to rewrite BeOS would be successful was if folks could continue running the apps they already had. Certainly, a lot of software available for BeOS is open source or actively maintained -- these apps could just be recompiled if necessary. Others -- PostMaster, gobe's Productive suite and a few other crucial apps -- weren't likely to get rebuilt, either because the original author had stopped maintainance without being kind enough to release the source, or because it just wouldn't be commercially feasible.

Some said that we were crazy; that it couldn't be done. Thankfully, cooler heads prevailed and we're well on our way to a binary compatible clone of R5.

"But wait!" you cry. "How did the cooler heads which prevailed know that the holy grail of binary compatibility was achievable?" I'm so glad you asked! Keeping reading and be enlightened, Grasshopper.

The Issues

There are three basic issues that have to be addressed to ensure binary compatibility:

The Nitty Gritty

"Good grief!" you say. "How on earth do I keep all this stuff straight?" Ah, Grasshopper, it is easier that you might imagine. Just follow these steps, and you should be binary compatible in no time!
  1. Make a copy of the appropriate Be header file
    This is now your header file. You may need to change a thing or two, but what you can (or will need to) change is quite limited, and discussed below.
  2. Implement public, protected and virtual functions
    In the course of doing this, you may discover that there are some private non-virtual function declarations that you just don't use. Feel free to axe them! Since they're private, nobody using the class will miss them, and because they're not virtual, they don't effect the vtable layout. Conversely, if you find a need to add some private, non-virtual functions, go right ahead (for the very same reasons).
  3. Make sure you don't change the number of bytes used for data
    There are two situations that can make this seem difficult. First, there may be data members that you don't use in your reimplementation. You can just leave them (safe, but a little messy) or you can add the extra members' bytes to the class's "unused" data array. An example will make this clear.
    Let's say we have a class BFoo:
    	class BFoo {
    	    public:
    	        BFoo();
    	        ~BFoo();
    	        void SomeFunc();
    	    private:
    	        int32 fBar;
    	        char  fZig;
    	        int32 fQux;
    	        int32 fUnused[2];
    	};
    		
    The Be engineers that originally wrote this BFoo purposely added some data padding in the form of an array of 2 int32s (they did this with most classes in the API). Now let's suppose in your implementation, you really didn't need fQux. You can add fQux's bytes into fUnused:
    	class BFoo
    	{
    	    ...
    	    private:
    	        int32 fBar;
    	        char  fZig;
    	        int32 fUnused[3];
    	};
    		
    Nothing could be easier! An additional twist that should be noted is that data member order must also be preserved. In much the same way as existing apps will "look" for virtual functions in a specific place in the vtable, so will they "look" for data members in a specific place in an object.
    "But what if I don't need fZig, either?" you wonder. "It's only one byte, not four like an int32!" Have no fear! Just rename it "fUnusedChar" and be done with it.

    The second situation that can make preserving object size tricky is if there aren't enough bytes of data available. Building on our cheesy BFoo example, let's suppose that rather than getting rid of fQux and fZig, you actually needed to add another 4 int32s worth of data: fNewData1 through fNewData4. The original implementation of BFoo has two extra int32s which we can use, but that leaves us two int32s short. What to do? The easiest thing is to create a data structure to hold your new data and convert one of the fUnused items into a pointer to that structure:

    	// Foo.h
    	struct _BFooData_;
    	class BFoo
    	{
    	    public:
    	        BFoo();
    	        ~BFoo();
    	        void SomeFunc();
    	    private:
    	       int32 fBar;
    	       char  fZig;
    	       int32 fQux;
    	       _BFooData_* fNewData;
    	       int32 fUnused[1];
    	};
    
    	// Foo.cpp
    	struct _BFooData_
    	{
    		int32 fNewData1;
    		int32 fNewData2;
    		int32 fNewData3;
    		int32 fNewData4;
    	};
    	BFoo::BFoo()
    	{
    		fNewData = new _BFooData_;
    	}
    	BFoo::~BFoo()
    	{
    		delete fNewData;
    	}
    		
    Voila! More data without making the class bigger. Make sure you're cleaning up your new (dynamically allocated) data in the destructor. NOTE: this trick will only work if the class originally had a destructor! Adding a destructor after the fact won't work for existing apps (since they won't call it), leading to memory leaks. Fortunately, there are very few instances in the BeAPI where a class doesn't have a destructor already declared.
And there you have it: Binary Compatibility in 3 Easy Steps!
Questions, comments, or corrections? Please let me know!

The OpenBeOS project is hosted by:

SourceForge Logo

Copyright © 2001-2002 OpenBeOS Project