2014-06-18 72 views
10

我想定義事件數組,如事件處理程序。 這個數組的內容在編譯時是完全已知的,但在多個編譯單元之間定義了 ,這些編譯單元分佈在多個庫中,至少在最終(靜態)鏈接之前,這些庫都是相互解耦的。我想 也保持這種方式 - 因此,添加或刪除編譯單元 也會自動管理事件處理程序,而不必修改事件處理程序的中心列表 。在鏈接時合併全局數組/從多個編譯單元填充全局數組

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

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或其他方式來在編譯時輕鬆運行一些自定義代碼,所以我正在尋找可以完全在代碼中實現的東西。
+0

您是否在尋找一個平臺* *獨立*的解決方案,或者您有一個特定的平臺?如果你把獨立性放在門外,各種怪異的東西都可以被利用(比如MS的鏈接器,它的部分名稱的字母順序,我個人的最愛之一)。否則,你可能會更好地使用init'er入口點(或者你提供的鏈接,這實際上是非常光滑的)。 – WhozCraig

+0

您是否考慮將回調數組構建爲makefile的一部分? –

+0

您已經爲C和C++標記了這一點,但對於其中一個很好的答案可能會是另一個答案。也許只是堅持其中之一。 – PlasmaHH

回答

2

試着解決實際問題。你需要的是多個回調函數,這些函數在各個模塊中定義,並沒有絲毫關聯。

雖然你做了什麼,但是在頭文件中放置了一個全局變量,每個模塊都可以訪問它,包括該頭文件。這引入了所有這些文件之間的緊密耦合,即使它們彼此不相關。此外,似乎只有回調處理程序.c函數需要實際調用函數,但它們會暴露給整個程序。

所以這裏的實際問題是程序設計,沒有別的。

實際上沒有明顯的理由說明爲什麼你需要在編譯時分配這個數組。唯一的理由是保存RAM,但這當然是嵌入式系統的有效理由。在這種情況下,該數組應聲明爲const並在編譯時初始化。

如果將數組作爲讀寫對象存儲,您可以保留與您的設計類似的內容。或者,如果數組必須是隻讀存儲器才能保存RAM,則必須進行劇烈的重新設計。

我給兩個版本,考慮哪一個是最適合你的情況:

基於RAM的讀/寫陣列

(優點:靈活,可以在運行時改變缺點:內存消耗,用於初始化的輕微頭代碼,RAM比閃存更容易受到bug的影響)

  • 讓模塊中的callback.h和callback.c只關心回調的處理功能。這個模塊負責分配回調的方式和執行時間。
  • 在callback.h中定義了回調函數的類型。這應該是一個函數指針類型,就像你所做的一樣。但是從.h文件中刪除變量聲明。
  • 在callback.c,申報的功能回調數組

    static callback_t callbacks [LARGE_ENOUGH_FOR_WORST_CASE]; 
    
  • 有沒有辦法可以避開 「LARGE_ENOUGH_FOR_WORST_CASE」。您處於內存有限的嵌入式系統中,因此您必須實際考慮最壞的情況,併爲此保留足夠的內存,不得多於,不少於。在微控制器嵌入式系統中,不存在「通常需要」或「爲其他進程節省一些RAM」之類的事情。您的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,真正的只讀存儲器從內存損壞漏洞安全缺點:不太靈活,取決於在項目中使用的每個模塊上,可能稍微慢一點接入,因爲它的閃光燈。)

在這種情況下,你必須把整個程序倒置。由於編譯時解決方案的性質,它將變得更加「硬編碼」。

而不是有多個不相關的模塊,包括回調處理程序模塊,您必須使回調處理程序模塊包括其他所有內容。各個模塊仍然不知道回調何時執行或分配的位置。他們只是將一個或多個函數聲明爲回調函數。回調模塊然後負責在編譯時將每個這樣的回調函數添加到它的數組中。

// 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,但它實際上不再需要:各個模塊必須確保它們的回調函數以任何方式以所需的格式寫入,無論有沒有這種類型當下。

+0

這是一個C解決方案,但C++將遵循相同的原則。只是將所有東西都包裝在類中,並使用更優雅的私有封裝,否則它將是同樣的事情。雖然請注意,對靜態初始化的擔憂更適用於具有靜態存儲持續時間的C++類對象:構造函數可能無法執行,或者您可能不希望在啓動時執行它們。 – Lundin

+1

感謝您的全面回答!但是,這兩種解決方案都無法解決我的問題。我的問題主要發生在編寫庫代碼時,它應該是通用的並且是分離的。第二種解決方案顯然打破了這種解耦。 但是,第一種解決方案還引入了數組聲明和提供回調的(個數)模塊之間的耦合。這只是一個小的耦合,但它仍然需要在添加或刪除模塊時(我想避免)手動更新數組大小,或者確實超大數組(這也不是我想要的)。 –

+0

@MatthijsKooijman正如我寫的,你無法避免'array [LARGE_ENOUGH_FOR_WORST_CASE]'。不存在過大的問題:陣列要麼足夠大,要麼太小。如果你希望這個常量是特定於應用程序的,你可以在.h文件中聲明它爲'extern const',從而要求應用程序實現它。 – Lundin

0

我也是面臨着類似的問題:

...需要多個回調函數,在各種 模塊中定義的,不在彼此相關半點。

Mine是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))預選賽基本上創建了一個空的身體,鏈接器將使用不同的變體,如果它替換默認的實現通過相同的名稱找到一個非弱變體。不幸的是,它並沒有完全解耦。但是你至少可以將這些超級全回調集合放在一個地方,而不是把它放在頭文件中。然後你的不同編譯單元基本上替換他們想要的超集的子集。如果有一種方法可以在所有模塊中使用相同的命名函數並只根據鏈接的內容進行調用,但還沒有找到能夠實現這一點的方法,我會很喜歡它。

3

正如以前的一些答案中所述,最好的選擇是使用自定義鏈接描述文件(使用KEEP(*(SORT(.whatever.*)))輸入節)。

反正可以無需修改連接器腳本(下面工作示例代碼),至少在某些平臺上用gcc完成

假設(Xtensa可嵌入式設備和Cygwin上測試)

  • 我們要避免使用RAM儘可能地(嵌入式)
  • 我們不想調用模塊瞭解與回調模塊任何東西(它是一個lib)
  • 沒有固定大小的列表(庫編譯時的未知大小)
  • 我正在使用GCC。其原理可在其它編譯器的工作,但我沒有測試過此示例中
  • 回調funtions接收沒有參數,但它是相當簡單的,如果需要

如何做到這一點修改:

  • 我們需要鏈接器在鏈接時指針數組以某種方式分配給功能
  • 由於我們不知道數組的大小,我們還需要鏈接器以某種方式紀念數組的結尾

這是非常具體的,因爲正確的方法是使用自定義鏈接腳本,但如果我們在標準鏈接腳本中找到始終「保留」和「排序」的節, 。

通常情況下,對於.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:

客戶端代碼註冊模塊的兩個回調0。生成的函數沒有名字(事實上他們確實有一個名字,但它奇蹟般地產生是編譯單元內唯一)

#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 

主.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) 
+0

這是非常優雅的,我希望我能夠對它進行明星處理。我喜歡這樣一個事實,即它只需要鏈接器腳本中的已排序部分(並且沒有開始/結束標記),如果您在多個「回調」中使用一個,則這一點相同。 – domen

+0

不錯,這實際上看起來像一個相當優雅的解決方案,我的問題。我只看到了這個(不知何故,stackoverflow並沒有給我發送電子郵件通知新的答案),而谷歌搜索一些其他相關的問題,並遇到我自己的問題:-) –