65.9K
CodeProject 正在变化。 阅读更多。
Home

C 语言中的类型安全多播回调

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (11投票s)

2017 年 6 月 9 日

CPOL

5分钟阅读

viewsIcon

14413

downloadIcon

317

一个用于在 C 语言中实现匿名函数调用的类型安全多播回调库

引言

函数指针用于减少两个代码片段之间的耦合。一个发布者定义一个回调函数签名,并允许匿名注册函数指针。一个订阅者创建一个符合发布者回调签名的函数实现,并在运行时向发布者注册该函数的指针。发布者代码对订阅者代码一无所知——注册和回调调用是匿名的。

多播回调允许两个或多个订阅者注册以通过回调获得通知。当发布者调用回调时,所有已注册的订阅者函数都会按顺序被调用。

本文提供了一个用 C 语言实现的简单、类型安全的 C 语言多播回调模块。

查看 GitHub 以获取最新源代码

查看相关的 GitHub 仓库

回调背景

软件系统被组织成不同的软件模块。模块的入站接口在接口文件中声明。出站接口可以通过在运行时注册和调用的函数指针来表达。发布者模块通过调用匿名函数 via a function pointer 来通知订阅者。“匿名”意味着发布者代码不包含任何订阅者头文件。 订阅者知道发布者,但发布者不知道订阅者。这样,当新的订阅者想要接收回调通知时,发布者代码模块就不会改变。

例如,假设我们的系统有一个警报模块。它负责处理检测到的警报。现在,系统内的其他模块可能对接收警报通知感兴趣。也许 GUI 需要向用户显示警报。日志模块可能会将警报保存到持久存储中。而执行器模块可能需要停止电机运动。理想情况下,警报模块不应该知道 GUI、日志或执行器模块。取而代之的是,订阅者向警报模块注册以获取通知,以便当发生警报时,每个订阅者的警报处理函数都会被调用。

Using the Code

我将首先介绍如何使用代码,然后深入探讨实现细节。

MULTICASTX_DECLARE 宏在发布者头文件中公开多播回调接口。“X”是表示回调中函数参数数量的数字。例如,如果回调函数有两个函数参数,则使用 MULTICAST2_DECLARE。该库支持 0 到 5 个参数。

第一个参数是回调名称。其他参数是函数参数类型。下面的宏定义了一个函数签名 `void MyFunc(int, float)`。

MULTICAST2_DECLARE(MyCallback, int, float)

MULTICASTX_DEFINE 宏在发布者源文件中实现多播回调接口。该宏放置在文件作用域。宏参数是回调名称、函数参数类型以及允许的最大注册者数量。在下面的示例中,最多可以注册 5 个函数指针。

MULTICAST2_DEFINE(MyCallback, int, float, 5)

这两个宏完整地实现了一个类型安全的 C 语言多播回调接口。宏根据提供的宏参数自动创建三个函数。

void MyCallback_Register(MyCallbackCallbackType callback);
void MyCallback_Unregister(MyCallbackCallbackType callback);
void MyCallback_Invoke(int val1, float val2);

订阅者使用 Register() 函数注册回调。

MyCallback_Register(&NotificationCallback);

同样,订阅者使用 Unregister() 函数取消注册。

MyCallback_Unregister(&NotificationCallback);

发布者使用 Invoke() 函数按顺序调用所有已注册的回调。

MyCallback_Invoke(123, 3.21f);

SysData 示例

SysData 是一个简单的模块,展示了如何公开一个出站多播回调接口。SysData 存储系统数据,并在模式更改时提供订阅者通知。下面的代码显示了接口。

#ifndef _SYSDATA_H
#define _SYSDATA_H

#include "multicast.h"

typedef enum
{
   MODE_STARTING,
   MODE_NORMAL,
   MODE_ALARM
} ModeType;

// Publisher declares a multicast callback called SysData_SetModeCallback.
// Subscribers register for callbacks with function signature: void MyFunc(ModeType mode)
MULTICAST1_DECLARE(SysData_SetModeCallback, ModeType)

// Set a new system mode and callback any clients registered with SysData_SetModeCallback.
void SysData_SetMode(ModeType mode);

#endif // _SYSDATA_H

下面的代码显示了 SysData 的实现。

#include "sysdata.h"

// Define the multicast callback for up to 3 registered clients
MULTICAST1_DEFINE(SysData_SetModeCallback, ModeType, 3)

static ModeType _mode = MODE_STARTING;

void SysData_SetMode(ModeType mode)
{
   // Update the private _mode value
   _mode = mode;
   
   // Invoke callbacks on all registered clients
   SysData_SetModeCallback_Invoke(_mode);
}

订阅者通过创建回调函数并在运行时注册函数指针来连接到 SysData

void SysDataCallback1(ModeType mode)
{
   printf("ModeCallback1: %d\n", mode);
}

int main(void)
{
   // Register with SysData for callbacks
   SysData_SetModeCallback_Register(&SysDataCallback1);

   // Call SysData to change modes
   SysData_SetMode(MODE_STARTING);
   SysData_SetMode(MODE_NORMAL);

   return 0;
}

请注意,每次调用 SysData_SetMode() 时都会调用 SysDataCallback1()。另请注意,SysData 不知道订阅者,因为注册是匿名的。

实现

