首页 > 解决方案 > 没有函数指针的 C 回调

问题描述

在一些通信堆栈遗留代码中,我发现了一种在 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); }

下层组件调用包含声明的上层函数。上层组件实现了这个类回调函数。这样下层组件就知道上层组件,这通常是一个糟糕的设计选择。引入这种依赖量有什么好的理由吗?

标签: cpointerscallback

解决方案


receiveDataHAL()将其视为“顶层”组件甚至“回调”是错误的。它只是实现者提供的必需接口——它是由硬件抽象层调用的特定于硬件的函数,因此明显低于抽象层。这没有什么不寻常或有什么不妥之处。无论是通过指针调用还是静态链接,在层层次结构中的位置都没有什么不同。receiveDataHAL()

如果您有一个具有用户提供的“必需接口”的较低层库,您可以按照您的建议通过函数指针回调来实现它,其中较高层必须通过提供回调来“初始化”库。这是一个运行时链接,其优点是只有函数签名需要匹配,函数名称是任意的,并且实现也可以动态更改(尽管如果实际上不需要,这可能不是一个优势)。它的缺点是需要运行时初始化,并且失败是运行时问题。

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

通常,所需的接口可能由“弱符号”定义- 这是一个默认实现,如果没有定义覆盖,它将链接。例如,这种情况下的硬件抽象层可能有:

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

然后您可以使用抽象层而无需明确定义receiveDataHAL()是否不需要它。

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

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

void SysTick_Handler(void)  
{
    msTicks++ ;
}

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

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

它定义了默认的处理程序——在这种情况下是一个无限循环。

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


推荐阅读