Can a C header file be considered as an interface?

Viewed 2385

I am learning about architecture from Robert C. Martin's book Clean Architecture. One of the main rules emphasized through the book is the DIP rule that states Source code dependencies must point only inwards, toward higher-level policies . Trying to translate this in the embedded domain assume 2 components scheduler and timer . The scheduler is high-level policy which relies on the low level timer driver and needs to call the APIs get_current_time() and set_timeout() , simply I would split the module into an implementation file timer.c and a header(an interface?) timer.h and the scheduler.c could simply include timer.h to use these APIs . Reading the book portrayed the previous scenario as breaking of the dependency rule and implied that an interface between the 2 components should be implemented to break the dependency.

To imitate that in c for example timer_abstract can include a generic structure with pointers to functions struct timer_drv { uint32 (*get_current_time)(void); void (*set_timeout)(uint32 t); }

This to me looks like over-design. Isn't a simple header file sufficient ? Can a C header file be considered as an interface ?

2 Answers

In computing, an "interface" is a common boundary across which two or more components or subsystems exchange information.

A header file in C or C++ is a text file that contains a set of declarations and (possibly) macros that can be inserted into a compilation unit (a separate unit of source code, such as a source file), and allow that compilation unit to use those declarations and macros. In other words #include "headerfile" within a source file is replaced by the content of headerfile by the C or C++ preprocessor before subsequent compilation.

Based on these definitions, I would not describe a header file as an interface.

A header file may define data types, declare variables, and declare functions. Multiple source files may include that header, and each will be able to use the data types, variables, and functions that are declared in that header. One compilation unit may include that header, and then define some (or all) of the functions declared in the header.

However types, variables, and functions need not be placed in a header file. A programmer who is determined enough can manually copy declarations and macros into every source file that uses them, and never use a header file. A C or C++ compiler cannot tell the difference - because all the preprocessor does is text substitution.

The logical grouping of declarations and macros is actually what represents an interface, not the means by which information about the interface is made available to compilation units. A header file is simply one (optional) means by which a set of declarations and macros can be made available to compilation units.

Of course, a header file is often practically used to avoid errors in using a set of declarations and macros - so can help make it easier to manage the interface represented by those declarations and macros. Every compilation unit that #includes a header file receives the same content (unless affected by other preprocessor macros). This is much less error prone than the programmer manually copying declarations into every source file that needs them. It is also easier to maintain - editing a header file means all compilation units can be rebuilt and have visibility of the changes. Whereas, manually updating declarations and macros into every source file can introduce errors, because programmers are error prone - for example, by editing the declarations inconsistently between source files.

I think the reason why you would want an interface for a timer is indeed to break dependencies. Since the Scheduler uses the Timer, every location Scheduler.o is linked to, Timer.o must be linked to as well provided you use scheduler symbols that depend on timer symbols.

If you would have used an interface for Timer, no linking from Scheduler.o to Timer.o (or Scheduler.so to Timer.so if you want) is required nor useful. You will create an instance of Timer at runtime, likely pass it to the constructor of Scheduler, Timer.o will be linked to elsewhere.

Now why would that be useful? Unit testing is one example: you can pass a Timer stub class to Scheduler's ctor and link to TimerTestStub.o etc. You can see that this way of working does break dependencies. Scheduler.o does require a Timer, but which one is not a requirement at the build time level of scheduler.so but higher up. You pass the Timer instance as an argument of Scheduler's ctor.

This is also very useful to lower the amount of build-time dependencies when using libraries. The real trouble starts when creating a dependency chain. Scheduler requires Timer, Timer requires class X, class X requires class Y, class Y requires class Z ... This may look still ok to you but know that every class could be in another library. You then want to use Scheduler but are forced to drag a ton of includepath settings and likely do a ton of linking. You can break dependencies by only exposing the functionality of Scheduler you really need in its interface, of course you can use multiple interfaces.

You should make your own demo, write 10 classes, put them in 10 shared libs, make sure every class requires 3 other classes out of those 10. Now include 1 of those class headers in your main.cpp and see what you need to do to get it build properly. Now you need to think on breaking those dependencies.

Related