没有函数指针的 C 中的回调

Callback in C without function pointers

在一些通信堆栈遗留代码中,我发现了一种在 C 中执行回调的非回调方式。我的问题是:为什么这样做,或者这种方法相对于传递函数指针的常用方式有什么优势回调函数?

这是我发现的伪代码示例:

top_interfaces.h

void sendData(int);
void receiveDataHAL(int);

顶层组件

#include "top_interfaces.h"
void sendData(int){ send(int); //call hal function }
void receiveDataHAL(int) { //use data }

hal_interfaces.h

void send(int);
void receive(int);

硬件抽象层

#include "hal_interfaces.h"
#include "top_interfaces.h"
void send(int){ //do sending over bus }
void receive(int){ receiveDataHAL(int); }

下层组件调用包含声明的上层函数。上层组件实现了这个类似回调的功能。 这样下层组件就知道上层组件,这通常是一个糟糕的设计选择。引入如此大的依赖性是否有充分的理由?

receiveDataHAL()视为“顶层”组件甚至“回调”是错误的。它只是实现者提供的一个必需的接口——它是一个 硬件特定的 函数,由 硬件抽象 层调用,所以是显然比抽象层低。这并没有什么不寻常或错误的地方。 receiveDataHAL()在图层层次结构中的位置无论是通过指针调用还是静态调用都没有区别link。

如果你有一个较低层的库,用户提供了“需要的接口”,你可以按照你的建议通过函数指针回调来实现它层必须通过提供回调来“初始化”库。这是一个 运行-time linkage 的优点是只有函数签名需要匹配,函数名称是任意的,实现也可以动态改变(尽管如果它实际上不是必需的)。它的缺点是需要 运行 次初始化,失败是 运行 次问题。

另一种方法是通过名称指定所需的函数并静态地link它。然后更高层必须通过名称和签名定义所需的接口,否则 link 将失败。 link 时间故障在大多数情况下优于 运行 时间故障,可能会或可能不会处理,并且处理不太可能特别优雅(并且 更代码).

通常 所需的接口 可能由 "weak symbol" 定义 - 这是默认实现,如果未定义覆盖,则将 linked。例如,这种情况下的硬件抽象层可能有:

void __attribute__((weak)) receiveDataHAL(int) { // do nothing }

然后如果你不需要它,你可以在不显式定义的情况下使用抽象层receiveDataHAL()

函数指针是一个变量。在不需要变量的地方,静态 link 通常更可取,具有 link 时间错误检查和静态不可修改 linkage 的优点。我不会称它为“糟糕的设计选择”,因为在这种情况下 receiveDataHAL() 并不是真正的“上层”组件,而是用户提供的下层组件——无论您是通过函数指针调用它,都是如此静态 link.

该技术 - 使用弱符号实现 - 例如在 ARM Cortex-M CMSIS 中用于中断和异常处理程序,以便支持中断,您只需要覆盖适当的“知名”函数名称即可直接分配一个函数作为中断处理程序。例如,定义 SYSTICK 处理程序非常简单:

void SysTick_Handler(void)  
{
    msTicks++ ;
}

其中startup_.s中有代码例如:

    .weak   SysTick_Handler
    .type   SysTick_Handler, %function
SysTick_Handler:
    B       .

定义默认处理程序 - 在本例中为无限循环。

另一个不使用弱 links 的例子是 Newlib C 库系统调用存根。这些是一组用户提供的调用,用于使库适应平台。 link加入 Newlib 的应用程序必须提供系统调用函数 - 这并不意味着它们是 应用程序层 函数。