Question

Background:

We're modeling the firmware for a new embedded system. Currently the firmware is being modeled in UML, but the code generation capabilities of the UML modeling tool will not be used.

Target language will be C (C99, to be specific).

Low power (i.e. performance, quick execution) and correctness are important, but correctness is the top priority, above everything else, including code size and execution speed.

In modeling the system, we've identified a set of well-defined components. Each component has its own interface, and many of the components interact with many of the components.

Most components in the model will be individual tasks (threads) under a real-time operating system (RTOS), although some components are nothing more than libraries. Tasks communicate with one another entirely via message passing / queue posting. Interaction with libraries will be in the form of synchronous function calls.

Because advice/recommendations might depend on scale, I'll provide some information. There are maybe around 12-15 components right now, might grow to ~20? Not 100s of components. Let's say on average, each component interacts with 25% of the other components.

In the component diagram, there are ports/connectors used to represent interfaces between components, i.e. one component provides what the other component requires. So far so good.

Here's the rub: there are many cases where we don't want "Component A" to have access to all of "Component B's" interface, i.e. we want to restrict Component A to a subset of the interface that Component B provides.

Question / problem:

Is there a systematic, fairly straightforward way to enforce -- preferably at compile time -- the interface contracts defined on the component diagram?

Obviously, compile-time solutions are preferable to run-time solutions (earlier detection, better performance, probably smaller code).

For example, suppose a library component "B" provides functions X(), Y() and Z(), but I only want component "A" to be able to call function Z(), not X() and Y(). Similarly, even though component "A" might be capable of receiving and handling a whole slew of different messages through its message queue, we don't any component to be able to send any message to any component.

The best I could come up with is to have different header files for each component-component interface, and to only expose (via the header file) the parts of the interface that the component is allowed to use. Obviously this could result in a lot of header files. This would also mean that message passing between components wouldn't done directly with the OS API, but rather through function calls, each of which builds & sends a specific (allowed) message. For synchronous calls/libraries, only the allowed subset of the API would be exposed.

For this exercise, you can assume people will be well-behaved. In other words, don't worry about people cheating & cutting & pasting function prototypes directly, or including header files that they're not allowed to. They won't directly post a message from "A" to "B" if it's not permitted, and so on...

Maybe there is a way to enforce contracts with compile-time assertions. Maybe there is a more elegant way to check/enforce this at run-time, even if it incurs some overhead.

Code will have to compile & lint cleanly, so the "function prototype firewall" approach is OK, but it just seems there might be a more idiomatic way to do this.

Was it helpful?

Solution

The idea with the headers is sound, but, depending on the interlacing between your components, it might be cleaner to divide the interface of each component into a number of sub-categories with their own header files instead of providing a header file for each component-component-connection.

The sub-categories need not necessarily be disjoint, but make sure (via preprocessor directives) that you can mix categories without getting re-definitions; this can be achieved in a systematic fashion by creating a header-file for each type or function declaration with its own inclusion guard, and then building the sub-category headers from these atomic blocks.

OTHER TIPS

#ifdef FOO_H_

   /* I considered allowing you to include this multiple times (probably indirectly)
      and have a new set of `#define`s switched on each time, but the interaction
      between that and the FOO_H_ got confusing. I don't doubt that there is a good
      way to accomplish that, but I decided not to worry with it right now. */

#warn foo.h included more than one time

#else /* FOO_H_ */

#include <message.h>

#ifdef FOO_COMPONENT_A

int foo_func1(int x);
static inline int foo_func2(message_t * msg) {
    return msg_send(foo, msg);
}
...

#else /* FOO_COMPONENT_A */

  /* Doing this will hopefully cause your compiler to spit out a message with
     an error that will provide a hint as to why using this function name is
     wrong. You might want to play around with your compiler (and maybe a few
     others) to see if there is a better illegal code for the body of the
     macros. */
#define foo_func1(x) ("foo_func1"=NULL)
#define foo_func2(x) ("foo_func2"=NULL)

...
#endif /* FOO_COMPONENT_A */

#ifdef FOO_COMPONENT_B

int foo_func3(int x);

#else /* FOO_COMPONENT_B */

#define foo_func3(x) ("foo_func3"=NULL)

#endif /* FOO_COMPONENT_B */

You should consider creating a mini-language and a simple tool to generate header files along the lines of what nategoose proposed in his answer.

To generate the header in that answer, something like this (call it foo.comp):

[COMPONENT_A]
int foo_func1(int x);
static inline int foo_func2(message_t * msg) {
    return msg_send(foo, msg);
}

[COMPONENT_B]
int foo_func3(int x);

(and extending the example to give an interface usable by multiple components):

[COMPONENT_B, COMPONENT_C]
int foo_func4(void);

This would be straightforward to parse and generate the header file. If your interfaces (I especially suspect the message passing might be) are even more boilerplate than I've assumed above, you can simplify the language somewhat.

The advantages here are:

  1. A bit of syntactic sugar to make the maintenance easier.
  2. You can change the protection scheme by changing the tool if you discover a better method later. There will be fewer places to change, which means you're more likely to be able to make the change. (For example, you might later find an alternative to the "illegal macro code" that nategoose proposes.)
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top