views:

88

answers:

3

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.

+1  A: 

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.

Christoph
Thanks Christoph. That's pretty much exactly what I was thinking (although I didn't explain it in that level of detail in my already-verbose question). Basically, create a single file for each function prototype or type, and build different headers by including the relevant fine-granularity files. That way, we are sure to only have one definition of each function.
Dan
+2  A: 
#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 */
nategoose
I like Dan's original approach better: if there's a component which will be accessed by nearly all other components, the `#if defined(FOO_COMPONENT_A) || defined(FOO_COMPONENT_B) || ...` will be far less readable than providing separate headers
Christoph
Dan
+1  A: 

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.)
bstpierre
I don't really see the point of creating a custom preprocesor if all it does is replace `[COMPONENT_A, COMPONENT_B]` with `#if defined(COMPONENT_A) || defined(COMPONENT_B) ... #endif`
Christoph
Depending on how often this would be used this could be overkill (since the language would have to understand C well enough not to mess it up), but it could help out by helping you keep all of the preprocessor conditional blocks straight. It would have to be careful if it reordered (to combine) blocks of code, though, since there may be order based dependencies.
nategoose
It buys flexibility -- with a certain cost. If the relationships between the components are subject to change and maintenance, then the gain from flexibility may be worth it. It's not necessarily true that "all it does is replace..." -- it could generate separated header files. If the APIs and/or messages are boilerplate, all conforming to a given interface, then it could simplify the declarations. It can make refactoring easier since there's a single place to change. Again, I'm not saying it's necessary or even desirable in all circumstances. Just something to consider.
bstpierre
@btspierre - thanks for the answer. Actually there is someone in the group who is chomping at the bit to create just such a tool. There are a couple pockets of resistance to this internally: 1) group has had bad experiences in the past with home-grown tools (before my time) - probably more complex tools however; 2) documentation (even simple documentation) would be required, and documentation tends to get bypassed/overlooked; 3) the "hit by a bus" idea - what if tool maintainer quits, etc. Also I suppose we'd need to ensure the tool is always run during build to re-generate the header.
Dan
@Dan - Yes, those are all valid things to consider.
bstpierre