如何在编译时强制执行接口契约(在C中)?

时间:2010-10-26 19:54:41

标签: c interface uml design-by-contract contract

背景

我们正在为新的嵌入式系统建模固件。目前,固件正在使用UML建模,但不会使用UML建模工具的代码生成功能。

目标语言为C(C99,具体而言)。

低功耗(即性能,快速执行)和正确性很重要,但正确性是首要任务,高于其他所有功能,包括代码大小和执行速度。

在对系统进行建模时,我们已经确定了一组明确定义的组件。每个组件都有自己的接口,许多组件与许多组件交互。

模型中的大多数组件都是实时操作系统(RTOS)下的单个任务(线程),尽管有些组件只不过是库。任务通过消息传递/队列发布完全相互通信。与库的交互将采用同步函数调用的形式。

因为建议/建议可能取决于规模,我会提供一些信息。现在可能有大约12-15个组件,可能会增长到~20?不是100多个组件。让我们说平均来说,每个组件与25%的其他组件交互。

component diagram中,有用于表示组件之间接口的端口/连接器,即一个组件提供其他组件所需的内容。到目前为止一切都很好。

这就是问题:在很多情况下我们不希望“组件A”访问“组件B”接口的所有,即我们想要的将组件A限制为组件B提供的接口的子集。

问题/问题:

是否有一种系统的,相当直接的方式来强制执行 - 最好是在编译时 - 组件图上定义的接口契约?

显然,编译时解决方案优于运行时解决方案(早期检测,更好的性能,可能更小的代码)。

例如,假设库组件“B”提供函数X(),Y()和Z(),但我只希望组件“A”能够调用函数Z(),而不是X()和Y()。类似地,即使组件“A”可能能够通过其消息队列接收和处理大量不同的消息,我们也没有任何组件能够向任何组件发送任何消息。

我能想到的最好的方法是为每个组件 - 组件接口提供不同的头文件,并且只显示(通过头文件)允许组件使用的接口部分。显然这可能导致很多头文件。这也意味着在组件之间传递的消息不是直接用OS API完成的,而是通过函数调用完成的,每个函数调用都是通过函数调用来实现的。发送特定(允许)消息。对于同步调用/库,只会暴露API的允许子集。

对于本练习,您可以假设人们会表现得很好。换句话说,不要担心人们作弊行为。切割&直接粘贴函数原型,或包括不允许的头文件。如果不允许,他们不会直接将消息从“A”发布到“B”,等等......

也许有一种方法可以使用编译时断言强制执行合同。也许有一种更优雅的方式可以在运行时检查/强制执行此操作,即使它会产生一些开销。

代码必须编译&干净利落地,所以“功能原型防火墙”的方法还可以,但似乎可能有更惯用的方法来做到这一点。

3 个答案:

答案 0 :(得分:2)

标题的想法是合理的,但是,根据组件之间的隔行扫描,将每个组件的界面划分为多个具有自己的头文件的子类别而不是提供头文件可能更清晰对于每个组件 - 组件连接。

子类别不一定是不相交的,但要确保(通过预处理器指令)可以混合类别而不需要重新定义;这可以通过系统的方式实现,为每个类型或函数声明创建一个头文件,使用自己的包含保护,然后从这些原子块构建子类头。

答案 1 :(得分:2)

#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 */

答案 2 :(得分:1)

你应该考虑创建一个迷你语言和一个简单的工具来生成nategoose proposed in his answer的行头文件。

要在该答案中生成标题,请执行以下操作(称之为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);

(并扩展示例以提供可由多个组件使用的接口):

[COMPONENT_B, COMPONENT_C]
int foo_func4(void);

这可以直接解析并生成头文件。如果你的接口(我特别怀疑消息传递可能是)比我上面假设的更加模板化,你可以稍微简化语言。

这里的优点是:

  1. 一些语法糖,使维护更容易。
  2. 如果稍后发现更好的方法,可以通过更改工具来更改保护方案。将会有更少的更改地点,这意味着您更有可能进行更改。 (例如,您可能稍后会找到nategoose建议的“非法宏代码”的替代方法。)