EmbeddedRelated.com
Blogs

C Is for Complacency: Friendship Is Not Transitive, but What About Module Dependencies?

Jason SachsJune 20, 2026

Today we’re going to take a look at a quirk of header files in the C programming language, and what you can do to reduce coupling between modules. Well, actually, there are no modules in C, except for C++20, where they grafted an import/export module concept onto the language, kind of like trying to graft a garden hose between someone’s nose and their lungs to prevent choking while swallowing. Breathe easy!

First, though, I’ll let you all in on a little secret: I hate C.

(This isn’t originally the way I wanted to say it. I had other plans.)

This article is available in PDF format for easy printing

Why I Hate C

I’ve been programming in C for 35 years, professionally for most of that time. It got me my first paying job. It’s essential on most embedded systems.

But we deserve better! C is a 50-year old language that is showing its age. It dates back to the days of minicomputers, acoustic modems, and dot-matrix printers, and is filled with conventions of software engineering from the 1970s that are no longer appropriate constraints for today.

Modern programming, for the most part, has moved on — and with fast computers and the Internet we have lots of alternatives: Java, JavaScript, C#, D, Python, Lua, Ruby, Rust, Go, Swift, TypeScript… the list goes on and on. Most of these are “high-level languages” — meaning you get certain features for free, like memory management and built-in data structures and libraries, that relieve you of the burden of worrying about them. But the “free” features come at a cost: they require a bytecode interpreter, or garbage collection, or at the very least a large runtime collection of libraries and facilities. No problem if you’re running them on a modern processor with gobs of memory. But there are still embedded microcontrollers with 64 kilobytes or less of RAM, and throwing runtime abstraction layers around just isn’t an option; many high-reliability hard real-time systems also can’t afford to use dynamic memory allocation. So you probably won’t see many commercial products with microcontrollers running JavaScript, for example, because it’s too much to fit in a low-cost microcontroller, and it isn’t an efficient use of computing power. The exceptions in my list above are Rust and Go (well, TinyGo), which compile down to machine language, and have some potential in embedded systems that need to support “bare metal” programming.

So we have C, and C++, and Rust, and Ada, and a few more obscure languages that could conceivably support bare-metal programming on embedded systems (in alphabetical order: Go via TinyGo, Nim, Odin, V, Zig). The deeper you get into the obscure list, the more difficult it is to find elements of the software ecosystem: compilers, debuggers, libraries, software engineers. C++/Rust/Ada are all well-supported on most ARM devices. But stray from the ARM processor world, and you’re often left with C as the only option. (If you’re lucky, you get a C++ compiler.) And therefore C is the least-common-denominator of the embedded systems world, 1970s warts and all.

I do hope that one of these other languages takes off and eclipses C, but I’m not holding my breath.

Until then: I’ll be shining a light on the warts from time to time.

Here we go!

In a Nutshell

We’re going to talk about three concepts in this article:

  • modules
  • dependency
  • transitivity

Software modules (from the concept of modular programming) are supposed to be these nice self-contained units of software, each with one coherent purpose, a well-defined interface, and an isolated implementation. Contrast with spaghetti code or “big ball of mud”.

For example, you might have a UART on your microcontroller, and someone writes a driver module in C with a well-defined interface that looks like this:

// uart.h: UART interface

#ifndef UART_H
#define UART_H

#include <stdint.h>
#include <stdbool.h>

typedef struct tagUART {
    int            baud_rate;
    volatile void *pregisters;
} UART;

/* Initialize UART */
void UART_init(UART *puart, const char *port, int baud_rate);

/* Write a byte to the UART. Block until accepted. */
void UART_write(UART *puart, uint8_t byteout);

/* Read a byte from the UART. Block until read. */
uint8_t UART_read(UART *puart);

/* Is a byte available to read from the UART? */
bool UART_read_ready(const UART *puart);

#endif // UART_H

The interface here is a data definition (UART) with some functions that operate with the data definition to do things, like initialize the UART to communicate on some desired port with some desired baud rate, read or write a byte, and tell if the UART is ready to read. Note that there are no implementation details shown here! Somewhere (likely in uart.c) there is an implementation that handles those details. We could replace that implementation with a different one that keeps the same interface, and our code should still operate correctly.

Interfaces are as simple and targeted as possible… at least in theory.

Dependency in software is all about references. Here there are two: stdint.h and stdbool.h, both header files in the C standard library as of C99 — in order to compile a file that includes uart.h, we need stdint.h and stdbool.h. Dependency is best viewed as a relation between two things (software modules or files in this case); we could represent it by the symbol \( \rightsquigarrow \), so that \( A \rightsquigarrow B \) means that A depends on B. In this case:

  • uart.h \( \rightsquigarrow \) stdint.h
  • uart.h \( \rightsquigarrow \) stdbool.h

A relation is transitive if it chains together two relations with an intermediate element in common: for example, if \( A > B \) and \( B > C \) then we know that \( A > C \).

The question for today is whether software dependency is transitive: if \( A \rightsquigarrow B \) (A depends on B) and \( B \rightsquigarrow C \) (B depends on C), does that mean \( A \rightsquigarrow C \) (A depends on C)?

Friendship Is Not Transitive

Some of you may remember from mathematics that binary relations can have the reflexive, symmetric, and/or transitive properties. For example, equality has all three of these properties:

  • Reflexive property — for any element \( x \), \( x = x \).
  • Symmetric property — if \( x = y \), then \( y = x \).
  • Transitive property — if \( x = y \) and \( y = z \), then \( x = z \).

We can make a table of some well-known relations, including the six common numerical relations and the subset \( \subset \) and superset \( \supset \) operators:

Relation Reflexive Symmetric Transitive
\(=\)
\(\not =\)
\(<, >\)
\(\le, \ge\)
\(\subset, \supset\)
\(\subseteq, \supseteq\)
friendship
acquaintance
IRS dependency n/a
software dependency

Wait, what?

Let’s take these last four one by one.

Antisocial Networks

Friendship:

  • Reflexive: Is everyone friends with themselves? Hmm, probably not.
  • Symmetric: If A is friends with B, does that mean B is friends with A? Yes. (We’re not including cases where A thinks they are friends with B and B does not think they are friends with A, or where A and B were friends but one of them is dead.)
  • Transitive: If A is friends with B, and B is friends with C, then A is friends with C? Nope, I’ve had a few cases where two friends of mine don’t like each other, and many cases where different friends of mine aren’t even aware of each other.

Acquaintance:

For the purposes of this section, if A is acquainted with B, then it means that A is aware of B’s existence and at least some aspects of B’s identity.

  • Reflexive: Everyone is acquainted with themselves.
  • Symmetric: I’m acquainted with several musicians who are not acquainted with me.
  • Transitive: I’m acquainted with several musicians, but I have no idea who they are acquainted with.

The implications of friendship and acquaintance do not seem to be well-modeled in social media. “Following” is a one-way form of acquaintance… except in real life you are not likely to be aware of who is acquainted with you. Should you be aware of who “follows” you in social media? Should your “friends” be able to see each other’s comments? Should they be able to see everyone who you are “friends” with? There is an openness that is assumed in various online sites, and it’s not necessarily appropriate. Not my cup of tea. I’m wary of what I share of my personal information.

Friendship isn’t transitive.

Dependency

IRS dependency:

In the United States, if you file a tax return, there are tax benefits to claiming someone as a dependent. There are specific legal rules that state that a dependent is a “qualifying child” or a “qualifying relative”, and “a dependent can’t claim a dependent on their own tax return.”

  • Reflexive: Can you be a dependent of yourself? No.
  • Symmetric: If A claims B as a dependent, can B claim A as a dependent? No.
  • Transitive: If A claims B as a dependent, and B claims C as a dependent, can A claim C as a dependent? This is not a valid case; if A claims B as a dependent, then B can’t claim C as a dependent.

Yeah, maybe this isn’t a great example.

Software dependency:

Different “modules” of software can be dependent on others; these form one or more direct acyclic graphs (DAG), and there are algorithms to analyze dependencies, such as topological sort.

  • Reflexive: Can a software module be dependent on itself? No, because that would form a cycle.
  • Symmetric: If module Foo is dependent on module Bar, can module Bar be dependent on module Foo? No, because that would form a cycle.
  • Transitive: If module Foo is dependent on module Bar, and module Bar is dependent on module Baz, then is module Foo dependent on module Baz? Well… (cough) it depends....

