I was working on an embedded program using C.
开发者_Go百科There are tons of hardware macros like
#ifdef HardwareA
do A
#endif
It's not readable, and hard to cover all the different paths with unit tests.
So, I decided to move the hardware related code to arch folders, and using macros in the makefile to decide which arch folder is linked. Like in the Linux kernel code.
But when I saw the Linux kernel, I noticed there are so many duplicates in the arch folders.
How do they make the changes to all related hardware when a bug was found in one hardware, but might affect all others?
I think doing this way will inevitably bring duplicates into the code base.
Does anyone have experience with this type of problem?
How to unit test on code which has lots of hardware macros?
Refactoring the code to move hardware macros off source code?
It sounds like you are replacing a function like this:
somefunc()
{
/* generic code ... */
#ifdef HardwareA
do A
#endif
/* more generic code ... */
}
with multiple implementations, one in each arch folder, like this:
somefunc()
{
/* generic code ... */
/* more generic code ... */
}
somefunc()
{
/* generic code ... */
do A
/* more generic code ... */
}
The duplication of the generic code is what you're worried about. Don't do that: instead, have one implementation of the function like this:
somefunc()
{
/* generic code ... */
do_A();
/* more generic code ... */
}
..and then implement do_A()
in the arch folders: on Hardware A it has the code for that hardware, and on the other hardware, it is an empty function.
Don't be afraid of empty functions - if you make them inline
functions defined in the arch header file, they'll be completely optimised out.
Linux tries to avoid code duplicated between multiple arch directories. You'll see the same functions implemented, but implemented differently. After all, all architectures need code for managing the page tables, but the details differ. So they all have the same functions, but with different definitions.
For some functions, there are CONFIG_GENERIC_*
defined by the build system that replace unnecessary architecture hooks with generic versions as well (often no-ops). For example, an arch without a FPU doesn't need hooks to save/restore FPU state on context switch.
This kind of #ifdef
hell is definitely to be avoided, but naturally you also want to avoid code duplication. I don't claim this will solve all your problems, but I think the single biggest step you can make it changing your #ifdef
s from #ifdef HardwareX
to #ifdef HAVE_FeatureY
or #ifdef USE_FeatureZ
. What this allows you to do is factor the knowledge of which hardware/OS/etc. targets have which features/interfaces out of all your source files and into a single header, which avoids things like:
#if defined(HardwareA) || (defined(HardwareB) && HardwareB_VersionMajor>4 || ...
rendering your sources unreadable.
I tend to move the hardware specific #defines into one header per platform, then select it in a "platform.h" file, which all source files include.
platform.h:
#if defined PLATFORM_X86_32BIT
#include "Platform_X86_32Bit.h"
#elsif defined PLATFORM_TI_2812
#include "Platform_TI_2812.h"
#else
#error "Project File must define a platform"
#endif
The architecture specific headers will contain 2 things.
1) Typedefs for all the common integer sizes, like typedef short int16_t;
Note that c99 specifies a 'stdint.h' which has these predefined. (Never use a raw int
in portable code).
2) Function headers or Macros for all the hardware specific behavior. By extracting all the dependencies to functions, the main body of code remains clean:
//example data receive function
HW_ReceiverPrepare();
HW_ReceiveBytes(buffer, bytesToFetch);
isGood = (Checksum(buffer+1, bytesToFetch-1) == buffer[0])
HW_ReceiverReset();
Then one platform specific header may provide the prototype to a complex HW_ReceiverPrepare()
function, while another simply defines it away with #define HW_ReceiverPrepare()
This works very well in situations like the one described in your comment where the differences between platforms are usually one or two lines. Just encapsulate those lines as function/macro calls, and you can keep the code readable while minimizing duplication.
精彩评论