This discussion has been locked.
You can no longer post new replies to this discussion. If you have a question you can start a new discussion

The impact of one huge header file

Hello,
Somebody here insists that all the program's definitions will be stored in one large header file that all C modules include, rather that keeping the declarations distributed among specific header files. That sounds pretty crappy to me - but how bad it is really? will it have a very negative impact on build time?

  • I meant, of course,
    "Somebody here insists that all the program's declarations..."

  • I do agree with you on that!

    "will it have a very negative impact on build time?"

    If every source file depends on this one header file, then any change to the header will cause all the source files to be rebuilt - even if the changes have no effect on most of the modules.

    Thus, effectively, every build becomes a full build!

    Also, most tools do have some finite file size limit - so if the header is really huge, you might blow that limit...

    There might also be limits on the numbers of symbols that the compiler and/or preprocessor can cope with in a single "translation unit"...

    The biggest practical issue that I see is where there is more than 1 programmer on the team: if there is only 1 header file, then everyone will be constantly fighting to make their changes to the one file!

    The major objection, however, is a "philosophical" one:
    One of the underpinning principles of block-structured languages is the facility to isolate the "scope" of things to just the area of code where they are really relevant.

    In the jargon, modules should be "loosely coupled".

    Using 1 massive global header, of course, totally destroys this!

  • Build time is affected by a lot of factors.

    Is your program large?
    Do you have a slow computer?
    Do you use maximum optimization?
    Do you regularly add global variables or change data types or function signatures, requiring all affected source files to be recompiled?

    In my case, I have one header file that brings in all general stuff and then require other source files to include some module-specific headers - for example all code requiring SPI has to include a header file with the SPI transfer functions declared.

    If I change my "general.h" file, every single C file in the project needs to be recompiled. But either my projects are too small, or my computer fast enough (I'm desperately waiting for a new one), but the build times are quite short.

    However, I would not like to have everything in a single file because that file would be way too large and hard to handle. If your source files may only include one custom header file, that header file should then include new header files for the different modules and devices. Having a huge header file would also be bad for version control - yes, CVS et al can merge changes from multiple developers. However, the file will get a very long history of changes and I like to be able to quickly get an overview who has changed what and when. CVS annotate/Subversion blame would also produce a too long list to scan.

    If we return back to the SPI stuff again. I have one spi header file dedicated to all the processor-specific constants (the Keil file only defines the device registers, not all the flags for each register) and a second header file with the names and data types of every symbol the SPI module export. The SPI implementation will require both header files, while other parts of the program will only need the header file with exported symbols, and their data types.

  • Hi Per,

    "
    and a second header file with the names and data types of every symbol the SPI module export. The SPI implementation will require both header files, while other parts of the program will only need the header file with exported symbols, and their data types.
    "

    I don't understand things about the second header file. If it is possible, would you please provide a tiny example?

    -> rtc1.h
    #define IMSEC           0x00000001
    #define IMMIN           0x00000002
    #define IMHOUR          0x00000004
    ( How to export a symbol here? )
    ( EXPORT_SYMBOL(IMHOUR)? )
    
    -> rtc2.h
    extern IMHOUR
    

  • It actually sounds like you don't understand about header files in general - is that right?

    -> rtc1.h
    #define IMSEC           0x00000001
    #define IMMIN           0x00000002
    #define IMHOUR          0x00000004
    ( How to export a symbol here? )
    ( EXPORT_SYMBOL(IMHOUR)? )
    
    -> rtc2.h
    


    How you "export" a symbol there is unaffected by how many header files you have - the method is always the same!

    In fact, you don't actually have to explicitly export anything in 'C'!
    In 'C', everything defined at file scope is automatically "Public" (ie, it is automatically exported) - unless you specifically make it "Private" (by using the static keyword)

    Therefore, 'C' header files are really about importing stuff - rather than exporting.

    The 'C' keyword to "import" an externally-defined symbol is extern; eg,

    // my_header.h
    
    extern void external_function( void ); // Import an external function
    
    extern int external_int;               // Import an external int "variable"
    

    Note that the extern is not strictly necessary for the function, but it's often helpful to put it there for completeness - see: c-faq.com/.../extern.html

    This is standard textbook stuff - nothing specific to Keil or embedded.

    See also: c-faq.com/.../decldef.html

    And, in fact, the whole of http://c-faq.com

  • I'm at home today, so no access to real Keil code.

    Anyway, I put together some demo examples - not compileable since it is incomplete and I invented from memory :)

    A general file "general.h" that is included by every single source file.

    /**
     * \file general.h
     * \brief Project-specific main include file
     *
     * Handles global constants and data types, and some project configurations
     */
    
    enum {
        VERSION_MAJOR   = 1,    ///< Stepped on incompatibility changes or big
                                ///< rewrites. 0 before initial release.
        VERSION_MINOR   = 0,    ///< Stepped on additions of new functionality.
                                ///< Initial value: 0 whenever major is stepped.
        VERSION_FIX     = 1,    ///< Stepped for bug-fixes.
                                ///< Initial value: 1 whenever major or minir is
                                ///< stepped.
    };
    
    // Some quick-to-modify global configuration options.
    enum {
        COM0_BAUD       = 19200,    ///< Baud-rate for monitor port.
        COM0_RX_BUFFER  = 256,      ///< Must be 2^n
        COM0_TX_BUFFER  = 8192,     ///< Must be 2^n
        COM1_BAUD       = 9600,     ///< Baud rate for RFID port.
        COM1_RX_BUFFER  = 32,       ///< Must be 2^n
        COM1_TX_BUFFER  = 32,       ///< Must be 2^n
    
        CPU_CRYSTAL     = 20,       ///< Crystal frequency in MHz.
        CPU_CORE_SPEED  = 48,       ///< Core speed in MHz
        CPU_PCLK        = 12,       ///< Default peripherial clock 12 MHz
    
        SSP0_PCLK       = 12,
        SSP0_RX_BUFFER  = 64,       ///< Must be 2^n
        SSP0_TX_BUFFER  = 64,       ///< Must be 2^n
    };
    
    #define SSP0_EXTRA_DEBUG 0
    #define SSP1_EXTRA_DEBUG 0
    #define ADC0_EXTRA_DEBUG 0
    #define FFT_EXTRA_DEBUG 0
    #define REGRESSION_BUILD 0
    
    // Maybe we don't have stdint.h support available from the compiler...
    typedef unsigned char uint8_t;
    typedef unsigned short uint16_t;
    typedef unsigned int uint32_t;
    typedef unsigned long long uint64_t;
    
    // Capabilities may vary depending on mounted variant.
    #if TARGET_CPU==2364
    #  define TARGET_LPC2364
    #  include <lpc23xx.h>
    #  include "lpc2364.h"
    #elif TARGET_CPU==2366
    #  define TARGET_LPC2366
    #  include <lpc23xx.h>
    #  include "lpc2366.h"
    #else
    #  error "Target CPU not defined"
    #endif
    
    // Capabilities and pin allocations may vary between hardware revisions.
    #if !defined(TARGET_REVISION)
    #  error "No target revision defined"
    #elif TARGET_REVISION==0
    #  include "defines_hwrev_0.h"
    #elif TARGET_REVISION==1
    #  include "defines_hwrev_1.h"
    #else
    #  error "Unknown target revision"
    #endif
    
    // Some includes every module should have.
    #include "config.h"
    
    // Some global variables.
    extern volatile uint32_t uptime_1s;     ///< Uptime in 1s resolution.
    extern volatile uint32_t uptime_1ms;    ///< Uptime in 1ms resolution.
    
    // Some misc functions generally available.
    int mon_printf(const char* fmt,...);
    const char* get_version_string(void);
    uint32_t get_app_area_crc(void);
    uint32_t get_config_area_crc(void);
    uint16_t crc16(uint16_t crc,const uint8_t data[],unsigned size);
    uint32_t crc32(uint32_t crc,const uint8_t data[],unsigned size);
    

    I may have a target-specific header file ssp_23xx.h that defines constants for the SSP (NXP LPC23xx name for the high-speed SPI):

    /**
     * \file ssp_23xx.h
     * \brief Register descriptions for NXP LPC23xx SSP device.
     */
    
    /// \brief Flags for the SSPxCR0 register
    enum {
        SSPCR0_DSS_4BIT         = 0x0003,   ///< 4-bit transfers
        ...
        SSPCR0_DSS_16IT         = 0x000f,   ///< 16-bit SPI transfers
        SSPCR0_FRF_SPI          = 0x0000,   ///< Frame Format: SPI
        SSPCR0_FRF_TI           = 0x0010,   ///< Frame Format: TI
        SSPCR0_FRF_MICROWIRE    = 0x0020,   ///< Frame Format: Microwire
        SSPCR0_CPOL_IDLE_LOW    = 0x0000,   ///< Keep clock low in idle
        SSPCR0_CPOL_IDLE_HIGH   = 0x0040,   ///< Keep clock high in idle
        SSPCR0_CPHA_LEADING     = 0x0000,   ///< Capture data on leading flank of clock
        SSPCR0_CPHA_TRAILING    = 0x0080,   ///< Capture data on trailing flank of clock
        SSPCR0_SCR_SHIFT        = 8,        ///< Bit offset of this field
        SSPCR0_SCR_MASK         = 0xff00    ///< Mask to extract this field
    };
    
    /// \brief Flags for the SSPxCR1 register
    enum {
        SSPCR1_LBM_OFF          = 0x0000,   ///< Loop-back mode off
        SSPCR1_LBM_ON           = 0x0001,   ///< Loop-back mode on
        SSPCR1_SSE_DISABLED     = 0x0000,   ///< SSP controller disabled
        SSPCR1_SSE_ENABLED      = 0x0002,   ///< SSP controller enabled
        SSPCR1_MS_MASTER        = 0x0000,   ///< Master/Slave: Master
        SSPCR1_MS_SLAVE         = 0x0004,   ///< Master/Slave: Slave
        SSPCR1_SLAVE_OUTPUT_ON  = 0x0000,   ///< Slave side read/write - driving MISO line
        SSPCR1_SLAVE_OUTPUT_OFF = 0x0008,   ///< Slave side read-only - not driving MISO line
    };
    

    And then a project-specific file ssp0.h that declare accessor functions. The level of the functions may vary from project to project. Some projects have raw read and write functions, while some projects are using buffered (possibly filtering) API functions, or even a complete protocol manager that take message data as input and transparently converts into device-transmittable packets:

    /**
     * \file ssp0.h
     * \brief Accessor functions for SPI communication on ssp0.
     */
    
    // Some statistics
    extern uint32_t ssp0_irq_count;     ///< # of SSP0 interrupts.
    extern uint32_t ssp0_tx_count;      ///< # of transmitted bytes.
    extern uint32_t ssp0_rx_count;      ///< # of received bytes.
    extern uint32_t ssp0_rx_overflows;  ///< # of detected RX FIFO overflows.
    
    void ssp0_init(void);
    void ssp0_deinit(void);
    void ssp0_send(uint8_t v);
    void ssp0_send_block(uint8_t data[],unsigned count);
    unsigned ssp0_idle(void);
    unsigned ssp0_have_rx_data(void);
    int ssp0_recv(void);
    int ssp0_recv_block(uint8_t data[],unsigned max_count);
    

    Then I have the implementation of the support for the SSP0 device:

    /**
     * \file ssp0.c
     * \brief Implements ssp0 functionality
     */
    
    #include "general.h"        // Some eneric data types, debug flags, ...
    #include "ssp_23xx.h"       // Processor-specific declarations for the SSP device
    #include "ssp0.h"           // Exported declarations for this module
    
    // Some configuration options for this module
    enum {
        SSP0CR0_SCR = xxx;      ///< What bit-rate to run the device in.
    };
    
    void ssp0_init(void) {
        // Should run in 16-bit mode SPI with clock idle low and sampling of falling edge.
        // Bit-rate defined at top of file.
        SSP0CR0 = SSPCR0_DSS_16BIT | SSPCR0_FRF_SPI | SSPCR0_CPOL_IDLE_LOW | SSPCR0_CPHA_TRAILING | SSP0CR0_SCR;
    
        // Run as master
        SSP0CR1 = SSPCR1_LBM_OFF | SSPCR1_SSE_DISABLED | SSPCR1_MS_MASTER;
    
        ...
    
        SSP0CR1 |= SSPCR1_SSE_ENABLED;  // Fully initialized - enable the device
    }
    

  • In the olden days with slow computers and paper tapes (or whatever) this was a huge issue.

    Today, who cares if a build takes 3 or 5 seconds.

    I have no idea what the effect is when using uVision, but with commandline build a total build of a 20 file project is about 5 seconds.

    Erik

  • The IDE should not matter in this case. Todays compilations are almost fully controlled by the amount of optimization. For some tools, the link step can also take a lot of time if a project is big and has debug information - especially C++ can produce huge amounts of debug information.

    Remember that you don't build with high optimization levels :)

  • "Today, who cares if a build takes 3 or 5 seconds."

    We used to have some bloke here with a poster that stated something along the lines of:

    "Remember guys, the faster your computer, the bigger your ....."

    (I'll let you fill in the missing word.)

  • "Today, who cares if a build takes 3 or 5 seconds."

    True - I don't think build time is the big issue here.

    I think the issues of modularity, etc, are for more important...

  • Hi Andy and Per,

    Many thanks to your help.

    There are still a lot of things that I need to learn, hope one day I can become a better programmer.

  • I didn't know the difference between enum and #define. So I did some study. Though it might be some kind of common sense for most people here, I think it is not bad to put an URL about "Enum Vs #define" here.

    http://www.keil.com/forum/docs/thread7149.asp

  • Sorry for any confusion, but I normally always use enum unless the intended target is the preprocessor (conditional compilation), or I declare a non-integer constant. enum can't be used for strings or floating point...

  • Ah, Sausage, a blok I used to work with hung this slogan on his monitor:

    "wie stabiliteit wilt, kan geen schoonheid verwachten"

    which means:

    "The one that wants stability, cannot expect beauty"

    It was funny that it was printed with these ornaments positioned around the inscription, like it was some kind of a religious decree [is it? I bet "Jack Sprat" knows :-) :-) ]