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

时间:2014-06-18 10:31:35

标签: c++ c arduino embedded compile-time

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

以下是我想做的一个例子(但不起作用)。

central.h:

typedef void (*callback_t)(void);

callback_t callbacks[];

central.c:

#include "central.h"

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

foo.c的:

#include "central.h"

void callback_foo(void) { }

callback_t callbacks[] = {
    &callback_foo
};

bar.c:

#include "central.h"

void callback_bar(void) { }

callback_t callbacks[] = {
    &callback_bar
};

我想要的是获得一个callbacks数组,其中包含 两个元素:&callback_foo&callback_bar。有了上面的代码,就有了 显然有两个问题:

  • 多次定义callbacks数组。
  • 编译sizeof(callbacks)
  • central.c未知。

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

我意识到这个问题的一个常见解决方案就是拥有一个创业公司 “注册”回调的函数或构造函数。但是,我只能看到 两种实现方法:

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

由于我在内存有限的微控制器平台(Arduino)上运行, 这些方法都不适合我。并给出了整个内容 数组在编译时是已知的,我希望有一种方法让编译器 也看到了这一点。

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

This solution是迄今为止我发现的最好的。它使用链表 在运行时填充,但使用每个静态分配的内存 单独编译单元(例如,为每个分配下一个指针 功能指针)。但是,这些下一个指针的开销不应该 需要 - 有更好的方法吗?

也许将动态解决方案与链接时优化相结合即可 不知何故导致静态分配?

尽管如此,也欢迎有关替代方法的建议 元素具有静态的事物列表和记忆效率。

此外:

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

3 个答案:

答案 0 :(得分:4)

正如之前的一些回答所述,最好的选择是使用自定义链接描述文件(带KEEP(*(SORT(.whatever.*)))输入部分)。

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

<强>假设:

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

怎么做:

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

这是非常具体的,因为正确的方法是使用自定义链接描述文件,但如果我们在标准链接描述文件中找到一个始终&#34;保持&#34;和&#34;排序&#34;。

通常情况下,.ctors.*输入部分都是如此(标准要求C ++构造函数按函数名称顺序执行,并且在标准ld脚本中实现如此),所以我们可以破解一点并尝试一下。

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

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

test.c:

一个库,它注册一个名为test的模块,并在某个时刻调用它的回调

#include "callback.h"

CALLBACK_LIST(test);

void do_something_and_call_the_callbacks(void) {

        // ... doing something here ...

        CALLBACKS(test);

        // ... doing something else ...
}

<强> callme1.c:

客户端代码为模块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...

<强> 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...

<强> callback.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

<强> main.c中:

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

void do_something_and_call_the_callbacks(void);

int main() {
    do_something_and_call_the_callbacks();
}

<强>输出:

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

app/callme1.c: _test_callback_8
app/callme1.c: _test_callback_4
app/callme2.c: _test_callback_4

在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

链接器映射:

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

 *(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)

答案 1 :(得分: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];
    
  • 你无法避免&#34; LARGE_ENOUGH_FOR_WORST_CASE&#34;。您使用的是具有有限RAM的嵌入式系统,因此您必须实际考虑最坏情况,并为此保留足够的内存,不多也不少。在微控制器嵌入式系统中,通常不需要&#34;通常需要&#34;并且&#34;让我们为其他进程保存一些RAM&#34;。你的MCU有足够的内存来覆盖最坏的情况,或者它没有,在这种情况下,没有多少聪明的分配可以节省你。

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

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

基于Flash的只读数组

(优点:保存RAM,真正的只读内存可以防止内存损坏错误。缺点:灵活性较差,取决于项目中使用的每个模块,可能会稍微慢一点,因为它在闪存中。)

在这种情况下,您必须将整个程序颠倒过来。根据编译时解决方案的性质,它将更加严格地编码&#34;。

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

// callback.c

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

static const callback_t CALLBACKS [] = 
{
  &timer_callback1,
  &timer_callback2,
  &spi_callback,
  ...
};

这样做的好处是,您可以自动获得由您自己的程序传递给您的最坏情况。数组的大小现在在编译时已知,它只是sizeof(CALLBACKS)/sizeof(callback_t)

当然,这并不像通用回调模块那样优雅。从回调模块到项目中的每个其他模块都会有紧密耦合,但不是相反。从本质上讲,callback.c是一个&#34; main()&#34;。

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

答案 2 :(得分:0)

我也面临类似的问题:

  

...需要多个回调函数,它们是以各种方式定义的   模块,彼此之间没有丝毫相关。

Mine是C,在Atmel XMega处理器上。您提到您正在使用GCC。以下并不能解决您的问题,它是上述#1解决方案的变体。它利用__attribute__((weak))指令。

1)对于每个可选模块,具有唯一(每个模块名称)但类似(每个用途)的回调函数。 E.g。

fooModule.c:
void foo_eventCallback(void) {
    // do the foo response here
}

barModule.c:
void bar_eventCallback(void) {
    // do the bar response here
}

yakModule.c:
void yak_eventCallback(void) {
    // do the yak response here
}

2)有一个类似于:

的回调起点
__attribute__((weak)) void foo_eventCallback(void) { }
__attribute__((weak)) void bar_eventCallback(void) { }
__attribute__((weak)) void yak_eventCallback(void) { }

void functionThatExcitesCallback(void) {
    foo_eventCallback();
    foo_eventCallback();
    foo_eventCallback();
}

__attribute__((weak))限定符基本上创建了一个带有空体的默认实现,如果它找到一个同名的非弱变体,链接器将替换为另一个变体。不幸的是,它并没有完全解耦。但你至少可以把这个超级全套的回调放在一个且只有一个地方,而不是用它进入头文件地狱。然后你的不同编译单元基本上替换他们想要的超集的子集。我希望如果有一种方法可以在所有模块中使用相同的命名函数,并且根据链接的内容调用那些,但是还没有找到能够做到这一点的东西。