在链接时合并全局数组/从多个编译单元填充全局数组

Mat*_*man 10 c c++ embedded arduino compile-time

我想定义一组事物,比如事件处理程序.这个数组的内容在编译时是完全已知的,但是在多个编译单元之间定义,分布在多个相当分离的库中,至少直到最终(静态)链接.我也希望保持这种方式 - 因此添加或删除编译单元也将自动管理事件处理程序,而无需修改事件处理程序的中央列表.

这是我想做的一个例子(但不起作用).

central.h:

typedef void (*callback_t)(void);

callback_t callbacks[];
Run Code Online (Sandbox Code Playgroud)

central.c:

#include "central.h"

void do_callbacks(void) {
    int i;
    for (i = 0; i < sizeof(callbacks) / sizeof(*callbacks); ++i)
        callbacks[i]();
}
Run Code Online (Sandbox Code Playgroud)

foo.c的:

#include "central.h"

void callback_foo(void) { }

callback_t callbacks[] = {
    &callback_foo
};
Run Code Online (Sandbox Code Playgroud)

bar.c:

#include "central.h"

void callback_bar(void) { }

callback_t callbacks[] = {
    &callback_bar
};
Run Code Online (Sandbox Code Playgroud)

我想要发生的是获得一个callbacks包含两个元素的数组:&callback_foo&callback_bar.使用上面的代码,显然有两个问题:

  • callbacks阵列定义了多次.
  • sizeof(callbacks)编译时不知道central.c.

在我看来,第一点可以通过链接器合并两个callbacks符号而不是抛出错误(可能通过变量的某些属性)来解决,但我不确定是否有类似的东西.即使有,问题的大小也应该以某种方式解决.

我意识到这个问题的一个常见解决方案是只有一个"注册"回调的启动函数或构造函数.但是,我只能看到两种实现方法:

  • 使用动态内存(realloc)作为回调数组.
  • 使用固定(大于通常需要的)大小的静态内存.

由于我在内存有限的微控制器平台(Arduino)上运行,因此这些方法都不适合我.鉴于数组的全部内容在编译时是已知的,我希望有一种让编译器也能看到它的方法.

我已经找到了这个这个解决方案,但那些需要一个自定义链接器脚本,这在我正在运行的编译环境中是不可行的(特别是因为这需要在链接器脚本中明确命名这些特殊数组,所以只需要添加单个链接器脚本在这里不起作用).

这个解决方案是我迄今为止发现的最好的 它使用在运行时填充的链表,但是在每个编译单元中单独使用静态分配的内存(例如,为每个函数指针分配下一个指针).不过,不应该要求这些下一个指针的开销 - 有没有更好的方法?

也许结合链接时优化的动态解决方案可能会以某种方式导致静态分配?

关于替代方法的建议也是受欢迎的,尽管所需元素具有静态的事物列表和内存效率.

此外:

  • 使用C++很好,我只是使用上面的一些C代码来说明问题,大多数Arduino代码无论如何都是C++.
  • 我正在使用gcc/avr-gcc,虽然我更喜欢便携式解决方案,但只有gcc的东西也可以.
  • 我有模板支持,但不是STL.
  • 在我使用的Arduino环境中,我没有Makefile或其他方式在编译时轻松运行一些自定义代码,所以我正在寻找可以在代码中完全实现的东西.

Ped*_*dro 5

正如在之前的一些答案中所评论的,最好的选择是使用自定义链接器脚本(带有KEEP(*(SORT(.whatever.*)))输入部分)。

无论如何,它可以在不修改链接器脚本(下面的工作示例代码)的情况下完成,至少在一些带有 gcc 的平台上(在 xtensa 嵌入式设备和 cygwin 上测试)

假设:

  • 我们希望尽可能避免使用 RAM(嵌入式)
  • 我们不希望调用模块知道有关带有回调的模块的任何信息(它是一个库)
  • 列表没有固定大小(库编译时大小未知)
  • 我正在使用 GCC。该原理可能适用于其他编译器,但我没有测试过
  • 此示例中的回调函数不接收任何参数,但如果需要,修改起来非常简单

怎么做:

  • 我们需要链接器在链接时以某种方式分配指向函数的指针数组
  • 由于我们不知道数组的大小,我们还需要链接器以某种方式标记数组的末尾

这是非常具体的,因为正确的方法是使用自定义链接器脚本,但如果我们在标准链接器脚本中找到始终“保留”和“排序”的部分,则不这样做恰好是可行的。

通常,对于.ctors.*输入部分,这是正确的(标准要求按函数名称顺序执行 C++ 构造函数,并且在标准 ld 脚本中是这样实现的),因此我们可以稍微修改一下并尝试一下。