What exactly does a dependency mean? There are different types of dependencies, so we have to state these more precisely on a case-by-case basis.

Dependencies in Java

For example: Java has both compile-time and run-time dependencies, and neither may be transitive, depending on the situation.

Consider the following file Expectation.java:

package com.blobbery.expectations;

import java.util.List;
import com.blobbery.shadow.Shadow;
import com.blobbery.whimsy.Borogove;

public class Expectation {
    final private Shadow shadow;

    public Expectation(List<Borogove> borogoves) {
        this.shadow = Shadow.of(borogoves);
    }

    public boolean covers(Object object) {
        return this.shadow.test(object)
    }
}

In this case, the expectations package depends on the shadow and whimsy package, but in two different ways:

  • com.blobbery.whimsy.Borogove is part of the public interface of class com.blobbery.expectations.Expectations — you cannot construct an Expectation without passing in a List of Borogove objects.
  • com.blobbery.shadow.Shadow is an internal implementation detail of class com.blobbery.expectations.Expectation — you cannot construct an Expectation object without utilizing a Shadow object.

To compile the expectations package, the compiler needs both the shadow and whimsy packages.

If you don’t tell it where to find one of them, you get a compile-time error — for example if we leave out a library with the com.blobbery.whimsy package:

$ javac src/com/blobbery/expectations/*.java -classpath lib/blobbery-shadow.jar
src/com/blobbery/expectations/Expectation.java:5: error: package com.blobbery.whimsy does not exist
import com.blobbery.whimsy.Borogove;
                          ^
src/com/blobbery/expectations/Expectation.java:10: error: cannot find symbol
    public Expectation(List<Borogove> borogoves) {
                            ^
  symbol:   class Borogove
  location: class Expectation
2 errors

In other words: class Expectation depends on class Borogove.

But the expectations package does not depend on packages that it doesn’t reference directly — for example, if Borogove.java contains this:

package com.blobbery.whimsy;

import java.util.concurrent.Callable;
import com.blobbery.character.Shmoo;

public abstract class Borogove implements Callable<String> {
    abstract public boolean mimsy();

    public String getShmooName(Shmoo shmoo) {
        return shmoo.getName();
    }
}

then even though Borogove has a compile-time dependency on com.blobbery.character.Shmoo, and Expectation has a compile-time dependency on Borogove, there is not a transitive dependency on Shmoo.

If you create another Java class in EvenSteven.java that uses the Expectation class, the compiler needs to know about com.blobbery.whimsy.Borogove — again, you have to pass in a List of Borogove objects in order to construct an Expectation — but it does not need to know about com.blobbery.shadow.Shadow because that’s an internal detail of the Expectation class, not exposed in its public interface. (And com.blobbery.character.Shmoo isn’t needed either, unless it’s referenced in EvenSteven.java.)

So to compile EvenSteven.java, the Java compiler needs the expectations package and the whimsy package.

To run EvenSteven.java, the Java runtime may need all three packages (but not com.blobbery.character for the Shmoo class) in addition to the compiled EvenSteven.java itself:

  • com.blobbery.expectations
  • com.blobbery.whimsy
  • com.blobbery.shadow

The conditions in which there is a runtime dependency are a little odd though, and they depend on whether the Java classloader reaches a point in execution where these classes are needed, for example:

  • an instance of one of the classes (Expectation, Borogove, Shadow) is created
  • an instance of a class is created that contains a public API that references one of the classes:
    public boolean isExpectationNull(Expectation expectation) {
        return expectation == null;
    }
    

If an Expectation object is created (which requires com.blobbery.expectations), then it also depends on the Shadow class.

If the Expectation class is referenced in a public API, but only for something like a null test, and no Expectation object is actually created, then there is not a runtime dependency on the Shadow class, but we still need the Expectation class to be loaded.

A dependency diagram for these classes might look like this:

To identify compile-time dependencies: each class is dependent on other classes referenced in both its definition and API, so Expectation depends on Borogove and Shadow, and EvenSteven depends on Expectation and Borogove, but not on Shadow, and the only class depending on Shmoo is Borogove.

To identify run-time dependencies: Java loads classes when they are referenced by already-loaded classes through an API, or referenced in the definition of another class when instances of that class are created. In addition, classes can be loaded dynamically through methods like Class.forName(), which is a typical pattern for plugins.

Java is complicated because of its dynamic class-loading behavior, so let’s leave things at that. But guess what: C/C++ with dynamic/shared libraries is just as complicated when it comes to dependencies.

OK, fine, so we’ll stick to C with statically-linked libraries only.

Modules in C?

What are modules in C? There is no inherent concept of “modules” defined in the C language itself (except in the aforementioned C++20) — but most software engineers use the term loosely, without regard to language, in the context of modular programming. For a few examples:

From Tim Bailey’s An Introduction to the C Programming Language and Software Design (2005):

Large-scale C programs are organised so that related functions and variables are grouped into separate source files. Grouping code by source file is central to C’s compilation model, which compiles each file separately to produce individual object modules, and these are later linked to form the complete program. Separate compilation, in conjunction with the C scoping rules, gives rise to the paradigm of modular programming.

This code organisation strategy works as follows. Each source file is a module containing related functions and variables. The declarations of functions and variables (and constants and data-types) to be shared with other modules are stored in an associated header file; this is called the public interface. Access to the module from other modules is restricted to the public interface.

Functions defined in a module that are called only by other functions within that module are declared static. These functions comprise the private interface—functions visible only from within the module, as part of the module’s internal implementation. Similarly, external variables used only within the module are declared static. These private interface declarations are not added to the header file, but are declared at the top of the source file.

And from the second edition of Harbison & Steele’s C: A Reference Manual (1987), in Chapter 10: (Program Structure)

10.1 MODULARIZATION

We prefer to modularize programs by data types. That is, we think of a program as consisting of a number of modules, each of which implements a new, abstract data type by providing objects of the type and operations on the objects. These modules are sometimes called type managers, to emphasizes that they have control over the internal representation of the types and the implementation of the operations on the types.

Harbison and Steele then go on to describe the design of a stack module, and some discussion of implementation, eventually arriving at a section on packaging:

10.7 PACKAGING THE MODULE

Now that the data structures and algorithms for the stack module are finished, we can give some thought to the best way to export the module’s facilities to the user. We will have to provide declarations of the functions and macros implementing the operations, and we’ll have to provide the type stack_type.

The custom in C is to collect these definitions in a header file that can be imported (with #include) by users of the module. The header file is also a good place to put some short documentation. Tables 10-7 and 10-8 show the header file for the stack module, which we have named stack.h following normal conventions.

A couple of points should be mentioned here. First, because of the restrictions on external names, we have defined macros that convert our names (and the names that should be used by clients) to shorter names less likely to conflict in the linker. (We have also been careful with the internal names, such as stack_push; they all begin with the prefix stack_ to minimize conflicts.) Second, although our principle exported type is stack_type, we must also export stack_element_type and boolean, since we use those types in the definition of stack_type. Third, although we consider the implementation of stack_type to be “private” to our type manager, there is no way to actually hide the implementation from the user of the module. A caller could alter the components of the structure in arbitrary ways, thus corrupting the data. Finally, it is very helpful to include as much documentation in the header file as possible.

All the other code goes into the stack implementation module, stack.c, which should begin with the line

#include <stack.h>

since it will need the same type and macro definitions. To avoid duplication, the declarations for stack_element_type, stack_type, boolean, stack_isempty, and stack_isfull may be removed from stack.c, since they are now supplied in stack.h.

And thus ingrained in the C programmer’s mind, as natural as the sky above being blue in the day and black at night, is the idea of a “module” in C, as a pair of files foo.c and foo.h. The .c file (translation unit) contains the required type and function declarations, and the .h file contains the public interface, to be #included in other translation units dependent on the module.

This stack design chapter in Harbison & Steele was removed from a later edition; the book has since been revised to stick very closely to the C standard — which was not published until 1989, two years after the second edition of Harbison & Steele — and now appears to avoid commentary or examples about good software engineering with C. The Fifth Edition speaks of “modules” more formally in terms of translation units and object modules:

1.3 AN OVERVIEW OF C PROGRAMMING

We expect most of our readers to be familiar with programming in a high-level language such as C, but a quick overview of C programming may be helpful to some.

A C program is composed of one or more source files, or translation units, each of which contains some part of the entire C program—typically some number of external functions. Common declarations are often collected into header files and are included into the source files with a special #include command (Section 3.4). One external function must be named main (Section 9.9); this function is where your program starts.

A C compiler independently processes each source file and translates the C program text into instructions understood by the computer. The compiler “understands” the C program and analyzes it for correctness. If the programmer has made an error the compiler can detect, then the compiler issues an error message. Otherwise, the output of the compiler is usually called object code or an object module.

When all source files are compiled, the object modules are given to program called the linker. The linker resolves references between the modules, adds functions from the standard run-time library, and detects some programming errors such as the failure to define a needed function. The linker is typically not specific to C; each computer system has a standard linker that is used for programs written in many different languages. The linker produces a single executable program, which can then be invoked or run....

The easy part of today’s question involves the #include directive, which is a form of dependency. If, during the compilation process, some file foo.c includes bar.h — that is, the text of bar.h is substituted in-place while processing foo.c, for example if it contains #include "bar.h" and it is not ignored because of a conditional #if/#ifdef/#ifndef directive — and the file bar.h includes baz.h, then foo.c includes baz.h: include dependencies are transitive.

$$\left.\begin{aligned} A &\underset{\text{include}}{\rightsquigarrow} B \quad \\ B &\underset{\text{include}}{\rightsquigarrow} C \end{aligned} \right\} \quad A \underset{\text{include}}{\rightsquigarrow} C $$

What else is there to say?

C Modules in the Real World, or at Least the Surreal World

But files are not modules.

To help understand some of the real-world challenges (*cough* warts) of modular programming in C, here are three modules:

  • blobberino
  • bulbous
  • bouffant
/*
 * blobberino.h: 
 *
 * Blobberinos are the fundamental particle in
 * complex simulations of drapery and electrostatic desserts.
 * 
 * (c) 2017 Blobbery Unrealistic Expectations Corporation
 *
 * You may use this file freely with attribution.
 */

