代码之家  ›  专栏  ›  技术社区  ›  Dan

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

  •  4
  • Dan  · 技术社区  · 14 年前

    背景:

    我们正在为一个新的嵌入式系统建模固件。目前固件是在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”,等等…

    也许有一种方法可以用编译时断言来执行契约。也许有一种更优雅的方法可以在运行时检查/执行这个,即使它会产生一些开销。

    代码必须干净地编译&lint,所以“函数原型防火墙”方法是可以的,但似乎有一种更惯用的方法可以做到这一点。

    3 回复  |  直到 14 年前
        1
  •  2
  •   Christoph    14 年前

    使用头的想法是合理的,但是,根据组件之间的交错,将每个组件的接口划分为多个子类别,并使用它们自己的头文件,而不是为每个组件组件连接提供头文件,这可能更为简单。

    子类别不一定是不相交的,但要确保(通过预处理器指令)可以在不重新定义的情况下混合类别;这可以通过为每个类型或函数声明创建一个带有其自身包含保护的头文件来系统地实现,以及然后从这些原子块构建子类别头。

        2
  •  2
  •   nategoose    14 年前
    #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 */
    
        3
  •  1
  •   Community Tales Farias    7 年前

    您应该考虑创建一个小型语言和一个简单的工具来沿着 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建议的“非法宏代码”的替代方案。)