簡體   English   中英

在鏈接時合並全局數組/從多個編譯單元填充全局數組

[英]Merging global arrays at link time / filling a global array from multiple compilation units

我想定義一系列事物,例如事件處理程序。 該數組的內容在編譯時是完全已知的,但在多個編譯單元之間定義,分布在相當解耦的多個庫之間,至少直到最終(靜態)鏈接為止。 我也想保持這種狀態-因此添加或刪除編譯單元也將自動管理事件處理程序,而無需修改事件處理程序的中央列表。

這是我想做的一個例子(但不起作用)。

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數組被多次定義。
  • 編譯central.c時不知道sizeof(callbacks)

在我看來,可以通過使鏈接器合並兩個callbacks符號而不是拋出錯誤(可能通過變量的某些屬性)來解決第一點,但是我不確定是否存在類似的東西。 即使存在,sizeof問題也應該以某種方式解決。

我意識到,解決此問題的常見方法是僅具有一個“注冊”回調的啟動函數或構造函數。 但是,我只能看到兩種實現方法:

  • 為回調數組使用動態內存(重新分配)。
  • 使用固定大小(比通常所需大)的靜態內存。

由於我在內存有限的微控制器平台(Arduino)上運行,因此這兩種方法都不吸引我。 鑒於在編譯時就知道了數組的全部內容,我希望找到一種方法讓編譯器也能看到這一點。

我發現這個解決方案,但那些需要自定義鏈接腳本,這是不是我跑的編譯環境是可行的(特別是沒有,因為這將需要明確地在連接腳本命名這些特殊的陣列,所以才僅添加一個鏈接描述文件在這里不起作用)。

該解決方案是我到目前為止找到的最好的解決方案 它使用一個在運行時填充的鏈表,但是使用分別在每個編譯單元中靜態分配的內存(例如,下一個指針與每個函數指針一起分配)。 盡管如此,這些下一個指針的開銷仍然不需要-是否有更好的方法?

也許將動態解決方案與鏈接時間優化相結合可以以某種方式導致靜態分配?

也歡迎提出有關替代方法的建議,盡管必需的元素具有靜態的事物清單和內存效率。

此外:

  • 使用C ++很好,我只是在上面使用了一些C代碼來說明問題,無論如何,大多數Arduino代碼都是C ++。
  • 我正在使用gcc / avr-gcc,盡管我更喜歡便攜式解決方案,但是僅使用gcc的東西也是可以的。
  • 我有可用的模板支持,但沒有STL。
  • 在我使用的Arduino環境中,我沒有Makefile或其他方式可以在編譯時輕松運行一些自定義代碼,因此我正在尋找可以完全在代碼中實現的東西。

如先前的回答所述,最好的選擇是使用自定義鏈接描述文件(帶有KEEP(*(SORT(.whatever.*)))輸入部分)。

無論如何,至少在某些使用gcc的平台(在xtensa嵌入式設備和cygwin上進行了測試)下, 無需修改鏈接描述文件 (下面的工作示例代碼) 就可以完成此操作。

假設:

  • 我們希望盡可能避免使用RAM(嵌入式)
  • 我們不希望調用模塊對帶有回調的模塊有任何了解(這是一個庫)
  • 列表沒有固定大小(庫編譯時大小未知)
  • 我正在使用GCC。 該原理可能適用於其他編譯器,但我尚未對其進行測試
  • 此示例中的回調函數不接收任何參數,但是如果需要的話,修改非常簡單

怎么做:

  • 我們需要鏈接器以某種方式在鏈接時分配指向函數的指針數組
  • 由於我們不知道數組的大小,因此我們還需要鏈接器以某種方式標記數組的結尾

這是非常具體的,因為正確的方法是使用自定義鏈接描述文件,但是如果我們在標准鏈接描述文件中找到始終“保留”和“排序”的部分,那么這樣做就很可行。

通常, .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)

嘗試解決實際問題。 您需要的是多個回調函數,這些函數在各個模塊中定義,彼此之間幾乎沒有任何關系。

但是,您要做的是將全局變量放在頭文件中,每個包含該頭的模塊都可以訪問該文件。 即使它們彼此不相關,這也會在所有這些文件之間引入緊密的耦合。 此外,似乎只有回調處理程序.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]; 
  • 您無法避免“ LARGE_ENOUGH_FOR_WORST_CASE”。 您所在的RAM有限的嵌入式系統上,因此您必須實際考慮最壞的情況,並為此預留足夠的內存。 在微控制器嵌入式系統上,沒有“通常需要”之類的東西,也沒有“讓其他進程節省一些RAM”的事情。 您的MCU有足夠的內存來應付最壞的情況,或者沒有,在這種情況下,沒有任何巧妙的分配可以節省您的時間。

  • 在callback.c中,聲明一個size變量,該變量跟蹤已初始化的回調數組的數量。 static size_t callback_size;

  • 編寫一個初始化函數void callback_init(void)來初始化回調模塊。 原型應位於.h文件中,並且在程序啟動時,調用方負責執行一次。
  • 在init函數中,將callback_size設置為0。之所以建議在運行時執行此操作,是因為您有一個嵌入式系統,其中可能不存在甚至不希望出現.bss段。 您甚至可能沒有將所有靜態變量都初始化為零的復制代碼。 這種行為與C標准不符,但在嵌入式系統中非常常見。 因此,切勿編寫依賴靜態變量自動初始化為零的代碼。
  • 編寫一個函數void callback_add (callback_t* callback); 包括回調模塊的每個模塊都將調用此函數,以將其特定的回調函數添加到列表中。
  • 保持do_callbacks函數do_callbacks (盡管有一do_callbacks說,請考慮重命名為callback_traverse,callback_run或類似名稱)。

基於Flash的只讀陣列

(優點:節省RAM,真正的只讀內存,防止內存損壞錯誤。缺點:靈活性較差,取決於項目中使用的每個模塊,訪問速度可能稍慢,因為它在閃存中。)

在這種情況下,您必須將整個程序顛倒過來。 根據編譯時解決方案的性質,它將更加“硬編碼”。

不必使多個不相關的模塊(包括回調處理程序模塊),而必須使回調處理程序模塊包含其他所有內容。 各個模塊仍然不知道何時執行回調或將回調分配到何處。 他們只是將一個或幾個函數聲明為回調。 然后,回調模塊負責在編譯時將每個此類回調函數添加到其數組中。

// 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是一個“ main()”。

雖然您仍然可以在callback.h中使用函數指針typedef,但實際上不再需要它:各個模塊必須確保無論如何都以所需的格式編寫其回調函數,無論是否存在這種類型。

我也面臨着類似的問題:

...需要多個回調函數,這些函數在各個模塊中定義,彼此之間幾乎沒有任何關系。

我的是C,在Atmel XMega處理器上。 您提到您正在使用GCC 以下內容無法解決您的問題,它是上述#1解決方案的變體。 它利用了__attribute__((weak))指令。

1)對於每個可選模塊,都有一個唯一的(每個模塊名稱)但相似的(每個目的)回調函數。 例如

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))限定符基本上會創建一個帶有空主體的默認實現,如果鏈接器發現同名的非弱變體,則鏈接器將用另一個變體替換。 不幸的是,它並沒有使其完全脫鈎。 但是,您至少可以將這種大型的超大型所有回調集放在一個且只有一個位置,並且不要因此而進入頭文件地獄。 然后,不同的編譯單元基本上會替換它們想要的超集的子集。 如果有一種方法可以在所有模塊中使用相同的命名函數,並且僅根據鏈接的內容進行調用,但還沒有找到能做到這一點的方法,我會喜歡它。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM