Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Code Reviews

Welcome to Software Development on Codidact!

Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.

Comments on Pattern / architecture for interfacing with components in C

Parent

Pattern / architecture for interfacing with components in C

+2
−0

I'm working on the architecture, where the assumption is to easily extend the options in the system and also to provide some kind of encapsulation (from the main we could only access the type1 / type2 through interface). It will be an embedded software system, where there will be e.g. the same end device, but different inputs (digital or analog). I want to use the same interface to get the signal, but it could be handled in different ways. Each functionality will have its implementation, but the final signal will be the same and will be handed over to the next function.

I'm wondering about declaring the pointers in type*.h and using them in type*.c in this way

type1.h
extern stType1 *type1_object;

Is it proper to variable declaration, definition and use in the function in this way? It compiles, but could it be used in that way?

type1.c
static stType1 memReservation;
stType1 *type1_object = &memReservation;

bool getOneType1()
{
    return type1_object->internalOne;
}

Whole code below:

interface.h

#ifndef _INTERFACE_H
#define _INTERFACE_H
#include <stdbool.h>
#include <stdint.h>

#define DIGITAL 0
#define ANALOG  1

typedef struct hwInterface hwInterface;

struct hwInterface
{
    void (*cyclic)();
    bool (*getOne)();
    bool (*getTwo)();
};

// common interface
extern hwInterface *hwInterfaceX;

void init(uint8_t Type);
void initType1();
void initType2();

#endif

interface.c

#include "interface.h"

static hwInterface memReservation;
hwInterface *hwInterfaceX = &memReservation;

void init(uint8_t Type)
{
    // program depend from parameter
    if (Type == DIGITAL)
    {
        initType1();
    }
    else if (Type == ANALOG)
    {
        initType2();
    }
}
type1.h

#include <stdio.h>
#include "interface.h"

typedef struct stType1
{
    hwInterface interface;
    bool internalOne;
    bool internalTwo;
} stType1;

extern stType1 *type1_object;

/////////////////////////
// INTERFACE FUNCTIONS //
/////////////////////////
void cyclicType1();
bool getOneType1();
bool getTwoType1();

///////////////////////
// PRIVATE FUNCTIONS //
///////////////////////
void type1Printer();

type1.c

#include "type1.h"

static stType1 memReservation;
stType1 *type1_object = &memReservation;

/////////////////////////
// INTERFACE FUNCTIONS //
/////////////////////////
void cyclicType1()
{
    // Read digital inputs, some logic  //
    // write to own variables           //
    type1Printer();
}

bool getOneType1()
{
    return type1_object->internalOne;
}

bool getTwoType1()
{
    return type1_object->internalTwo;
}

//////////////////////////
//   INITIALIZATION     //
//////////////////////////
void initType1()
{
    printf("Init Type1 function\n");
    type1_object->interface.cyclic = &cyclicType1;
    type1_object->interface.getOne = &getOneType1;
    type1_object->interface.getTwo = &getTwoType1;
    hwInterfaceX = (hwInterface*)type1_object;
    
    type1_object->internalOne = false;
    type1_object->internalTwo = true;
}

///////////////////////
// PRIVATE FUNCTIONS //
///////////////////////
void type1Printer()
{
    printf("Cyclic type1 own function\n");
}

type2.h

#include <stdio.h>
#include "interface.h"

typedef struct stType2
{
    hwInterface interface;
    bool internalOne;
    bool internalTwo;
} stType2;

extern stType2 *type2_object;

/////////////////////////
// INTERFACE FUNCTIONS //
/////////////////////////
void cyclicType2();
bool getOneType2();
bool getTwoType2();

///////////////////////
// PRIVATE FUNCTIONS //
///////////////////////
void type2Printer();

type2.c
#include "type2.h"

static stType2 memReservation;
stType2 *type2_object = &memReservation;

/////////////////////////
// INTERFACE FUNCTIONS //
/////////////////////////
void cyclicType2()
{
    // Read digital inputs, some logic  //
    // write to own variables           //
    type2Printer();
}

bool getOneType2()
{
    return type2_object->internalOne;
}

bool getTwoType2()
{
    return type2_object->internalTwo;
}

//////////////////////////
//   INITIALIZATION     //
//////////////////////////
void initType2()
{
    printf("Init Type2 function\n");
    type2_object->interface.cyclic = &cyclicType2;
    type2_object->interface.getOne = &getOneType2;
    type2_object->interface.getTwo = &getTwoType2;
    hwInterfaceX = (hwInterface*)type2_object;
    
    type2_object->internalOne = true;
    type2_object->internalTwo = false;
}

///////////////////////
// PRIVATE FUNCTIONS //
///////////////////////
void type2Printer()
{
    printf("Cyclic type2 own function\n");
}

main.c

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include "interface.h"


void display(bool input)
{
    printf("%d\n", input);
    
}

int main()
{
    uint8_t Type = DIGITAL;
    
    //////////////////////
    // READ PARAMETERS  //
    //////////////////////
    printf("Choose type (0 - DIG, 1 - ANALOG): ");
    scanf("%hhd", &Type);
    
    // INITIALIZATION //
    init(Type);
    
    while(1) 
    {
        /////////////////////////////////////////////////////
        // cyclic  - access posible only through interface //
        /////////////////////////////////////////////////////
        hwInterfaceX->cyclic();
        
        // external block (display) reads footboards state from interface
        display(hwInterfaceX->getOne());
        display(hwInterfaceX->getTwo());
        
        // some delay
        for(volatile uint32_t i = 0; i < 700000000; i++);
        
    }

    return 0;
}




History
Why does this post require attention from curators or moderators?
You might want to add some details to your flag.
Why should this post be closed?

1 comment thread

Note about embedded systems (1 comment)
Post
+4
−0

Code review part:

Design (important!)

  • Global variables/external linkage are to be avoided (Why is global evil?).

  • You don't actually have private encapsulation in this code since the internals of the struct are exposed to the caller. There are better ways to design this with "opaque types" and private encapsulation, see How to do private encapsulation in C? As a bonus, such a design allows for multiple instances of each object instead of "singleton" which is what you have currently.

    In embedded systems, multiple instances can for example be convenient when you have multiple hardware peripherals in a MCU, all behaving the same.

  • It's a contrived design that both type1 and type2 seem to know about each other and share the same members, and that they know of the interface. This is not a proper program design, it's just 3 files glued together with tight coupling between them. type1 shouldn't know about type2 and vice versa. The interface, which I suppose is equivalent of a hardware abstraction layer (HAL) in this case, should know about which type1, type2 etc it uses but the user of the interface need not know/care about that.

    The program dependencies should go like:
    main.c uses interface, which contains type1 and type2

Performance/memory

  • Type ought to have been an enum instead, unless you have very good reasons to avoid them on the target system (ie 8-bitter archaic MCU or the like).
  • Using function pointers in a struct as means of inheritance is not necessarily wrong, but a bit clunky. They are hard for the compiler to optimize and adding them to every struct wastes a bit of RAM. We could as well have ordinary functions taking a struct member as parameter, the syntax is a bit different but the meaning is the same.

Style & best practices

  • The variable names could be better than "type1", "type2" etc but I take it this is just a simplified, artificial example.
  • Similarly, stdio.h is to be avoided in (embedded) systems but I take it this is just a simplification as well.
  • It's a good idea to use source code prefixes for your code, so if you have a module hwinterface.h, then you could prefix all identifiers belongint to that one as hwinterface or maybe hwif if that's too long. So don't name a function init, name it hwinterface_init.
  • Avoid pointless comments like // INITIALIZATION when commenting the line init(). That's already self-documenting code, the comment fills no purpose.

C language issues

  • Note that a function declared in C as void func() is a function accepting any parameter. This is inconvenient for type safety reasons. And besides, this is getting changed in the upcoming C23 standard where empty parenthesis will become equivalent to (void), just like C++ has always worked.
  • Identifiers beginning with an underscore followed by a capital letter are reserved for the compiler and standard library. So don't name header guards _INTERFACE_H.

Alternative design (draft)

Ok so I cooked up an alternative design. The program does nonsense stuff, but it is more an example of how you can use opaque type and private encapsulation. I also wrote a simple memory pool for shared static allocation, which may or may not be desired in an embedded application. I'll post each source file then comment on it below each file.

//hwinterface.h

#ifndef HW_INTERFACE_H
#define HW_INTERFACE_H

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

#include "mempool.h"
#include "type1.h"
#include "type2.h"

typedef union hwinterface hwinterface;

typedef enum
{
  HW_ANALOG,
  HW_DIGITAL,
} hw_t;

hwinterface* hwinterface_init (hw_t hw, int stuff1, int stuff2);

void hwinterface_run (hwinterface* hwi);

#endif
  • Bit of renaming and clean-up, source code prefixes, added an enum etc etc.
  • typedef union hwinterface hwinterface; This is a forward declaration of an opaque type. I'll explain why I used a union further below.
  • hwinterface_init This is a constructor and it uses allocation internally (which isn't a must). It creates a type1 and type2 object internally but the caller doesn't really need to care. Apart from the enum, I made up some nonsense input parameters.
  • hwinterface_run is some function to call cyclically.
// hwinterface.c
#include "hwinterface.h"

union hwinterface
{
  struct
  {
    hw_t hw;
    type1* t1;
    type2* t2;
  };
  uint8_t raw [sizeof(hw_t) + sizeof(type1*) + sizeof(type2*)];
};

hwinterface* hwinterface_init (hw_t hw, int stuff1, int stuff2)
{
  hwinterface* hwi = (hwinterface*) mempool_alloc(sizeof(hwinterface));
  if(hwi == NULL)
    for(;;){} // error handling here

  hwi->hw = hw;

  hwi->t1 = type1_alloc();
  if(hwi->t1 == NULL)
    for(;;){} // error handling here
  type1_set_stuff(hwi->t1, stuff1);

  hwi->t2 = type2_alloc();
  if(hwi->t2 == NULL)
    for(;;){} // error handling here
  type2_set_stuff(hwi->t2, stuff2);

  return hwi;
}

void hwinterface_run (hwinterface* hwi)
{
  if(hwi->hw == HW_DIGITAL)
  {
    int stuff1 = type1_get_stuff(hwi->t1);
    int stuff2 = type2_get_stuff(hwi->t2);
    type1_set_stuff(hwi->t1, (bool)stuff1);
    type2_set_stuff(hwi->t2, (bool)stuff2);
  }

  type1_run(hwi->t1);
  type2_run(hwi->t2);
}
  • The union looks a bit peculiar here, but it is made this way in order to utilize a memory pool without invoking undefined behavior. The mempool_alloc function will return an uint8_t* and the only way we can safely type pun that one into a larger type is by using a type where an uint8_t[] array exists as one of the members. Normally the (hwinterface*) would have been very fishy otherwise.
  • hwinterface_init allocates an instance of the interface itself, then call constructors of the type1 and type2, all by using the shared memory pool (which again is totally optional).
  • hwinterface_run does some nonsense thing by translating the values obtained by each of type1 and type2 to 1 or 0 in case of HW_DIGITAL. This isn't important.

// mempool.h
#ifndef MEMPOOL_H
#define MEMPOOL_H

#include <stdint.h>
#include <stddef.h>

#define MEMPOOL_SIZE  96
#define MEMPOOL_ALIGN  4

void mempool_init (void);

uint8_t* mempool_alloc (size_t bytes);

#endif // MEMPOOL_H

Very straight-forward and simple memory pool. You can set a maximum size and a normal alignment if applicable (otherwise 1).

// mempool.c
#include "mempool.h"

static uint8_t mempool [MEMPOOL_SIZE];
static uint8_t mempool_used;

void mempool_init (void)
{
  mempool_used = 0;
}

uint8_t* mempool_alloc (size_t bytes)
{
  uint8_t* result;

  uint8_t remain = bytes % MEMPOOL_ALIGN;
  if(remain)
  {
    bytes += MEMPOOL_ALIGN - remain;
  }

  if(mempool_used + bytes > MEMPOOL_SIZE)
    return NULL;

  result = &mempool[mempool_used];
  mempool_used += bytes;
  return result;
}

This just comes with some crude alignment handling and returning a pointer into a memory pool, if there is room. In a real-world application we have to make sure that mempool is aligned correctly, through some non-standard compiler extension or linker script. Basically this is just to get rid of dynamic allocation, which shouldn't be used in embedded systems. Why should I not use dynamic memory allocation in embedded systems?

We should note that there are two serious problems when trying to allocate a larger type onto a byte array like this. One is the mentioned alignment, the other is the "strict aliasing rules". I won't get into that here, but with by union trick we dodged strict aliasing too. It is still highly recommended to compile using -fno-strict-aliasing on all gcc-like compilers, particularly when doing embedded systems.

// type1.h
#ifndef TYPE1_H
#define TYPE1_H

#include <stdint.h>
#include <stdio.h>
#include "mempool.h"

typedef union type1 type1;

type1* type1_alloc (void);

int  type1_get_stuff (const type1* t1);
void type1_set_stuff (type1* t1, int stuff);

void type1_run (type1* t1);

#endif // TYPE1_H

Generic class implementation with some nonsense setter/getter functions. I made type1 and type2 100% identical apart from the name, though in a real application these would naturally be different, meaningful things.

// type2.h
#ifndef TYPE2_H
#define TYPE2_H

#include <stdint.h>
#include <stdio.h>
#include "mempool.h"

typedef union type2 type2;

type2* type2_alloc (void);

int  type2_get_stuff (const type2* t2);
void type2_set_stuff (type2* t2, int stuff);

void type2_run (type2* t2);

#endif // TYPE2_H

Same deal as type1.

// type1.c
#include "type1.h"

static void type1_print (const type1* t1);


union type1
{
  struct
  {
    int stuff;
  };

  uint8_t raw [sizeof(int)];
};


type1* type1_alloc (void)
{
  type1* t1 = (type1*) mempool_alloc(sizeof(type1));
  if(t1 == NULL)
    for(;;){} // error handling here

  t1->stuff = 0;

  return t1;
}

int type1_get_stuff (const type1* t1)
{
  return t1->stuff;
}

void type1_set_stuff (type1* t1, int stuff)
{
  t1->stuff = stuff;
}

void type1_run (type1* t1)
{
  type1_print(t1);
}

static void type1_print (const type1* t1)
{
  printf("%s %d\n", __func__, t1->stuff);
}

Very similar to the hwinterface opaque type. Some setter/getter functions. A private function for printing. type2.c will be identical

// type2.c
#include "type2.h"

static void type2_print (const type2* t2);


union type2
{
  struct
  {
    int stuff;
  };

  uint8_t raw [sizeof(int)];
};


type2* type2_alloc (void)
{
  type2* t2 = (type2*) mempool_alloc(sizeof(type2));
  if(t2 == NULL)
    for(;;){} // error handling here

  t2->stuff = 0;

  return t2;
}

int type2_get_stuff (const type2* t2)
{
  return t2->stuff;
}

void type2_set_stuff (type2* t2, int stuff)
{
  t2->stuff = stuff;
}

void type2_run (type2* t2)
{
  type2_print(t2);
}

static void type2_print (const type2* t2)
{
  printf("%s %d\n", __func__, t2->stuff);
}

Same deal as type1.c.


This will print:

type1_print 1
type2_print 1
History
Why does this post require attention from curators or moderators?
You might want to add some details to your flag.

2 comment threads

Is there any benefit of defining MEMPOOL_SIZE and MEMPOOL_ALIGN in the header? (2 comments)
Thank you for all your tips and your commitment to help. I’m sure I will use your advice. I read thes... (3 comments)
Is there any benefit of defining MEMPOOL_SIZE and MEMPOOL_ALIGN in the header?
Melkor-1‭ wrote 8 months ago

Is there any benefit of defining MEMPOOL_SIZE and MEMPOOL_ALIGN in the header?

Lundin‭ wrote 8 months ago

Melkor-1‭ The header is the exposed part to the caller so you could write documentation there regarding how to tweak them. The caller shouldn't mess with the .c file. Optionally you could have the memory pool itself take these as input parameters to an init function. But most often we want the size to be set at link-time, so a compile-time constant makes more sense than run-time parameters.