【问题标题】:How to enforce interface contracts (in C) at compile time?如何在编译时强制执行接口合同(用 C 语言)?
【发布时间】:2010-10-26 19:54:41
【问题描述】:

背景:

我们正在为新的嵌入式系统建模固件。目前固件正在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,所以“函数原型防火墙”方法是可以的,但似乎有一种更惯用的方法来做到这一点。

【问题讨论】:

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


    【解决方案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 */
    

    【讨论】:

    • 我更喜欢 Dan 的原始方法:如果有一个组件几乎可以被所有其他组件访问,那么 #if defined(FOO_COMPONENT_A) || defined(FOO_COMPONENT_B) || ... 的可读性将远低于提供单独的标头
    • 感谢您的回答。现在我正在尝试权衡/评估不同方法的可扩展性。我们过去看到的一个问题是剪切和粘贴错误和/或缺乏对细节的关注让事情溜走了。例如(听到这个不要尖叫),我继承的一个头文件是 6000 行长。哎哟。
    【解决方案3】:

    您应该考虑创建一种迷你语言和一个简单的工具来生成头文件,类似于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 提出的“非法宏代码”的替代方案。)

    【讨论】:

    • 如果它所做的只是用 #if defined(COMPONENT_A) || defined(COMPONENT_B) ... #endif 替换 [COMPONENT_A, COMPONENT_B],我真的看不出创建自定义预处理器的意义
    • 这取决于它的使用频率,这可能是矫枉过正(因为该语言必须足够好地理解 C 才不会搞砸),但它可以帮助您保留所有预处理器条件块直接。但是,如果它重新排序(组合)代码块,则必须小心,因为可能存在基于顺序的依赖关系。
    • 它以一定的成本购买了灵活性。如果组件之间的关系需要更改和维护,那么从灵活性中获得的收益可能是值得的。 “它所做的只是替换......”不一定是真的 - 它可以生成单独的头文件。如果 API 和/或消息是样板文件,都符合给定的接口,那么它可以简化声明。它可以使重构更容易,因为只有一个地方可以改变。同样,我并不是说它在所有情况下都是必要的,甚至是可取的。只是需要考虑的事情。
    • @btspierre - 感谢您的回答。实际上,小组中有人正在努力创建这样的工具。内部对此有一些反对意见:1)团队过去在使用本土工具(在我之前)方面有过糟糕的经历——但可能是更复杂的工具; 2) 需要文档(甚至是简单的文档),并且文档往往会被绕过/忽略; 3)“被公共汽车撞”的想法 - 如果工具维护者退出等怎么办。另外我想我们需要确保工具在构建期间始终运行以重新生成标题。
    • @Dan - 是的,这些都是需要考虑的有效因素。
    猜你喜欢
    • 2014-11-05
    • 2013-09-01
    • 1970-01-01
    • 2014-02-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多