#ifndef BLOBBERINO_H
#define BLOBBERINO_H

#include <stdint.h>

typedef struct tagBlobberino {
    uint16_t id;
    int16_t  voltage;
    int32_t  position;
    int16_t  gain;
} Blobberino;

void Blobberino_initialize(Blobberino *pblob, uint16_t id, int16_t gain); 

static inline int32_t Blobberino_get_position(const Blobberino *pblob) {
    return pblob->position;
}

static inline int16_t Blobberino_get_voltage(const Blobberino *pblob) { 
    return pblob->voltage;
}

void Blobberino_update(Blobberino *pblob, int16_t voltage);

#endif   // BLOBBERINO_H

/*
 * blobberino.c:
 *
 * Blobberinos are the fundamental particle in
 * complex simulations of drapery and electrostatic desserts.
 * 
 * (c) 2017 Blobbery Unrealistic Expectations Corporation
 *
 * You may use this file freely with attribution.
 */

#include <stdint.h>
#include "blobberino.h"

void Blobberino_initialize(Blobberino *pblob, uint16_t id, int16_t gain) {
    pblob->id   = id;
    pblob->gain = gain;
}

void Blobberino_update(Blobberino *pblob, int16_t voltage) {
    pblob->voltage = voltage;
    pblob->position += (int32_t)pblob->gain * voltage; 
}

