How to write cross-platform code
blog.backblaze.com
blog.backblaze.com
Simply don't accept the changes if the code doesn't build cleanly and passes the test suite on all platforms.
Why bother the programmer to remember to do this?
Yuck. Here's how we do it:
$ git commit
$ git push origin HEAD:refs/for/master
The commit shows up in gerrit for code review. Immediately the buildbots (one for each platform) notice the change and attempt to compile it. The results are reported back to gerrit and a failed compile blocks the change from being submitted.Assuming the change compiles cleanly and passes code review, it can then merged to the master branch. Importantly, broken code doesn't get checked-in in the first place.
Now, we happen to use git and gerrit and buildbot, but any decent system should have code review and a build farm that allow changes to be tested and reviewed before they are checked in.
If your developers aren't following some procedure (e.g., test compile on every platform), consider it may be because there is too much friction. Instead of firing the developers, ask why they aren't following it. Maybe other developers are impacted too and just have a higher tolerance for menial tasks, but those tasks are still costing the organization.
http://code.google.com/p/gerrit/
http://github.com/jaysoffian/buildbot/commit/e4c0d458374b9a0...
And if you happen to use hudson:
Our rule was the code most compile w/o warnings on 3 dissimilar platforms before you could commit it. We were able to do this easily because we had network-mounted home directories, so when you logged into another machine, our development tree was automatically mounted. This made it easy to compile without checking in, then checking out.
You test your changes on all the various platforms by pushing it to the try server, it compiles on the various platforms we have set up at the moment (FBSD, Mac OS X, Windows, Ubuntu Linux) and gives you a status report on what if anything broke. If nothing is broken you can push it out to Review Board, at which point someone reviews the code.
Once the code has been reviewed and ship-it has been tagged on RB, the code is checked into subversion proper. At this time the buildbot picks up the change and does a quick (svn update) build to make sure nothing was broken, and then does a full (rm -rf buildir; svn co) build at the top of the hour.
Reports get mailed, code is checked in.
Somehow, almost magically, if you have two or preferably three platforms the relevant code abstracts itself nicely out of what is the low-end mess composed of platform specific concepts. I don't mean merely thinking in terms of FooHandles and BarStreams instead of "file descriptors" but the whole process of thinking that happens when you have to separate, in your brain, that what is "platform level code" and what is "problem level code".
On top of basic socket support, you then have to add support for the common protocols (HTTP, SMTP etc.). I think this alone explains the popularity of networking "batteries included" dynamic languages like Python, and Java's status as the de-facto enterprise standard for network programming. Standardized languages like C and C++ move too slowly for the internet world.
1) Don't use C++. Not all platforms have a c++ compiler.
2) Get your build system right. Don't let it get in your way.
3) Avoid #ifdefs. These don't scale particularly well as you increase the number of platforms. In the example in the article with the file size function, there would be a completely separate file with this function in it for each different version. The build system will compile the correct one.
4) Use plain boring ANSI C.
5) Developers don't have to compile on every platform (doesn't scale when you support bazillions of platforms and toolchains). If something is broken on your platform you fix it. If someone breaks something, you politely let them know what the problem was.
6) Avoid undefined behaviour. This is harder than you might think. Different compilers will be able to do different optimisations, and they will take advantage of different bits of undefined behaviour. Classic problems include overflowing a signed integer, right shifts on negative numbers, modulo on a negative number, type punning for endian checks, etc.
7) I agree with the point about standard 'C' types. However, there are a few extra bits. First, know what the types actually mean (char >= 8-bit, short/int >= 16-bit, long => 32-bit). Don't make any assumptions that aren't in the C spec. You might then want to put typedefs on top of these types to give them a bit more meaning.
8) Don't assume there is a FPU. Be prepared to do fractional maths in fixed point. There will always be float/double types, but they will be so dog slow on some platforms, and the memory cost of bringing in the library for them will be bad.
9) I disagree with UTF-8 for all APIs. Define a string abstraction, so on platforms (or rather, "on windows") you can have UTF-16 strings passed in and operated on without a conversion, and everywhere else you can use UTF-8. Avoid paying the conversion penalty on every operation.
10) Think about memory management. When memory is allocated, the malloc-like thing that is doing it should know what the memory is actually for. On some platforms, you will want to put different allocations into different spots in memory (this is also a nice place for another abstraction, since on some machines there won't be any different types of memory).
11) Don't avoid undefined behaviour by writing a single wrapper function (e.g. "safe signed integer add") and using it everywhere. The behaviour is typically undefined for a good reason (since different platforms will natively want to do it different ways). In the integer add example, you might want to assert that it never overflows, wrap around on overflow, truncate on overflow, return an error if it overflows, etc. Each of these things will have their spot, so don't bunch them all together.
12) Code that might want to be implemented in assembly should live in a small .c files. Then when you go to write it, you can just do it bit by bit (and again, use the build system to tell it how to build it, don't hack it with the preprocessor).
Often, I won't even get to see the compiler for the platform, I'll just write the code, send it to a completely external person, and say "this should compile and run on your platform, I can help if it doesn't".
The core of what I do is audio DSP, so it has to be memory/cycle efficient too, otherwise no one wants to use it.
But as soon as you start developing for these platforms, you then need to build testing tools which also work there, and all the other fun stuff that goes along with it, so it ends up being a lot more than just the core DSP stuff (which is where most of the platform specific stuff comes for me actually, as the core DSP stuff is just memory in memory out type stuff with no side effects).
Agreed. If you need, put the ifdef code in a utility/library. e.g., instead of ifdef'ing your code for pthreads and windows threads, build a simple thread library to abstract the two to a common api.
It's not just right shifts of negative numbers. It's also all shifts by the size of the input or larger.
Yes, when 4 == sizeof(int), shifting an int by 32 may have the same value as shifting by 0. Shifting by 33 may have the same value as shifting by 1. And no, this isn't a signed/unsigned problem.
Also in your example, you should have said sizeof(int)*CHAR_BIT == 32 :)
Even better, use stdint.h:
uint8_t, int16_t, etc. Completely unambiguous to anyone.
So, once you have a working stdint-like thing you then usually want the "least" (or maybe "fast") variants of them instead:
uint_least8_t, int_least16_t etc.
These have the advantage of being actually guaranteed to exist (the exact size variants are only required to exist if the compiler exposes those types, and they are 8, 16, 32 or 64-bits (from memory)). This means that if you are running on a 24-bit chip (e.g. char==short==int==24-bit, long==48-bit) your code will still work good (where not working good could mean that it doesn't compile because it is missing types, or it could mean that it compiles, but is really slow as it is emulating un-naturally sized types)
On the other hand, the type int_least8_t must exist. (as must 16, 32 and 64). However, to do the 64-bit type, you have to be careful, as C90 doesn't require a 64-bit type, so a fallback abstraction usually needs compiler specific tricks ("long long" is a safe bet).
Of course, if you actually wanted a type that is exactly 16-bits wide, then use these types, as that is what they are for... but usually you don't care if it is bigger.
And this is why the normal char/short/int/long types are good: They don't over-specify.
>Rule #4: Use only built in #ifdef compiler flags, do not invent your own
Actually you can separate the source code into src/linux src/w32 src/mac -- but that is just my preference.
>Rule #9: Require all programmers to compile on all platforms
No no no -- build has to take no more than a make command. Anything else is just domed to failure.
IMO the author suggests to develop in a single source tree which compiles on every platform for very good reasons, in particular to minimize duplication and being able to update a single method in a single file to work on all platforms. Your suggested multi-directory approach requires to duplicate most os-specific methods and classes. Depending on the situation one or the other approach might be better, but I think the author has a point in many of them.
> No no no -- build has to take no more than a make command. Anything else is just domed to failure.
Building with just a "make" should be fine on Linux, but for anyone who downloads the source code to a Windows/VS or OSX/XCode machine, it's great being able to just open the project in the standard editor and compile it.
Small matter, easily detected and fixed in the first build, but would be nice to automate somehow.
In any case, I used CMake in one shop I worked in and it was nice to have the build files all managed in one place. We were able to manage builds for Windows, Linux, Mac, and Solaris.
Nice article. Is the GoF Abstract Factory pattern not popular for #5, "developing cross-platform base libraries?"
Make it compilable on your target platforms: PC, Wii, PS3, Xbox, what other is needed.
Don't care about other platforms, until they arrive.
Instead of #ifdef, try to find the pattern in your code, and use select macroses such as:
PC_WIIXBOXPS3(pcValue,consoleValue) or WII_XBOXPS3_PC(wiiValue,xboxOrPs3value,pcValue)
wrap align, threadlocal, etc. as macro-define.
The UNICODE advise is wrong. These system do not have even working locales, so don't trust strupr(), strlwr(), toupper() - etc. to work correctly for your localized game.
I've been using it for the past six years to do most of my development and am quite happy. I am able to develop on my macs and deploy anywhere.
(BTW, what really shocked me about this was that it means some file paths can't be turned into file: URLs! I had assumed you could always do that.)
... and have a build server do this automatically & email the results. Manually building for each platform if there are more than two seems like quite a hassle and you know programmers will skip over it for minor changes.
Which is really the right answer. But it makes the actual cross-platform stuff trivial in many apps.