4

I'm a firmware developer and I'm interested in applying the SOLID practice in low level programming especially in Hardware Abstraction Layers in ARM microcontrollers.

Every example I come across on the internet is implemented in C++ or C# or Java and it seems a little hard to follow those patterns in C.

Is there any examples that could give me a hint of how to make that work in C?

4
  • 2
    All the ARM toolchains I've worked on in the past 5+ years have C++ support. As Amon's answer shows, you can fake up polymorphic behaviour in C, but you'd almost certainly be better off just using (a limited subset of) C++. Commented May 25, 2020 at 13:20
  • Yes, true but I'll end up with bigger binary.
    – MrBit
    Commented May 25, 2020 at 13:35
  • Have you actually tested that? The suggestion here is effectively to manually create vtables in C, at which point the compiler may well actually do a better job than you do. And you can use the time you save optimising the size of the binary. Commented May 25, 2020 at 14:00
  • 2
    @MrBit My experience with C++ is that it results in optimal code – many of its abstractions have zero overhead. For others you just pay for what you use, e.g. you might want to disable exceptions or even avoid the standard library. Templates can lead to size bloat, but that's because templates copy&paste of your function definitions. That's sometimes avoidable by using runtime polymorphism. Every time I ported a C project to C++ it has paid off because it's a so much better/useful language.
    – amon
    Commented May 25, 2020 at 14:54

1 Answer 1

8

Applying SOLID is not always appropriate. Dependency inversion implies some indirection, and that typically means overhead. This kind of overhead is unlikely to be appropriate in memory-constrained devices. But not all is lost: we can implement relevant OOP functionality in C, but we might also find that using the preprocessor provides enough flexibility.

A typical dependency inversion example refactors this kind of code:

class Dependency {
  int concreteStuff;
}

class Context {
  Dependency d;
  void doSomething() {
    print(d.concreteStuff);
  }
}

new Context(new Dependency()).doSomething();

To:

interface Interface {
  int getConcreteStuff();
}

class Dependency implements Interface {
    int concreteStuff;
    int getConcreteStuff() { return this.concreteStuff; }
}

class Context {
  Interface i;
  void doSomething() {
   print(i.getConcreteStuff());
  }
}

new Context(new Dependency()).doSomething();

While C doesn't have interfaces in the Java sense, one option is to implement this OOP-like functionality (run-time polymorphism) ourselves:

// interface:
typedef struct {
  void* data;
  int (*getConcreteStuff)(Interface*);
} Interface;

// dependency:
typedef struct {
  int concreteStuff;
} Dependency;

static int getConcreteStuff(Interface* interface) {
  return ((Dependency*)interface->data)->concreteStuff;
}

Interface Dependency_new() {
  Dependency* d = malloc(sizeof(*d));
  d->concreteStuff = 0;
  return { d, getConcreteStuff };
}

// context:
typedef struct {
  Interface i;
} Context;

void Context_doSomething(Context* ctx) {
  printf("%d\n", ctx->i.getConcreteStuff(&ctx->i));
}

// composition
Context ctx = { Dependency_new() };
Context_doSomething(&ctx);

The Interface represents a classic vtable that stores function pointers to the interface methods. In simple cases where you only have a few function pointers you can do away with the explicit interface and store the pointers directly in the context. The context does not know anything about the concrete dependency, and only interacts with it through the interface function pointers – the actual dependency is hidden behind a void pointer. In all cases, the concrete dependency is resolved during composition, and can be freely selected at run time.

So this kind of approach is appropriate when you do need the ability to select different dependencies at run time, or when you don't know all possible interface implementations (e.g. when you are writing a library to be extended by other applications).

But that kind of runtime flexibility is not always needed! Especially in an embedded context, you might be able to resolve the dependencies at build time and then flash the appropriate configuration. You also likely know all possible dependencies in advance. Then, the most C-ish approach is to use the preprocessor.

For example, you might use the preprocessor to select the correct definitions for structs and functions

#ifdef DEPENDENCY = "TEST"
  typedef struct {} Dependency;
  int getConcreteStuff(Dependency*) { return 42; }
#else
  typedef struct {
    int concreteStuff;
  } Dependency;

  int getConcreteStuff(Dependency* d) { return d->concreteStuff; }
#endif

typedef struct {
  Dependency d;
} Context;

void doSomething(Context* ctx) {
  printf("%d\n", getConcreteStuff(&ctx->d));
}

Alternatively, you might compile all dependencies and use the preprocessor to name the correct dependency:

// invoke compiler with -DDependency=TestDependency to use this implementation
typedef struct {} TestDependency;

int TestDependency_getConcreteStuff(TestDependency*) {
  return 42;
}

typedef struct {
  int concreteStuff;
} StandardDependency;

int StandardDependency_getConcreteStuff(StandardDependency* d) {
  return d->concreteStuff;
}

// default to StandardDependency
#ifndef Dependency
#define Dependency StandardDependency
#endif

// helper to call functions with correct name
#define METHOD(m) Dependency ## _ ## m;

typedef struct {
  Dependency d;
} Context;

void doSomething(Context* ctx) {
  printf("%d\n", METHOD(getConcreteStuff)(&ctx->d));
}

I prefer this latter approach because all the code is still compiled and type checked, thus guarding against bitrot. The extra generated machine code can be optimized away to save space, either if the dependency function are inline, have internal linkage, or by using link time optimization.

8
  • Thanks for the answer. I just read here stackoverflow.com/a/26205828/3829694 that header files can be used as interfaces in C. What's the difference between that and your answer?
    – MrBit
    Commented May 25, 2020 at 12:44
  • @MrBit Header files primarily offer modularization/encapsulation. You can declare functions and structs in the header, but define them in a separate compilation unit. In particular, static functions are private. That's a weak kind of dependency inversion where all compilation units depend on declarations in the headers. But dependency inversion is more concerned with how dependencies are fulfilled/linked/injected. In the OOP examples there's some piece of code that selects concrete dependencies combines the objects. In the preprocessor variants, this decision is moved to compiler flags.
    – amon
    Commented May 25, 2020 at 13:41
  • I think you want return ((Dependency*)interface->data)->concreteStuff; in getConcreteStuff.
    – Erik Eidt
    Commented May 25, 2020 at 16:11
  • FWIW, I don't see this as a "classic vtable", as each interface object carries an entire vtable copy along with a pointer to the data. I would expect the other way around: the data to carry a pointer a vtable (that could be shared by all members of the same concrete class), mimicking class Dependency implements Interface
    – Erik Eidt
    Commented May 25, 2020 at 16:18
  • Thanks @Erik, I fixed that mistake. Yes you're right that there are different ways to do vtables. I tried to illustrate that data + vtable belong together, although in practice you'd either embed the full vtable or a vtable pointer in the object (like a typical C++ or Java implementation), or detach the vtable from the object and store it alongside the data pointer (as in typical Go, Rust, Haskell implementations). In C, it seems somewhat common to abstract only over behaviour but not data layout: using a vtable, but no void pointer to the data.
    – amon
    Commented May 25, 2020 at 16:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.