Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

In my work, I end up writing code that compiles on a lot more platforms than that. These are my thoughts, I suspect I work at a much lower level than the author, so the disagreements I have are likely just because of that:

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).



>> First, know what the types actually mean (char >= 8-bit, short/int >= 16-bit, long => 32-bit) ... You might then want to put typedefs on top of these types to give them a bit more meaning.

Even better, use stdint.h:

uint8_t, int16_t, etc. Completely unambiguous to anyone.


Yes, however to get these, you are now in C99 land, which excludes you from some compilers (the obvious one is the MS compiler). What we do is define our own compiler abstraction which can use stdint.h on C99 compilers, and typedef standard types into C99 versions for other compilers.

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.


what sort of programs are you working on that must run on platforms that lack a C++ compiler and need to be portable? (I am legitimately curious)


Tiny embedded things from people you've never heard of.

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).



Probably embedded - many more constraints than desktop environments. ANd their C++ support is usually problematic.


Yes. But I suppose the thing about embedded stuff is that if you write good "embedded" code (whatever that means), then it works well everywhere. If you write good "desktop" code, then it probably won't work well on an embedded system. So, when I'm doing my own hobby projects which are never going to go onto hardware, I still write it as if it might end up on a whacky chip one day, because when you do it this way, C seems to smile on you, and have exactly all the tools needed to express what you mean without saying more than you mean.


-Avoid #ifdefs.

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.


> Classic problems include overflowing a signed integer, right shifts on negative numbers, modulo on a negative number, type punning for endian checks, etc.

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.


Yes, that is another classic one. And this one I think will hit people even when they aren't doing embedded stuff. I seem to recall a compiler optimisation which would make the assumption that the shift count would be well defined, and so it could then do some other optimisation (e.g. maybe that shift count was then used in a loop later, you can then assume that the loop runs less than 31 times)

Also in your example, you should have said sizeof(int)*CHAR_BIT == 32 :)




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: