Blog 6 min read

Learn basic "C" coding rules from open source projects

Share this article
Learn basic "C" coding rules from open source projects

Every project has its own style guide: a set of conventions about how to write code for that project. Some managers choose basic coding rules, while others prefer very advanced ones. In many projects, there are no coding rules at all—each developer uses their own style.

It is much easier to understand a large codebase when all the source code is in a consistent style.

Many resources discuss the best coding rules to adopt. We can learn good coding practices by:

  • Reading a book or magazine.
  • Web sites.
  • From a colleague.
  • Attending a training course.

Another, more interesting approach is to study a well-known, mature open-source project to see how its developers write code. In the "C" world, the Linux kernel could be a good candidate.

For beginner or even intermediate C developers, the Linux kernel may not be easy to dive into. However, the goal is not necessarily to contribute to its source code, but rather to explore how it is implemented.

Let's take a function implementation from the Linux source code as an example.

linux11

The code looks very clean; indeed, the function:

  • Has only a few lines of code.
  • The signature is well-defined.
  • It's well commented.
  • It's well indented.
  • The variable names are very clear.

The same function could be implemented by another developer like this:

linux12

Coding style has a major impact on source code readability. Investing a few hours in developer training and conducting periodic code reviews helps make the code easier to maintain and evolve.

Let's go inside the Linux kernel source code using CppDepend and discover some basic coding rules adopted by its developers.

Modularity

Modularity is a software design technique that increases the extent to which software is composed of separate parts, making modular code easier to manage and maintain.

For a procedural language like C, where logical constructs such as namespaces, components, or classes do not exist, we can achieve modularity by using directories and files.

Here are some possible scenarios:

  • Put all the source files in one directory.
  • Isolate files related to a module or a submodule in a specific directory.

In the case of the Linux kernel, directories and subdirectories are used to modularize the kernel source code.

linux15

Encapsulation

Encapsulation involves hiding functions and data that are internal to an implementation. In C, encapsulation is performed by using the keyword static. These entities are called file-scope functions and variables.

Let's search for all static functions by executing the following CQLinq query.

linux17

We can use the Metric View to get a good idea of how many functions are involved. In the Metric View, the code base is represented through a Treemap. Treemapping is a method for displaying tree-structured data by using nested rectangles. The tree structure used in a CppDepend treemap is the usual code hierarchy:

  • Projects contain directories.
  • Directories contain files.
  • Files contain structs, functions, and variables.

The treemap view provides a useful way to represent the results of a CQLinq query, allowing us to see the affected code elements visually.

linux2

As we can observe, many functions are declared as static.

Let's search now for the static fields:

linux3

The same observation applies to variables: many are declared as static.

In the Linux kernel source code, encapsulation is used whenever functions and variables must be private to the file scope.

Use structs to store your data model

In C programming, functions use variables to carry out their processing; these variables could be:

  • Static variables.
  • Global variables.
  • Local variables
  • Variables from structs.

Each project has its data model, which could be used by many source files. Using global variables is one solution, but not a good one; grouping data into structs is preferable.

Let's search for global variables with a primitive type:

linux4

Only a few variables are involved, and some of them could perhaps be grouped into structs, like (elfcorehdr_addr and elfcorehdr_size) or (pm_freezing and pm_nosig_freezing).

Let functions be short and sweet

Here's, from the Linux coding style web page, some advice about the length of functions:

Functions should be short and sweet, and do just one thing.  They should
fit on one or two screenfuls of text (the ISO/ANSI screen size is 80x24,
as we all know), and do one thing and do that well.

The maximum length of a function is inversely proportional to the
complexity and indentation level of that function.  So, if you have a
conceptually simple function that is just one long (but simple)
case-statement, where you have to do lots of small things for a lot of
different cases, it's OK to have a longer function.

Let's search for functions where the number of lines of code is more than 30.

linux14

Only a few functions have more than 30 lines of code.

Function Number of parameters

Functions where NbParameters > 8 might be painful to call and might degrade performance. An alternative is to provide a structure dedicated to handling argument passing.

linux7

Only two functions have more than eight parameters.

Number of local variables

Functions where NbVariables is greater than 8 are difficult to understand and maintain. Functions where NbVariables is greater than 15 are extremely complex and should be split into smaller functions (except if they are automatically generated by a tool).

linux9

Only 5 functions have more than 15 local variables.

Avoid defining complex functions

Many metrics exist to detect complex functions; NBLinesOfCode, number of parameters, and number of local variables are the basic ones.

There are other interesting metrics to detect complex functions:

  • Cyclomatic complexity is a popular procedural software metric that measures the number of decision paths in a procedure.
  • Nesting Depth is a metric defined for functions that represents the maximum depth of nested scopes within a function body.
  • Max Nested Loops is equal to the maximum level of loop nesting in a function.

The maximum acceptable values for these metrics depend on the team's choices; there are no universal standards.

Let's search for functions that are candidates for refactoring:

linux8

Only very few functions could be considered complex.

Naming convention

There is no universal standard for naming conventions; each project can choose what works best. However, it is very important to follow the chosen convention consistently.

For example, in the case of Linux, structs must begin with a lowercase letter, and we can check if that's true for the whole kernel source code. Let's execute the following query:

linux5

Only 4 structs begin with "_" instead of a lowercase letter.

Indentation

Indentation is very useful for making code easy to read. Here's, from the Linux coding style web page, the motivation behind indentation:

Rationale: The whole idea behind indentation is to clearly define where
a block of control starts and ends.  Especially when you've been looking
at your screen for 20 straight hours, you'll find it a lot easier to see
how the indentation works if you have large indentations.

Now, some people will claim that having 8-character indentations makes
the code move too far to the right, and makes it hard to read on a
80-character terminal screen.  The answer to that is that if you need
more than 3 levels of indentation, you're screwed anyway, and should fix
your program.

Conclusion

Exploring well-known open-source projects is always a good way to improve your programming skills. No need to download and build the project — you can just discover the code on GitHub, for example.

Share this article