只是考虑到它可能不适用于所有平台(我已经在 xtensa 嵌入式架构和 CygWIN 中对其进行了测试,但这是一个黑客技巧,所以......)。

此外,当我们将指针放在构造函数部分时,我们需要使用一个字节的 RAM(对于整个程序)来跳过 C 运行时初始化期间的回调代码。


测试.c:

注册名为 的模块test并在某个时间调用其回调的库

#include "callback.h"

CALLBACK_LIST(test);

void do_something_and_call_the_callbacks(void) {

        // ... doing something here ...

        CALLBACKS(test);

        // ... doing something else ...
}
Run Code Online (Sandbox Code Playgroud)

callme1.c:

为 module 注册两个回调的客户端代码test。生成的函数没有名字(确实有名字,但是神奇地生成在编译单元内是唯一的)

#include <stdio.h>
#include "callback.h"

CALLBACK(test) {
        printf("%s: %s\n", __FILE__, __FUNCTION__);
}

CALLBACK(test) {
        printf("%s: %s\n", __FILE__, __FUNCTION__);
}

void callme1(void) {} // stub to be called in the test sample to include the compilation unit. Not needed in real code...
Run Code Online (Sandbox Code Playgroud)

callme2.c:

客户端代码正在为模块注册另一个回调test...

#include <stdio.h>
#include "callback.h"

CALLBACK(test) {
        printf("%s: %s\n", __FILE__, __FUNCTION__);
}

void callme2(void) {} // stub to be called in the test sample to include the compilation unit. Not needed in real code...
Run Code Online (Sandbox Code Playgroud)

回调.h:

还有魔法...

#ifndef __CALLBACK_H__
#define __CALLBACK_H__

#ifdef __cplusplus
extern "C" {
#endif

typedef void (* callback)(void);
int __attribute__((weak)) _callback_ctor_stub = 0;

#ifdef __cplusplus
}
#endif

#define _PASTE(a, b)    a ## b
#define PASTE(a, b)     _PASTE(a, b)

