I have created a modular design pattern which provide a single interface that can be used create instances with swapable back-end components, however I'm not entirely satisfied with it.
My practical implementation involves creating a generic interface to certain kinds of device drivers. The hope is that I can create an interface which exposes a layer meant as an adapter (for initializing drivers which non-interface conforming implementations) and to bring up framework infrastructure.
To bring focus to the design pattern itself I am showing a simplified example.
The source code can be found here.
say I have some application with a main.c like so:
#include <stdio.h>
#include <stdlib.h>
#include "boatModuleIF.h"
int main(int argc, char *argv[]) {
char *type = argv[1];
int typeNum = atoi(type);
moduleIF_t *IF = init_moduleIF(typeNum);
if (IF) {
IF->printCfg(IF->ctx);
}
else {
printf("Failed to init module");
}
return 0;
}
I use boatModuleIFDefs as a header which is common to all boatModuleComponents (i.e. boatModule, boatModuleIF, cnc, beneteau)
It exposes the following to those components: boatModuleIFDefs.h
#ifndef __MODULEIFDEFS_H_
#define __MODULEIFDEFS_H_
typedef struct moduleIF_CTX *moduleIF_CTX_t;
typedef struct {
void (*printCfg)(moduleIF_CTX_t ctx);
moduleIF_CTX_t ctx;
} moduleIF_t;
__attribute__((weak)) extern moduleIF_t *init_moduleIF_CNC();
__attribute__((weak)) extern moduleIF_t *init_moduleIF_Beneteau();
#endif // __MODULEIFDEFS_H_
Note that the init function for the CNC and Beneteau module interfaces are declared as weak symbols. This means that this generic code can expose a symbol which may or may not be defined. This is used as a proxy to determine whether our application has been compiled with the cnc.c "driver".
with boatModuleIF's header and source implemented as:
boatModuleIF.h
#ifndef __TESTMODULEIF_H_
#define __TESTMODULEIF_H_
#include "boatModuleIFDefs.h"
enum {
TYPE_CNC = 0,
TYPE_BENETEAU,
};
moduleIF_t *init_moduleIF(int type);
#endif // __TESTMODULEIF_H_
boatModuleIF.c
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include "boatModuleIFDefs.h"
#include "boatModuleIF.h"
moduleIF_t *init_moduleIF(int type) {
switch (type) {
case TYPE_CNC:
if (init_moduleIF_CNC) {
return init_moduleIF_CNC();
}
else {
printf("failed cnc");
return 0;
}
break;
case TYPE_BENETEAU:
if (init_moduleIF_Beneteau) {
return init_moduleIF_Beneteau();
}
else {
printf("failed ben");
return 0;
}
break;
default:
return 0;
}
}
boatModule also has a defs file which looks like:
#ifndef __TESTMTRDEFS_H_
#define __TESTMTRDEFS_H_
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
typedef struct {
uint8_t size;
uint8_t speed;
} boatCfg_t;
__attribute__((weak)) extern boatCfg_t dfltBoatCfg;
#endif // __TESTMTRDEFS_H_
Note that the dfltBoatCfg is defined as a weak symbol similarily to our init functions. This is useful if we have multiple kinds of configurations that may not be applicaple to all sailboats.
boatModule's header and source implemented as:
boatModule.h
#ifndef __TESTMODULE_H_
#define __TESTMODULE_H_
#include "boatModuleIFDefs.h"
moduleIF_t *init_moduleIF();
#endif // __TESTMODULE_H_
boatModule.c
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include "boatModuleIFDefs.h"
#include "boatModule.h"
#include "boatModuleDefs.h"
struct moduleIF_CTX {
uint8_t size;
uint8_t speed;
};
static void printCfg(moduleIF_CTX_t ctx) {
printf("speed %d size %d", ctx->speed, ctx->size);
}
moduleIF_t *init_moduleIF() {
struct moduleIF_CTX * const CTX = calloc(1, sizeof(struct moduleIF_CTX));
if (&dfltBoatCfg) {
CTX->size = dfltBoatCfg.size;
CTX->speed = dfltBoatCfg.speed;
}
moduleIF_t * const IF = (moduleIF_t *) calloc(1, sizeof(moduleIF_t));
IF->ctx = CTX;
IF->printCfg = printCfg;
return IF;
}
Then we have two source files for two different boatModule implementations (which here are a stand-in for drivers)
cnc.c
#include "boatModuleDefs.h"
boatCfg_t dfltBoatCfg = {
.size = 10,
.speed= 10,
};
beneteau.c
#include "boatModuleDefs.h"
boatCfg_t dfltBoatCfg = {
.size = 5,
.speed= 5,
};
The interesting part (imho) is really in the makefile however.
makefile
all: joinedModules
joinedModules: boatModule_Cnc.o boatModule_Beneteau.o boatModuleIF.o boatModuleDefs.h
gcc boatModuleIF.o boatModule_Cnc.o boatModule_Beneteau.o main.c -o getBoatCfg
boatModuleIF.o:
gcc -c boatModuleIF.c
boatModule_Beneteau.o: boatModule.o beneteau.o boatModuleDefs.h
ld -r beneteau.o boatModule.o -o boatModule_Beneteau.o
objcopy --redefine-sym printCfg=printCfg_Beneteau boatModule_Beneteau.o
objcopy --redefine-sym init_moduleIF=init_moduleIF_Beneteau boatModule_Beneteau.o
objcopy --redefine-sym dfltBoatCfg=dfltBoatCfg_Beneteau boatModule_Beneteau.o
boatModule_Cnc.o: boatModule.o cnc.o boatModuleDefs.h
ld -r cnc.o boatModule.o -o boatModule_Cnc.o
objcopy --redefine-sym printCfg=printCfg_CNC boatModule_Cnc.o
objcopy --redefine-sym init_moduleIF=init_moduleIF_CNC boatModule_Cnc.o
objcopy --redefine-sym dfltBoatCfg=dfltBoatCfg_CNC boatModule_Cnc.o
boatModule.o: beneteau.o cnc.o boatModuleDefs.h
gcc -c -fPIE boatModule.c
cnc.o: boatModuleDefs.h
gcc -c -fPIE cnc.c
beneteau.o: boatModuleDefs.h
gcc -c -fPIE beneteau.c
Essentially what I am doing is merging both the module object file and a backend component together to create a new merged object file. I can then redefine the global symbols used by both to prevent symbol collisions in such a way that maintains the "simplicity" of the generic module interface files.
This has some benefits, notably:
- During initialization of the interface I don't need some messy switch case which would have to check whether each type boat has each kind of boat config.
- Supports code reuse.
- If I want to add new configuration's there's little that needs to be added (except for one addtional if statement in the moduleif init and the definition in the boat/driver that actually cares about that config).
- If I want to add a new boat then all I need is to add a new init weak symbol and some tweaks to the makefile (really just adding the source name, the rest could be done automatically with make rules)
The issue is this doesn't quiet feel right and I'm wondering if this is some kind of code smell?
It could make evaluating the code much more difficult (e.g. I have not defined any symbol named dfltBoatCfg, it's redefined as dfltBoatCfg_CNC and dfltBoatCfg_Beneteau) however this could be mitigated with good comments/documentation.
If so is there some better/modified approach I could take would allow me to create this kind of modular design pattern that is future proofed against maintanence hell in the event that we should have to support many types of boats with many different configurations?
Any and all feedback is much appreciated.