So far, so good.

/*
 * bulbous.h:
 *
 * Bulbous blobberinos have certain preconceived notions.
 *
 * (c) 2017 Blobbery Unrealistic Expectations Corporation
 *
 * You may use this file freely with attribution.
 */

#ifndef BULBOUS_H
#define BULBOUS_H

#include <stdint.h>
#include "blobberino.h"

static inline void Bulbous_Seventy(Blobberino *pblob) {
    Blobberino_update(pblob, 70);
}

static inline void Bulbous_Configure_Gridknuckle(Blobberino *pblob, uint16_t id) {
    Blobberino_initialize(pblob, id, 1234);
}

#endif   // BULBOUS_H

/*
 * bouffant.h:
 *
 * Certain hairstyles require special electrostatic treatment.
 *
 * (c) 2017 Blobbery Unrealistic Expectations Corporation
 *
 * You may use this file freely with attribution.
 */

#ifndef BOUFFANT_H
#define BOUFFANT_H

#include <stdint.h>
#include <stdbool.h>
#include "blobberino.h"
#include "bulbous.h"

typedef enum tagBouffantColor {
    BC_BLONDE = 0,
    BC_BLACK  = 1,
    BC_BROWN  = 2,
    BC_RED    = 3,
    BC_GRAY   = 4,
    BC_WHITE  = 5,
    BC_OTHER  = 6
} BouffantColor;

typedef struct tagBouffant {
    Blobberino    blob;
    BouffantColor color;
    uint16_t      height_mm;
    int16_t       relax_factor;
} Bouffant;

void Bouffant_initialize(Bouffant *pbouffant,
                         uint16_t id,
                         BouffantColor color,
                         int16_t relax_factor);

static inline void Bouffant_recharge(Bouffant *pbouffant) {
    Bulbous_Seventy(&pbouffant->blob);
    pbouffant->height_mm += Blobberino_get_position(&pbouffant->blob) >> 24;
}

static inline void Bouffant_relax(Bouffant *pbouffant) {
    // exponential decay
    pbouffant->height_mm -= ((int32_t)relax_factor * pbouffant->height_mm) >> 16;
}

int16_t Bouffant_get_risk_factor(const Bouffant *pbouffant);

void Bouffant_update(Bouffant *pbouffant, bool recharge);

#endif   // BOUFFANT_H

/*
 * bouffant.c:
 *
 * Certain hairstyles require special electrostatic treatment.
 *
 * (c) 2017 Blobbery Unrealistic Expectations Corporation
 *
 * You may use this file freely with attribution.
 */

#include <stdint.h>
#include "blobberino.h"
#include "bulbous.h"
#include "bouffant.h"

void Bouffant_initialize(Bouffant *pbouffant,
                         uint16_t id,
                         BouffantColor color,
                         int16_t relax_factor)
{
    Bulbous_Configure_Gridknuckle(&pbouffant->blob, id);
    pbouffant->color = color;
    pbouffant->relax_factor = relax_factor;
}

int16_t Bouffant_get_risk_factor(const Bouffant *pbouffant)
{
    return pbouffant->height_mm ^ Blobberino_get_position(&pbouffant->blob)
         - pbouffant->color; 
}

void Bouffant_update(Bouffant *pbouffant, bool recharge)
{
    if (recharge) {
        Bouffant_recharge(pbouffant);
    }
    Bouffant_relax(pbouffant);
}

If we draw a diagram of the three modules with #include dependencies, we end up with something like this:

Which is… icky. And it’s worth noting the cause of the ickiness: inline function definitions. All three modules have them. If you write some program that uses the bouffant module, you’re stuck with bringing in bulbous and blobberino into the mix as well, even though the bouffant module’s interface doesn’t depend on either: blobberino is needed because it helps define the Bouffant type, and the bulbous module is used for bouffant‘s inline functions.

Why would I ever use inline functions in C? Because I want to put the function definition in one module, but allow the compiler to substitute that function’s definition inline, and optimize it while compiling the translation unit of another module. This is important sometimes for hard real-time embedded systems; it avoids the function call overhead which is typically pushing some registers onto the stack, a call operation, a return operation, and popping the registers back off of the stack.

But it also adds dependencies: header files that contain inline function definitions now mix implementation details with interface.

One alternative that helps clean up this dependency is to split the header file into one for type definitions and another for function declarations (and inline function definitions):

For example, the two header files of the bouffant module would look like this:

/*
 * bouffant_types.h:
 *
 * Certain hairstyles require special electrostatic treatment.
 *
 * (c) 2017 Blobbery Unrealistic Expectations Corporation
 *
 * You may use this file freely with attribution.
 */

#ifndef BOUFFANT_TYPES_H
#define BOUFFANT_TYPES_H

#include <stdint.h>
#include "blobberino_types.h"

typedef enum tagBouffantColor {
    BC_BLONDE = 0,
    BC_BLACK  = 1,
    BC_BROWN  = 2,
    BC_RED    = 3,
    BC_GRAY   = 4,
    BC_WHITE  = 5,
    BC_OTHER  = 6
} BouffantColor;

typedef struct tagBouffant {
    Blobberino    blob;
    BouffantColor color;
    uint16_t      height_mm;
    int16_t       relax_factor;
} Bouffant;

#endif   // BOUFFANT_TYPES_H

/*
 * bouffant.h:
 *
 * Certain hairstyles require special electrostatic treatment.
 *
 * (c) 2017 Blobbery Unrealistic Expectations Corporation
 *
 * You may use this file freely with attribution.
 */

#ifndef BOUFFANT_H
#define BOUFFANT_H

#include <stdint.h>
#include <stdbool.h>
#include "blobberino.h"
#include "bulbous.h"
#include "bouffant_types.h"

void Bouffant_initialize(Bouffant *pbouffant,
                         uint16_t id,
                         BouffantColor color,
                         int16_t relax_factor);

static inline void Bouffant_recharge(Bouffant *pbouffant) {
    Bulbous_Seventy(&pbouffant->blob);
    pbouffant->height_mm += Blobberino_get_position(&pbouffant->blob) >> 24;
}

static inline void Bouffant_relax(Bouffant *pbouffant) {
    // exponential decay
    pbouffant->height_mm -= ((int32_t)relax_factor * pbouffant->height_mm) >> 16;
}

int16_t Bouffant_get_risk_factor(const Bouffant *pbouffant);

void Bouffant_update(Bouffant *pbouffant, bool recharge);

#endif   // BOUFFANT_H

This keeps the type definition files very clean; the only dependencies that get pulled in from another module are the ones directly needed to define the memory layout of allocated variables. (I usually put inline “getter” functions like Blobberino_get_position in the type definition files because they don’t add any extra dependencies, and the only additional implementation details are to extract a structure member.)

Header files containing the function declarations — as opposed to type definitions — are sometimes messier, and that’s just the nature of the situation in C.

We could go further, and isolate the inline function definitions in bouffant_inline.h, which would allow bouffant.h to remove the #include dependencies on bulbous.h and blobberino.h.

So Are the Dependencies of C “Modules” Transitive? (And Why Does It Matter?)

OK, so what about module dependencies in C? Are they transitive?

Well, as in Java, there are different kinds of dependency; unlike Java, C is less rigid — as a result, module dependencies can vary on how someone organizes and builds their project.

Here is a “nicer” dependency diagram in C, with four modules: gazebo, bouffant, bulbous, and blobberino:

In this case, the interfaces (header files) are nicely decoupled from implementation (translation units) and the compile-time dependencies are minimal:

  • gazebo’s only external dependency is bouffant.h
  • bouffant’s only external dependencies are bulbous.h and blobberino.h

Here compile-time dependency is not transitive, and there’s no need to care about the bulbous and blobberino modules at all.

This kind of situation is unusual in C: more often there is a transitive dependency between header files when types are involved, so, for example, #include "bouffant.h" would need to pull in blobberino.h as well if bouffant.h defines a struct type that depends on a type defined in blobberino.h. You can get around it with a forward declaration, under certain restricted conditions — for example, if bouffant.h looked like this:

/*
 * bouffant.h:
 *
 * Certain hairstyles require special electrostatic treatment.
 *
 * (c) 2017 Blobbery Unrealistic Expectations Corporation
 *
 * You may use this file freely with attribution.
 */

#ifndef BOUFFANT_H
#define BOUFFANT_H

#include <stdint.h>
#include <stdbool.h>

typedef enum tagBouffantColor {
    BC_BLONDE = 0,
    BC_BLACK  = 1,
    BC_BROWN  = 2,
    BC_RED    = 3,
    BC_GRAY   = 4,
    BC_WHITE  = 5,
    BC_OTHER  = 6
} BouffantColor;