该实现使用宏和标记粘贴(token pasting)为使用多播回调提供类型安全的接口。标记粘贴运算符 (##) 用于在预处理器扩展宏时合并两个标记。下面显示了 MULTICAST1_DECLARE 宏。

#define MULTICAST1_DECLARE(name, arg1) \
    typedef void(*name##CallbackType)(arg1 val1); \
    void name##_Register(name##CallbackType callback); \
    void name##_Unregister(name##CallbackType callback);

在上面使用的 SysData 示例中,宏展开为:

typedef void(*SysData_SetModeCallbackCallbackType)(ModeType val1);
void SysData_SetModeCallback_Register(SysData_SetModeCallbackCallbackType callback);
void SysData_SetModeCallback_Unregister(SysData_SetModeCallbackCallbackType callback);

请注意,每个 name## 位置都替换为宏名称参数,在本例中为声明中的 SysData_SetModeCallback

MULTICAST1_DECLARE(SysData_SetModeCallback, ModeType)

下面的代码显示了实现宏。

#define MULTICAST1_DEFINE(name, arg1, max) \
   static CB_Data name##Multicast[max]; \
      void name##_Register(name##CallbackType callback) { \
   CB_MulticastAddCallback(&name##Multicast[0], max, (CB_CallbackType)callback); } \
      void name##_Unregister(name##CallbackType callback) { \
   CB_MulticastRemoveCallback(&name##Multicast[0], max, (CB_CallbackType)callback); } \
       void name##_Invoke(arg1 val1) { \
           for (size_t idx=0; idx<max; idx++) { \
               name##CallbackType callback = (name##CallbackType)CB_MulticastGetCallback
                                             (&name##Multicast[0], max, idx); \
               if (callback != NULL) \
                   callback(val1); } }

SysData 示例展开的 MULTICAST1_DEFINE 结果如下:

static CB_Data SysData_SetModeCallbackMulticast[3];

void SysData_SetModeCallback_Register(SysData_SetModeCallbackCallbackType callback)
{
   CB_MulticastAddCallback(&SysData_SetModeCallbackMulticast[0], 3, (CB_CallbackType)callback);
}

void SysData_SetModeCallback_Unregister(SysData_SetModeCallbackCallbackType callback)
{
   CB_MulticastRemoveCallback(&SysData_SetModeCallbackMulticast[0], 3, (CB_CallbackType)callback);
}

void SysData_SetModeCallback_Invoke(ModeType val1)
{
   for (size_t idx=0; idx<3; idx++)
   {
       SysData_SetModeCallbackCallbackType callback =
          (SysData_SetModeCallbackCallbackType)CB_MulticastGetCallback
                               (&SysData_SetModeCallbackMulticast[0], 3);
   
       if (callback != NULL)
           callback(val1);
   }
}

请注意,该宏提供了 CB_MulticastAddCallback()CB_MulticastRemoveCallback() 函数的一个薄而类型安全的包装器。如果注册了错误的函数签名,编译器会生成错误或警告。宏会自动处理您通常手写的那种单调的样板代码。

回调函数只是存储在一个数组中。Invoke() 函数只需遍历回调函数数组并调用任何非 NULL 的元素。

回调的添加/删除函数只是存储一个通用的 CB_CallbackType,供样板宏代码提取和使用。

#include "multicast.h"
#include <stdbool.h>
#include <assert.h>

void CB_MulticastAddCallback(CB_Data* cbData, size_t cbDataLen, CB_CallbackType callback)
{
    bool success = false;

    if (cbData == NULL|| callback == NULL || cbDataLen == 0)
    {
        assert(0);
        return;
    }

    // TODO - software lock

    // Look for an empty registration within the callback array
    for (size_t idx = 0; idx<cbDataLen; idx++)
    {
        if (cbData[idx].callback == NULL)
        {
            // Save callback function pointer
            cbData[idx].callback = callback;
            success = true;
            break;
        }
    }

    // TODO - software unlock

    // All registration locations full?
    if (success == false)
    {
        assert(0);
    }
}

void CB_MulticastRemoveCallback(CB_Data* cbData, size_t cbDataLen, CB_CallbackType callback)
{
    if (cbData == NULL || callback == NULL || cbDataLen == 0)
    {
       assert(0);
       return;
    }

    // TODO - software lock

    // Look for an empty registration within the callback array
    for (size_t idx = 0; idx<cbDataLen; idx++)
    {
        if (cbData[idx].callback == callback)
        {
            // Remove callback function pointer
           cbData[idx].callback = NULL;
           break;
        }
    }

    // TODO - software unlock
}

CB_CallbackType CB_MulticastGetCallback(CB_Data* cbData, size_t cbDataLen, size_t idx)
{
    if (cbData == NULL || cbDataLen == 0 || idx >= cbDataLen)
    {
       assert(0);
       return NULL;
    }

    // TODO - software lock

    CB_CallbackType cb = cbData[idx].callback;

    // TODO - software unlock

    return cb;
}

多线程

多播库可以用于单线程或多线程系统。如果在带有操作系统线程的系统上使用,它可以做到线程安全。`multicast.c` 文件包含有关放置软件锁的注释。一旦放置了锁,多播 API 和宏就可以安全地部署在多线程系统中,其中来自另一个线程的订阅者可以注册和取消注册。

在多线程系统中,如果订阅者代码在单独的线程上执行,那么通常订阅者的回调函数实现会将消息发布到线程队列,以便异步处理。这样,生成回调的发布者线程就不会被阻塞,订阅者代码可以通过 OS 消息队列异步处理通知。

结论

多播回调消除了模块之间不必要的代码耦合。注册多个回调函数指针提供了一个方便的通知系统。本文演示了一种可以统一部署在系统中的可能设计。实现被保持在最低限度,便于在任何嵌入式或其他系统上使用。少量宏代码可以自动处理样板代码,并为 C 语言库提供类型安全性。

历史

  • 2017 年 6 月 9 日:首次发布
© . All rights reserved.