© John T. Taylor, Wayne T. Taylor 2021
J. T. Taylor, W. T. TaylorPatterns in the Machinehttps://doi.org/10.1007/978-1-4842-6440-9_12

12. File Organization and Naming

John T. Taylor1   and Wayne T. Taylor2
(1)
Covington, GA, USA
(2)
Golden, CO, USA
 

Source code file organization is very often done organically. That is, developers start writing code and organizing their files based on their immediate needs, which at the beginning of a project usually do not include unit tests or code reuse. Consequently, later in the project when code reuse for unit tests and functional simulators becomes an issue, changing these now rigid source code directory structures can be very painful.

Project and company-wide file organization planning that is done early and deliberately is a strategic best practice. A well-thought-out, consistent file organization not only facilitates code reuse and the creation and maintenance of unit tests but it is also critical to your ability to build multiple images and executables from your code base.

Organizing Files by Namespace

Here is PIM’s recommended approach to file organization:
  • Organize your files by component dependencies, not by project. That is, do not create your directory structure to reflect a top-down project decomposition. Rather, organize your code by namespaces where nested namespaces are reflected as subdirectories in their parent namespace directory. By having the dependencies reflected in the directory structure, it is a quick and visual sanity check to avoid undesired and cyclical dependencies.

The C programming language standard does not support namespaces. However, the concept of namespace can still be implemented with C by applying the following convention: with functions, types, enums, variable names, preprocessor symbols, etc., that appear in header files, prefix the namespace on the names. For example, for a hypothetical function hello_world() in the Foo::Bar namespace, the function name would be Foo_Bar_hello_world(). The hello_world() function definition would be in a header file in the directory Foo/Bar/. The prefixing rule does not apply to symbols that are exclusive to a single .c file (e.g., file static functions and variables).

For example, a quick review of the #include statements of your project might show incorrect dependencies for the following use cases:
  • An application-independent driver that is including application-specific header files.

  • A file that is including a header file from a direct subdirectory. Parent namespaces should never depend on child or nested namespaces. By doing so, you create a circular dependency.

    Cyclical dependencies manifest themselves in header file #include race conditions. That is, the compile fails because of the order of the #include statements. There should never be a required or magic order for your #include statements.

  • Place header files and their corresponding .c|.cpp files in the same directory. Do not create separate source/ and include/ directories.

  • Create non-namespace directories for organizational purposes. However, for this use case, I recommend that non-namespace directories be easily recognized as such. That is, start with a leading underscore. For example, the PIM example code uses the convention of one _0test/ directory per namespace to group together the unit test files for a namespace (e.g., StormComponent\_0test, StormDm\_0test, etc.)

  • Reference header files with a full path relative to the root of your source. This means that your #include for header files will contain the namespace information for each header file.

    By doing this, file names (header and .c|.cpp files) do not have to be globally unique. The file names only need to be unique within a given directory and namespace. Directories and namespaces are your friends when it comes to naming because they provide a simple mechanism for avoiding future naming collisions. Having to refactor code to fix a naming conflict is a result of tactical thinking.

Note

Not requiring globally unique .c|.cpp file names can potentially have an impact on your build scripts. It is not uncommon for build scripts and makefiles to put all of the generated object files into a single directory. (The default QT1 build scripts are an example of this.) Make sure you construct your build scripts in a way that path information about where a source code file is located is not lost when generating and referencing derived objects.

Here is an example of organizing by namespace from the PIM example code. The following namespaces are organized into the directory structure shown in Figure 12-1. Non-namespace directories are identifiable by names that start with a leading underscore (“_”).
  • Storm

  • Storm::Component

  • Storm::Component::Equipment

  • Storm::Component::Equipment::Stage

  • Storm::Dm

  • Storm::Thermostat

  • Storm::Thermostat::Main

  • Storm::Thermostat::SimHouse

  • Storm::TShell

  • Storm::Type

  • Storm::Utils

../images/499401_1_En_12_Chapter/499401_1_En_12_Fig1_HTML.png
Figure 12-1

Directory tree for namespaces

Here is an example of the Storm/Component/AirFilterMonitor.h and Storm/Component/Equipment/Cooling.h header files with #include statements that contain the full namespace path:
File: Storm/Component/AirFilterMonitor.h
#include "Storm/Component/Base.h"
#include "Storm/Dm/MpSimpleAlarm.h"
#include "Storm/Dm/MpVirtualOutputs.h"
#include "Cpl/Dm/Mp/Uint32.h"
#include "Cpl/Dm/Mp/ElapsedPrecisionTime.h"
File: Storm/Component/Equipment/Cooling.h
#include "Storm/Component/Control.h"
#include "Storm/Component/Equipment/StageApi.h"

Organizing External Packages

While the previous section discussed best practices for organizing your source code, it did not mention how to manage the file organization for external source code. External source code (or packages) can be anything from purchased commercial software to open source projects to existing in-house code bases and libraries. Using external packages on your projects is a good thing; it is code reuse in action. However, it is also surprisingly difficult to do it right if you’re not thinking strategically. If there is one and only one version of each external package over the entire life of your project (including maintenance and support), then it is easier. But if you have to manage different versions of external packages over time, or different versions concurrently, then things get messy, especially if you’re trying to adhere to the OCP for both your source code and build scripts. (And PIM does consider build scripts to be “source code” when it comes to the OCP.)

Unfortunately, I don’t have a best practice recommendation to solve this problem. I have a solution that I have used on many projects over the course of my career, but it is fundamentally flawed because it only works with projects that are infrequently released, which is typical for embedded projects. This infrequent release cadence can be attributed to the fact that releases of embedded software usually coincide with new hardware releases, which take longer to turn around than software releases.

Note

For a more in-depth discussion of how to manage external source code packages, see the “Outcast” section of Appendix E. This appendix also goes into a proposed, albeit work in progress, solution for package management that does not violate the OCP.

My solution is to include an xsrc/ directory that is a sibling to the src/ directory. Under the xsrc/ directory, there is a top-level subdirectory for each external package. The example source code on PIM’s GitHub repository is organized this way. Figure 12-2 shows a simplified directory listing for the PIM repository.
../images/499401_1_En_12_Chapter/499401_1_En_12_Fig2_HTML.png
Figure 12-2

xsrc tree listing from the PIM GitHub repository

However, here’s where things break down:
  • How do you handle different or concurrent versions of packages?

  • How are the different versions realized under the xsrc/ directory?

  • What is the impact to the build scripts?

  • What is the impact to #include statements in your in-house code?

  • How do the #include statements in your in-house code reference files in an external package?

  • Does one or more compiler search path options (-I) need to be added to the build script for each external package?

  • Are your #include statements that reference external packages relative to the xsrc/ directory? If yes, what happens if you have multiple versions of the same package under the xsrc/ directory tree?

Naming

Just like file organization, naming conventions are a strategic best practice. PIM recommends that you define naming conventions that, at a minimum, provide protection against name collisions. This includes source code entities (class names, functions, preprocessor symbols, etc.) as well as file and directory naming conventions. The strategic goal for your naming conventions is to prevent future name collisions and to facilitate future source code reuse.

I worked on a telecom project that used a well-known RTOS for its operating system. Since our target hardware was not one of the prepackaged BSPs, we had to create our own BSP using the RTOS vendor’s SDK. We kept having intermittent failures that were tracked down to our BSP, but we couldn’t isolate the actual root cause. Finally, we discovered that the vendor’s SDK contained a preprocessor macro called m_data, and we also had a variable in our custom BSP code named m_data. The net result was that while our BSP code compiled and linked, the runtime behavior was incorrect because of the macro expansion of the vendor’s m_data macro. This is an example of a worst-case scenario of naming collisions. Typical naming collisions result in compile and link errors. However, fixing any naming collision requires modifying existing source code files, which PIM tries very hard to avoid.

Defining naming conventions will also draw you into a style conversation: uppercase vs. lowercase, camel case vs. snake case, and so on. This means that you will never get 100% agreement by the team members on a universal style for the project. Nevertheless, solicit input from the team when coming up with your naming conventions; then pick a single style and stick with it.

Naming Recommendations for C++

PIM’s naming recommendations are slightly different depending on whether you are using C++ or C. Here are the naming recommendations for C++:
  • Use namespaces and map your directory structure to your namespace structure (see “Organizing Files by Namespace” earlier).

  • Namespaces and directories should have the same name and case. File names should have the same name and case as the primary class in the file. This simplifies finding source code files when you start with a symbol in the code. For example, using this convention, it would be simple to find the source code file that contains the method: Storm::Component::Equipment::IndoorHeating::reset().

  • It would be in the file:

    src/Storm/Component/Equipment/IndoorHeating.h|.cpp.
  • Typedefs and enums need to be encapsulated with namespaces or classes. Here is an example of how you can encapsulate an enum in a class:
    class Foo
    {
    public:
        /// Magic values for balance status
        enum Balance_T { eLEFT=-1, eEVEN=0, eRIGHT=1 };
    ...
    };
  • All preprocessor symbols in a header file should be prefixed with associated namespace and file names, for example:

    File: src/Storm/Component/OperatingMode.h
    #ifndef Storm_Component_OperatingMode_h
    #define Storm_Component_OperatingMode_h
    ...
    /** This constant defines the negative cooling offset (in degrees
        'F) for preferring cooling operating mode over heating mode
     */
    #ifndef OPTION_STORM_COMPONENT_OPERATING_MODE_COOLING_OFFSET
    #define OPTION_STORM_COMPONENT_OPERATING_MODE_COOLING_OFFSET 1.0F
    #endif
    ...
    #endif  // end header latch
    File: src/Cpl/System/Trace.h
    #define CPL_SYSTEM_TRACE_MSG(sect, var_args)   do { … } while(0)

Naming Recommendations for C

The C programming language standard does not support namespaces. However, the abstract concept of namespace can still be applied to C code. Conceptual namespaces are implemented as directories in your source code, and, in turn, this directory and file structure can be prefixed to all of the symbols in your header files. But this only applies to symbols in header files. For variables, functions, and macros that are exclusively scoped to a single file, you do not need to append any additional text.

Here is a hypothetical example of what a C implementation might look like. The Storm::Component::Equipment::IndoorHeating class would be defined as a set of data structures and functions in a file called src/Storm/Component/Equipment/IndoorHeating.h, which contains information that would look something like this:
#ifndef STORM_COMPONENT_EQUIPMENT_INDOORHEATING_H
#define STORM_COMPONENT_EQUIPMENT_INDOORHEATING_H
#include "Storm/Component/Control.h"
#include "Storm/Component/Equipment/StageApi.h"
/// Internal data for the Indoor Heating module
typedef
{
    StormComponentEquipement_StageApi_T* stage1;
    StormComponentEquipement_StageApi_T* stage2;
    StormComponentEquipement_StageApi_T* stage2;
} StormComponentEquipement_IndoorHeating_T;
/// Initialize the module
StormComponentEquipment_IndoorHeating_initialize
(
StormComponentEquipement_StageApi_T* instanceMemory,
...
);
/** This method will be called on a periodic basis (as determined by
    the calling Control Component instance) to perform active
    conditioning.
 */
bool StormComponentEquipment_IndoorHeating_executeActive
(
StormComponentEquipement_StageApi_T*    thisPtr,
StormComponentEquipment_Control_Args_T* args
);
...
#endif  // end header latch

Yes, the naming is very verbose in C. But, as I work on more C projects than C++, I can say from experience that you (and the team) will get used to it quickly. Additionally, with modern IDE’s auto-completion features, there is not as much extra typing as you might think.

As in the preceding C++ example, all preprocessor symbols in a header file should be prefixed with their associated namespace and file names. This also includes enum values, because enum symbol values in C (defined in a header file) have a global scope.

For example, here is a problematic implementation that you will want to avoid:
/// Colors
typedef enum
{
eRED,          //!< eRED is a global symbol
eGREEN,
eBLUE
} Foo_Bar_Colors_T;
Instead, for the source file src/Foo/Bar.h, you should implement the enum as follows:
/// Colors
typedef enum
{
eFOO_BAR_COLORS_RED,    // eFOO_BAR_COLORS_RED is a global symbol,
                        // but has a unique name
eFOO_BAR_COLORS_GREEN,
eFOO_BAR_COLORS_BLUE
} Foo_Bar_Colors_T;
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.234.83