#define CALLBACK(module) \
        static inline void PASTE(_ ## module ## _callback_, __LINE__)(void); \
        static void PASTE(_ ## module ## _callback_ctor_, __LINE__)(void); \
        static __attribute__((section(".ctors.callback." #module "$2"))) __attribute__((used)) const callback PASTE(__ ## module ## _callback_, __LINE__) = PASTE(_ ## module ## _callback_ctor_, __LINE__); \
        static void PASTE(_ ## module ## _callback_ctor_, __LINE__)(void) { \
                 if(_callback_ctor_stub) PASTE(_ ## module ## _callback_, __LINE__)(); \
        } \
        inline void PASTE(_ ## module ## _callback_, __LINE__)(void)

#define CALLBACK_LIST(module) \
        static __attribute__((section(".ctors.callback." #module "$1"))) const callback _ ## module ## _callbacks_start[0] = {}; \
        static __attribute__((section(".ctors.callback." #module "$3"))) const callback _ ## module ## _callbacks_end[0] = {}

#define CALLBACKS(module) do { \
        const callback *cb; \
        _callback_ctor_stub = 1; \
        for(cb =  _ ## module ## _callbacks_start ; cb <  _ ## module ## _callbacks_end ; cb++) (*cb)(); \
} while(0)

#endif
Run Code Online (Sandbox Code Playgroud)

主文件:

如果你想试一试……这是一个独立程序的入口点(在 gcc-cygwin 上测试和工作)

void do_something_and_call_the_callbacks(void);

int main() {
    do_something_and_call_the_callbacks();
}
Run Code Online (Sandbox Code Playgroud)

输出:

这是我的嵌入式设备中的(相关)输出。函数名称在 at 生成callback.h并且可以有重复,因为函数是静态的

app/callme1.c: _test_callback_8
app/callme1.c: _test_callback_4
app/callme2.c: _test_callback_4
Run Code Online (Sandbox Code Playgroud)

而在 CygWIN 中...

$ gcc -c -o callme1.o callme1.c
$ gcc -c -o callme2.o callme2.c
$ gcc -c -o test.o test.c
$ gcc -c -o main.o main.c
$ gcc -o testme test.o callme1.o callme2.o main.o
$ ./testme
callme1.c: _test_callback_4
callme1.c: _test_callback_8
callme2.c: _test_callback_4
Run Code Online (Sandbox Code Playgroud)

链接器映射:

这是链接器生成的映射文件的相关部分

 *(SORT(.ctors.*))
 .ctors.callback.test$1    0x4024f040    0x0    .build/testme.a(test.o)
 .ctors.callback.test$2    0x4024f040    0x8    .build/testme.a(callme1.o)
 .ctors.callback.test$2    0x4024f048    0x4    .build/testme.a(callme2.o)
 .ctors.callback.test$3    0x4024f04c    0x0    .build/testme.a(test.o)
Run Code Online (Sandbox Code Playgroud)


Lun*_*din 1

努力解决实际问题。您需要的是在各个模块中定义的多个回调函数,这些回调函数彼此之间没有丝毫关联。

不过,您所做的是将全局变量放置在头文件中,包括该头文件的每个模块都可以访问该全局变量。这在所有此类文件之间引入了紧密耦合,即使它们彼此不相关。此外,似乎只有回调处理程序 .c 函数需要实际调用这些函数,但它们却暴露给整个程序。

所以这里的实际问题是程序设计而不是其他。

实际上没有明显的理由说明为什么需要在编译时分配这个数组。唯一合理的理由是节省 RAM,但这对于嵌入式系统来说当然是一个合理的理由。const在这种情况下,数组应该在编译时声明并初始化。

如果将数组存储为读写对象,您可以保留与您的设计类似的内容。或者,如果出于节省 RAM 的目的,该阵列必须是只读阵列,则必须进行彻底的重新设计。

我将提供两个版本,请考虑哪一个最适合您的情况:

基于RAM的读/写阵列

(优点:灵活,可以在运行时更改。缺点:消耗 RAM。初始化代码需要少量开销。RAM 比闪存更容易出现错误。)

  • 让callback.h和callback.c来自一个只关心回调函数处理的模块。该模块负责如何分配回调以及何时执行它们。
  • 在callback.h 中定义回调函数的类型。正如您所做的那样,这应该是函数指针类型。但从 .h 文件中删除变量声明。
  • 在callback.c中,将函数回调数组声明为

     static callback_t callbacks [LARGE_ENOUGH_FOR_WORST_CASE];
    
    Run Code Online (Sandbox Code Playgroud)
  • 您无法避免“LARGE_ENOUGH_FOR_WORST_CASE”。您使用的是 RAM 有限的嵌入式系统,因此您必须实际考虑最坏的情况是什么,并为此预留足够的内存,不多也不少。在微控制器嵌入式系统上,不存在“通常需要”或“为其他进程节省一些 RAM”之类的东西。您的 MCU 要么有足够的内存来应对最坏的情况,要么没有,在这种情况下,再巧妙的分配也救不了您。

  • 在callback.c 中,声明一个大小变量,用于跟踪已初始化的回调数组的大小。static size_t callback_size;

  • 编写一个 init 函数void callback_init(void)来初始化回调模块。原型应该位于 .h 文件中,调用者负责在程序启动时执行一次。
  • 在 init 函数内部,设置callback_size为 0。我建议在运行时执行此操作的原因是因为您有一个嵌入式系统,其中.bss段可能不存在,甚至不需要。您甚至可能没有将所有静态变量初始化为零的复制代码。这种行为不符合 C 标准,但在嵌入式系统中很常见。因此,永远不要编写依赖于自动初始化为零的静态变量的代码。
  • 写一个函数void callback_add (callback_t* callback);。包含回调模块的每个模块都会调用此函数以将其特定的回调函数添加到列表中。
  • 保持do_callbacks函数原样(尽管作为一个小备注,请考虑重命名为callback_traverse、callback_run 或类似名称)。

基于闪存的只读阵列

(优点:节省 RAM,真正的只读内存,不受内存损坏错误的影响。缺点:灵活性较差,取决于项目中使用的每个模块,访问速度可能会稍慢,因为它位于闪存中。)

在这种情况下,你必须把整个程序颠倒过来。根据编译时解决方案的性质,它将更加“硬编码”。

您必须使回调处理程序模块包含其他所有内容,而不是拥有多个不相关的模块(包括回调处理程序模块)。各个模块仍然不知道回调何时执行或分配在哪里。它们只是将一个或多个函数声明为回调。然后,回调模块负责在编译时将每个此类回调函数添加到其数组中。

// callback.c

#include "timer_module.h"
#include "spi_module.h"
...

static const callback_t CALLBACKS [] = 
{
  &timer_callback1,
  &timer_callback2,
  &spi_callback,
  ...
};
Run Code Online (Sandbox Code Playgroud)

这样做的好处是,您将自动获得您自己的程序交给您的最坏情况。数组的大小现在在编译时就已知,它很简单sizeof(CALLBACKS)/sizeof(callback_t)

当然,这并不像通用回调模块那么优雅。您可以从回调模块到项目中的每个其他模块紧密耦合,但反之则不然。本质上,callback.c 是一个“main()”。

不过,您仍然可以在callback.h中使用函数指针typedef,但实际上不再需要它:各个模块必须确保它们的回调函数无论如何都以所需的格式编写,无论存在或不存在这种类型。