typedef struct tagBouffant Bouffant; // forward declaration

void Bouffant_initialize(Bouffant *pbouffant,
                         uint16_t id,
                         BouffantColor color,
                         int16_t relax_factor);

void Bouffant_recharge(Bouffant *pbouffant);

void Bouffant_relax(Bouffant *pbouffant);

int16_t Bouffant_get_risk_factor(const Bouffant *pbouffant);

void Bouffant_update(Bouffant *pbouffant, bool recharge);

#endif   // BOUFFANT_H

Here there is no type definition of Bouffant — all we’re doing is telling the compiler that there is a type Bouffant which is a struct tagBouffant, and all of the function declarations that use it are non-inline and only deal with pointers to Bouffant.

Other modules can interact with Bouffant pointers using these functions — but they cannot allocate Bouffant objects, either statically or dynamically, because there is no type definition. (Dynamic allocation could be used by encapsulating Bouffant allocation/deallocation inside functions declared in bouffant.h and implemented in bouffant.c, without any other module needing to know about the memory layout of the Bouffant type. Static allocation requires other modules to have access to the Bouffant type definition.)

But at least it cuts off compile-time dependencies to other modules that Bouffant‘s implementation may use.

Aside from compile-time dependencies, someone has to link the whole program together, and at this point dependencies are transitive; if Gazebo‘s implementation uses functions implemented in Bouffant, and Bouffant‘s implementation uses functions implemented in Blobberino, then at link time all of these objects are required, whether you like it or not. But this could be managed privately by the Bouffant module maintainers: they could publish the header file bouffant.h and a precompiled library bouffant.a which contains translation units bouffant.c and blobberino.c, freeing other modules from having to know about Blobberino… except if some other programmer that uses bouffant.a also wants to use the Blobberino module for other purposes, then they need to use the blobberino objects compiled in bouffant.a; otherwise, if they compile their own version of blobberino.c and go to link the program together, there will be a link error because there are two copies of the functions in blobberino.c.

Why does all this matter?

Because module transitivity causes indirect dependencies which may be undesirable. If you’re marrying some nice woman but her father and brothers are a bunch of inconsiderate jerks, you’re going to be stuck with them. (Family membership is transitive, to a degree.)

For the programmer, each indirect dependency bears some risk of problems. If you are writing gazebo.c which contains #include "bouffant.h", and bouffant.h contains #include "bulbous.h", and bulbous.h contains some declaration that causes a compile error for you (perhaps a duplicate of a type definition, or of some #define macro), or you’re using static analysis tools for MISRA checking and you get an error because of something in bulbous.h, then you have to fix it yourself… even though you just wanted to use bouffant.h, you’re stuck with the bulbous.h that it pulled in.

Watch your #includes — if you don’t need them, remove them, because they’re like sticky tar that can congeal your project into one big agglutinous mess.

Wrapup

Here’s what we covered today:

  • C doesn’t have a specified definition of a module (except for C++20), but by convention, a module in C is a group of closely-related source files, with .c files for implementation and header files for the module interface

  • Transitivity in module dependency is usually unwanted because it pulls in any undesirable aspects of indirect dependencies: compile errors, static analysis errors, etc.

  • C modules can avoid the compile-time aspects of transitive dependency by reducing dependencies in header files, and leaving dependencies in .c files

    • use of inline function definitions (static inline) will generally increase module coupling
    • splitting header files into _types.h (type definitions and getter functions only) and .h (function declarations, inline function definitions), or even isolating inline function definitions in their own _inline.h, can counteract the coupling effects of inline function definitions

The lack of a built-in module concept in C forces us to manage modular programming ourselves; this is only one of many “warts” of C. Stay tuned for some more! And good luck with your own C programming projects.


© 2026 Jason M. Sachs, all rights reserved.


To post reply to a comment, click on the 'reply' button attached to each comment. To post a new comment (not a reply to a comment) check out the 'Write a Comment' tab at the top of the comments.

Please login (on the right) if you already have an account on this platform.

Otherwise, please use this form to register (free) an join one of the largest online community for Electrical/Embedded/DSP/FPGA/ML engineers: