弱符號(hào)
弱符號(hào)是指在定義或者聲明一個(gè)對(duì)象(變量、結(jié)構(gòu)體成員、函數(shù))時(shí),在對(duì)象的前面添加 attribute ((weak)) 標(biāo)志所得到的對(duì)象符號(hào)。如下所示函數(shù)即為一個(gè)弱對(duì)象符號(hào) void test_weak_attr(void),或者稱該函數(shù)是弱函數(shù)屬性的、虛函數(shù)。
__attribute__((weak)) void test_weak_attr(void)
// 或者使用如下樣式的定義,兩者等效
void __attribute__((weak)) test_weak_attr(void)
{
printf("Weak Func!\\r\\n");
}
弱符號(hào)的作用與示例
弱符號(hào)是相對(duì)于強(qiáng)符號(hào)而言的,在定義或者聲明變量、函數(shù)時(shí),未添加 attribute ((weak)) 標(biāo)識(shí)的就默認(rèn)為強(qiáng)符號(hào)。如下,最普通的函數(shù)定義,就是定義了一個(gè)強(qiáng)符號(hào) void test_strong_ref(void):
void test_weak_attr(void)
{
printf("this is a strong func\\r\\n");
}
驅(qū)動(dòng)程序往往需要考慮兼容性,因?yàn)橐嫒魏芏鄰S商的不同型號(hào)的設(shè)備。若驅(qū)動(dòng)程序中使用強(qiáng)符號(hào)定義一些與適配的設(shè)備的特性相關(guān)的功能,則下次適配其他設(shè)備時(shí),該強(qiáng)符號(hào)函數(shù)可能需要被修改,以兼容新的設(shè)備。當(dāng)適配的設(shè)備很多時(shí),頻繁地更改驅(qū)動(dòng)代碼將破壞驅(qū)動(dòng)的可維護(hù)性。
弱符號(hào)的出現(xiàn)可以很好地解決該問(wèn)題。弱符號(hào)的對(duì)象具有可以被重定義的功能(即可以被重載)。下面通過(guò)測(cè)試說(shuō)明弱符號(hào)這種可被重載的特性。
在 test_weak_attr.c 程序中定義如下弱函數(shù):
// test_weak_attr.c
#include < stdio.h >
__attribute__((weak)) void test_weak_attr(void)
{
printf("this is a weak func\\r\\n");
}
在 main.c 中定義如下程序:
// main.c
void test_weak_attr(void)
{
printf("this is a strong func\\r\\n");
}
void app_main(void)
{
printf("init done\\r\\n");
test_weak_attr();
}
編譯運(yùn)行該 main.c 程序,得到的結(jié)果是什么樣子的呢?
將 main.c 中的 void test_weak_attr(void) 函數(shù)注釋掉,再重新編譯運(yùn)行程序得到的結(jié)果是:
小結(jié):在使用弱符號(hào)函數(shù)時(shí),我們可以重新定義一個(gè)同名的強(qiáng)符號(hào)函數(shù)來(lái)替代它;若沒(méi)有重新定義一個(gè)強(qiáng)函數(shù)來(lái)替換它,就使用弱函數(shù)的實(shí)現(xiàn)。弱函數(shù)就好像是一個(gè)可以被替換的“默認(rèn)函數(shù)”。
值得一提的是,舊版本的編譯器還可以使用如下方式的定義(僅聲明無(wú)效)將一個(gè)對(duì)象定義為一個(gè)弱對(duì)象:
在 linux 的一些代碼中,__weak 其實(shí)就是通過(guò) attribute ((weak))的重命名,兩者等效。
弱引用
弱引用是在聲明一個(gè)對(duì)象時(shí),通過(guò)__attribute__ ((weakref()) 定義一個(gè)符號(hào)的引用關(guān)系。如下所示即定義 test_weakref() 函數(shù)弱引用 test_weak_ref() 函數(shù)。
弱引用是相對(duì)于強(qiáng)引用而言的。未通過(guò) attribute ((weakref()) 的符號(hào)和實(shí)現(xiàn)代碼之間的關(guān)系是強(qiáng)引用。如下即為一個(gè)強(qiáng)引用函數(shù)。它直接給出了 函數(shù) test_strong_ref(void) 的實(shí)現(xiàn)。
在編譯程序的時(shí)候,我們可以直接使用 test_strong_ref(void) 而不必?fù)?dān)心編譯不通過(guò)。如果,我沒(méi)有時(shí)間去實(shí)現(xiàn) test_strong_ref(void) ,還想在程序里先使用該函數(shù)那該如何呢?(是的,就是想白嫖,不想實(shí)現(xiàn),還想先在程序里使用這個(gè)函數(shù))。
這個(gè)時(shí)候弱引用就派上用場(chǎng)了??梢韵葘⒃摵瘮?shù)定義為弱引用插入到代碼中,待后期有時(shí)間再慢慢優(yōu)化代碼實(shí)現(xiàn)這個(gè)函數(shù)完整的功能。下面結(jié)合測(cè)試進(jìn)行說(shuō)明。
測(cè)試代碼1:
static void test_weakref(void) __attribute__ ((weakref("test_weak_ref")));
void app_main(void)
{
printf("init done\\r\\n");
if (test_weakref) {
test_weakref();
} else {
printf("There is no weakref\\r\\n");
}
}
測(cè)試結(jié)果:
There is no weakref
測(cè)試代碼2:
void test_weak_ref(void)
{
printf("this is a weak ref\\n");
}
static void test_weakref(void) __attribute__ ((weakref("test_weak_ref")));
void app_main(void)
{
printf("init done\\r\\n");
if (test_weakref) {
test_weakref();
} else {
printf("There is no weakref\\r\\n");
}
}
測(cè)試結(jié)果:
this is a weak ref
小結(jié):強(qiáng)引用,在未定義該強(qiáng)引用的實(shí)現(xiàn)時(shí),編譯會(huì)報(bào)錯(cuò)誤:未定義的引用。弱引用允許定義一個(gè)未實(shí)現(xiàn)(未實(shí)例化)的對(duì)象,這在編譯的時(shí)候會(huì)將該對(duì)象處理成 NULL,編譯器并不會(huì)報(bào)錯(cuò)。通過(guò)使用弱引用可以實(shí)現(xiàn)后期優(yōu)化代碼的功能。而避免改動(dòng)使用該函數(shù)的地方。使用弱函數(shù)可以實(shí)現(xiàn)類似“鉤子(hook)"函數(shù)的功能。
實(shí)際上,包括C、python、go 編程語(yǔ)言在內(nèi)的很多語(yǔ)言 都有類似用法,本篇文章敘述的方法同樣適用于這些語(yǔ)言的相關(guān)開(kāi)發(fā)。
注意:弱引用僅在靜態(tài)編譯中有效,動(dòng)態(tài)鏈接中可能無(wú)效。
總結(jié)
弱符號(hào)、弱引用都是增強(qiáng)程序的可維護(hù)性的方法。弱符號(hào)通過(guò)可以被重定義的特性,實(shí)現(xiàn)可以被替換實(shí)現(xiàn)。弱引用通過(guò)可以暫時(shí)使用一個(gè)未定義的函數(shù)的功能,實(shí)現(xiàn)允許后期再實(shí)現(xiàn)該函數(shù)具體功能,而不必?fù)?dān)心編譯不通過(guò)。
為了方便理解,我們先預(yù)設(shè)一個(gè)應(yīng)用場(chǎng)景:
我們編寫(xiě)了一個(gè)模擬IIC的驅(qū)動(dòng),希望它能夠在不同的的平臺(tái)運(yùn)行,目標(biāo)的平臺(tái)就設(shè)為 stm32 標(biāo)準(zhǔn)庫(kù),stm32 HAL 庫(kù),stm32 LL 庫(kù),和 RT-Thread Driver 驅(qū)動(dòng)庫(kù)。
或許讀者有疑惑,為什么同樣是 stm32 ,卻分成三個(gè)平臺(tái)呢?這是因?yàn)閺目缙脚_(tái)軟件編寫(xiě)者的角度看,只要調(diào)用的庫(kù)的 API 不一致,就和換一個(gè)不同的平臺(tái)沒(méi)有什么本質(zhì)的差別,如果在代碼中寫(xiě)死了 API 的調(diào)用,即使是同一個(gè)平臺(tái),仍然像多平臺(tái)一樣不能運(yùn)行。
由此可以看出,跨平臺(tái)的困難所在,也不是由硬件平臺(tái)所導(dǎo)致的,而是由代碼所依賴的 API 的不同導(dǎo)致的。同一個(gè)平臺(tái),如果依賴的 API 不同,代碼就不能跨平臺(tái),同樣地,不同的平臺(tái),如果依賴的 API 相同,也可以跨平臺(tái)。
所以歸根結(jié)底,是代碼所依賴的 API 出現(xiàn)了不同,所以下文中所說(shuō)的“平臺(tái)”,實(shí)際上對(duì)應(yīng)的是一套 API 。
我們繼續(xù)說(shuō)這個(gè)模擬 IIC 的驅(qū)動(dòng),模擬 IIC 驅(qū)動(dòng)是使用 GPIO 的反轉(zhuǎn)來(lái)模擬 IIC 協(xié)議,所以依賴了平臺(tái)的 GPIO API,如果把調(diào)用 GPIO 的部分寫(xiě)死,那么換一個(gè)平臺(tái),就肯定不能在多個(gè)平臺(tái)上運(yùn)行。
下面我們開(kāi)始討論在多平臺(tái)運(yùn)行的解決方案。
我們先從最樸素簡(jiǎn)單的解決方案開(kāi)始,然后逐步迭代到弱函數(shù)的方案,這樣有兩方面好處:
一是從簡(jiǎn)單的方案開(kāi)始,循序漸進(jìn)地介紹,可以降低閱讀門檻。
二是可以帶讀者親歷一遍方案的演進(jìn)過(guò)程,以及演進(jìn)動(dòng)因。
和給直接給結(jié)果相比,注重過(guò)程和動(dòng)因更能夠還原技術(shù)決策的真實(shí)過(guò)程。因?yàn)槿魏渭夹g(shù)都是從簡(jiǎn)單樸素逐步演進(jìn)而來(lái)的,如果直接給出最后的結(jié)果,會(huì)產(chǎn)生理解的斷層,即使記住了幾種技術(shù)的優(yōu)劣,在新的場(chǎng)景中,面對(duì)更加多樣化的實(shí)際問(wèn)題也會(huì)難免乏力。
先從最樸素的方案講起。
方案一、手動(dòng)控制添加編譯的 .c 文件
樸素的方案有很多,比如就是多搞幾個(gè)版本的 .c 文件,比如SIMU_IIC_STM32_HAL.c ,SIMU_IIC_STM32_LL.c, SIMU_IIC_RTT.c 需要哪個(gè)就添加哪個(gè)進(jìn)去編譯不就完了嘛!
這種樸素的方案雖然看起來(lái)簡(jiǎn)單,但是,這幾個(gè)文件中包含有共用的邏輯,例如模擬 IIC 的協(xié)議的實(shí)現(xiàn),如何將 8bit 的數(shù)據(jù)依次發(fā)送,等等。
這些共用的邏輯,相當(dāng)于在每個(gè)文件中都復(fù)制了一份,一旦修改到共用的邏輯,就要手動(dòng)同步每個(gè)文件,這會(huì)導(dǎo)致代碼冗余和維護(hù)難度的急劇增加,很容易出現(xiàn)人為失誤。如果需要添加更多的平臺(tái)支持,就需要再次復(fù)制修改代碼。
另一個(gè)問(wèn)題是,使用不同的編譯工具鏈,添加編譯文件的方式并不一樣,例如,Visual Studio 和 MDK keil 通常是手動(dòng)添加,而 CMake 通常直接添加目錄或者通過(guò)文件后綴進(jìn)行搜索。用戶在使用不同的編譯工具時(shí),需要針對(duì)編譯工具來(lái)分別處理,這也增加了維護(hù)的成本。
因此,我們需要尋找更加優(yōu)雅的解決方案。
方案一中最核心的問(wèn)題,是沒(méi)有分離共用的邏輯,和各個(gè)平臺(tái)的適配接口。
在通常的命名慣例中,共用的邏輯稱為 Common,而各個(gè)平臺(tái)的適配接口稱為 Port。
在接下來(lái)的方案中,我們就會(huì)引入 Common 和 Port 分離的設(shè)計(jì)思想,Common 和 Port 的分離,使得對(duì)共用邏輯的修改和增強(qiáng)直接 “分發(fā)” 到了各個(gè)的 Port 中,而增添新的平臺(tái),不需要對(duì) Common 做任何修改。
方案二、條件編譯
一種更加優(yōu)雅的解決方案是使用條件編譯。條件編譯是一種編譯時(shí)根據(jù)條件選擇編譯代碼的技術(shù),可以通過(guò)編譯器提供的宏定義和預(yù)處理指令來(lái)實(shí)現(xiàn)。
在我們的模擬 IIC 驅(qū)動(dòng)中,可以直接編寫(xiě) Common 部分,然后 Common 部分通過(guò)條件編譯,可以根據(jù)平臺(tái)的不同選擇不同的 GPIO Port API。
例如,在 STM32 標(biāo)準(zhǔn)庫(kù)中,可以使用 GPIO_SetPinMode 和 GPIO_WritePin 接口來(lái)模擬 IIC 協(xié)議,而在 STM32 HAL 庫(kù)中,可以使用 HAL_GPIO_WritePin 和 HAL_GPIO_ReadPin 接口來(lái)模擬 IIC 協(xié)議。因此,在代碼中可以使用如下的條件編譯方式:
#if defined (USE_STM32_STD_LIB)
/* STM32 Standard Peripheral Library */
GPIO_SetPinMode(SDA_PORT, SDA_PIN, GPIO_MODE_OUTPUT_PP);
GPIO_SetPinMode(SCL_PORT, SCL_PIN, GPIO_MODE_OUTPUT_PP);
...
#elif defined (USE_STM32_HAL_LIB)
/* STM32 HAL Library */
HAL_GPIO_WritePin(SDA_PORT, SDA_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_SET);
...
#elif defined (USE_STM32_LL_LIB)
/* STM32 LL Library */
LL_GPIO_SetOutputPin(SDA_PORT, SDA_PIN);
LL_GPIO_SetOutputPin(SCL_PORT, SCL_PIN);
...
#elif defined (USE_RTT_DRIVER)
/* RT-Thread Driver */
rt_pin_mode(SDA_PIN, PIN_MODE_OUTPUT);
rt_pin_mode(SCL_PIN, PIN_MODE_OUTPUT);
...
#endif
這樣,在編譯代碼時(shí),我們可以通過(guò)宏定義來(lái)選擇編譯使用哪個(gè)平臺(tái)的代碼,
從而實(shí)現(xiàn)跨平臺(tái)運(yùn)行。
然而,這種條件編譯方式還是有一些問(wèn)題,如果我們需要添加新的平臺(tái)支持,就需要添加新的宏定義和條件編譯,而且需要修改模塊的源碼。
方案三 函數(shù)指針
我們可以進(jìn)一步分離 Common 和 Port,將其放在不同的 .c 文件中,Common 通過(guò)函數(shù)調(diào)用的方式來(lái)訪問(wèn) Port 提供的接口,這樣可以更加靈活和方便地添加新的平臺(tái)支持。
具體實(shí)現(xiàn)可以通過(guò)在 Common 中定義一些函數(shù)指針類型來(lái)實(shí)現(xiàn)。
例如,我們可以定義一個(gè)名為 IICOps 的結(jié)構(gòu)體,其中包含了一些指向函數(shù)的指針,這些函數(shù)實(shí)現(xiàn)了具體的 IIC 操作。
在 Port 中,我們實(shí)現(xiàn)這些函數(shù),并將其指針傳遞給 Common 中的 IICOps 結(jié)構(gòu)體。
這樣,在 Common 中就可以通過(guò)調(diào)用這些函數(shù)指針來(lái)訪問(wèn) Port 提供的接口了。
這種方案的好處是,添加新的平臺(tái)支持時(shí),只需要實(shí)現(xiàn)相應(yīng)的 Port 函數(shù),并將其指針傳遞給 Common 中的結(jié)構(gòu)體即可,不需要修改 Common 的源碼。
同時(shí),由于 Common 和 Port 分離,不同平臺(tái)的適配代碼可以相互獨(dú)立,修改一方不會(huì)影響到另一方,從而減少了代碼冗余和維護(hù)難度。
但是,使用函數(shù)指針有兩個(gè)主要的缺點(diǎn),一是函數(shù)指針本身需要占據(jù)內(nèi)存,增加了內(nèi)存的開(kāi)銷,二是函數(shù)指針需要在初始化時(shí)進(jìn)行加載。
具體來(lái)說(shuō),函數(shù)指針的內(nèi)存開(kāi)銷相對(duì)于代碼本身并不大,通常可以忽略不計(jì),但在嵌入式系統(tǒng)中,資源有限,內(nèi)存開(kāi)銷需要更加注意。
而函數(shù)指針的初始化需要在程序啟動(dòng)時(shí)進(jìn)行,這也會(huì)對(duì)程序的啟動(dòng)時(shí)間產(chǎn)生一定的影響。此外,函數(shù)指針的使用可能會(huì)導(dǎo)致一定的運(yùn)行時(shí)開(kāi)銷,需要在程序性能和資源利用方面做出權(quán)衡。
另外,函數(shù)指針的使用需要程序員有一定的技術(shù)水平和經(jīng)驗(yàn),需要合理地進(jìn)行函數(shù)指針的定義、傳遞和調(diào)用等操作,避免出現(xiàn)潛在的錯(cuò)誤和安全問(wèn)題。
總的來(lái)說(shuō),函數(shù)指針是一種比較靈活和方便的方式,可以幫助我們實(shí)現(xiàn)代碼的跨平臺(tái)支持,但需要在實(shí)際應(yīng)用中仔細(xì)考慮其使用場(chǎng)景和影響,做出合理的決策。
方案四、Common 中聲明,Prot 中實(shí)現(xiàn)
在這種方案中,我們?nèi)匀粚?Common 和 Port 分離,但是我們不再使用函數(shù)指針來(lái)訪問(wèn) Port 中的接口,而是將其定義為 extern 聲明,由 Port 來(lái)實(shí)現(xiàn)具體的函數(shù)。
具體實(shí)現(xiàn)可以通過(guò)在 Common 中定義一些 extern 聲明的函數(shù),這些函數(shù)實(shí)現(xiàn)了具體的 IIC 操作,但是并不在 Common 中實(shí)現(xiàn)具體的代碼邏輯,而是在 Port 中實(shí)現(xiàn)。
在 Port 中,我們實(shí)現(xiàn)這些函數(shù),并將其聲明為 extern,然后在編譯時(shí)鏈接到 Common 中。
這樣,在 Common 中就可以通過(guò)調(diào)用這些函數(shù)來(lái)訪問(wèn) Port 提供的接口了。
這種方案的好處是,添加新的平臺(tái)支持時(shí),只需要實(shí)現(xiàn)相應(yīng)的 Port 函數(shù),并在編譯時(shí)鏈接到 Common 中即可,不需要修改 Common 的源碼。
這樣 Port 的實(shí)現(xiàn)函數(shù)的掛載就提前到了編譯階段,避免了運(yùn)行時(shí)掛載函數(shù)指針的復(fù)雜性和容易出錯(cuò)問(wèn)題。以及避免了函數(shù)指針的內(nèi)存占用。
但是,和 IIC 的例子不同的是,在一些更實(shí)際的項(xiàng)目中,隨著軟件復(fù)雜度的提升, Common 中使用的 Port 函數(shù)數(shù)量會(huì)快速膨脹,這時(shí),每個(gè) Port 函數(shù)都必須要實(shí)現(xiàn),即使這個(gè)功能非常的冷門,這樣 Common 中每增加一個(gè) Port 的依賴,都要求所有的 Port 進(jìn)行及時(shí)的跟進(jìn),否則整個(gè)項(xiàng)目都無(wú)法編譯通過(guò)。
接下來(lái),就要有請(qǐng) weak (弱函數(shù))機(jī)制出馬了
但方案四的缺點(diǎn)在于,在一些更實(shí)際的項(xiàng)目中,隨著軟件復(fù)雜度的提升,Common 中使用的 Port 函數(shù)數(shù)量會(huì)快速膨脹,這時(shí),每個(gè) Port 函數(shù)都必須要實(shí)現(xiàn),即使這個(gè)功能非常的冷門,這樣 Common 中每增加一個(gè) Port 的依賴,都要求所有的 Port 進(jìn)行及時(shí)的跟進(jìn),否則整個(gè)項(xiàng)目都無(wú)法編譯通過(guò)。
方案五 弱函數(shù)
為了解決這個(gè)問(wèn)題,可以使用 weak (弱函數(shù))機(jī)制,將所有的 Port 函數(shù)都定義為 weak 函數(shù)。
weak 函數(shù)是一種特殊的函數(shù)類型,帶 weak 的函數(shù)和不帶 weak 的函數(shù)可以同時(shí)存在,如果有不帶 weak 的函數(shù),就會(huì)優(yōu)先鏈接不帶 weak 的實(shí)現(xiàn),如果沒(méi)有找到不帶 weak 的實(shí)現(xiàn)函數(shù),就會(huì)使用 weak 函數(shù)作為默認(rèn)的實(shí)現(xiàn)。
即,在使用弱函數(shù)時(shí),如果找到多個(gè)實(shí)現(xiàn),鏈接器會(huì)選擇優(yōu)先級(jí)最高的實(shí)現(xiàn)。
在 C 語(yǔ)言中,可以使用 attribute((weak)) 來(lái)聲明一個(gè)函數(shù)為弱函數(shù)。例如:
attribute((weak)) void port_func()
{
// 默認(rèn)實(shí)現(xiàn)
}
attribute((weak)) void port_func()
而在 Port 中,只需要實(shí)現(xiàn)需要的函數(shù)即可,如果某些函數(shù)不需要實(shí)現(xiàn),可以不用管它,因?yàn)樵阪溄訒r(shí)會(huì)使用 Common 中的默認(rèn)實(shí)現(xiàn)。
這樣在編譯時(shí)如果沒(méi)有找到相應(yīng)的實(shí)現(xiàn)函數(shù),就會(huì)使用默認(rèn)的實(shí)現(xiàn),而不會(huì)導(dǎo)致編譯錯(cuò)誤。
而在 Port 中,只需要實(shí)現(xiàn)需要的函數(shù)即可,如果某些函數(shù)不需要實(shí)現(xiàn),可以不用管它,因?yàn)樵诰幾g時(shí)會(huì)使用 Common 中的默認(rèn)實(shí)現(xiàn)。
這樣,在添加新的平臺(tái)支持時(shí),只需要實(shí)現(xiàn)需要的函數(shù),而不用實(shí)現(xiàn)所有的函數(shù),大大簡(jiǎn)化了開(kāi)發(fā)的難度和工作量。同時(shí),也避免了函數(shù)指針的內(nèi)存占用和運(yùn)行時(shí)掛載函數(shù)指針的復(fù)雜性和容易出錯(cuò)問(wèn)題。
弱函數(shù)的多編譯器支持
在不同的平臺(tái)上, weak 的聲明方法也會(huì)有所不同,因此需要自己定義一個(gè) weak 聲明,例如 MY_WEAK,來(lái)支持不同的平臺(tái):
/* Compiler */
#if defined(__ARMCC_VERSION) && (__ARMCC_VERSION >= 5000000) /* ARM Compiler \\
*/
#define MY_WEAK __attribute__((weak))
#elif defined(__IAR_SYSTEMS_ICC__) /* for IAR Compiler */
#define MY_WEAK __weak
#elif defined(__MINGW32__) /* MINGW32 Compiler */
#define MY_WEAK
#elif defined(__GNUC__) /* GNU GCC Compiler */
#define MY_WEAK __attribute__((weak))
#endif
/* default MY_WEAK */
#ifndef MY_WEAK
#define MY_WEAK
#endif
可以看到,在不同的編譯器下,weak 有不同的寫(xiě)法,上面的這些定義包含了對(duì) armcc5、 armclang、IAR、GCC 的支持。
然而,弱函數(shù)的方案在一些平臺(tái)有一些明顯的缺陷,例如,MSVC編譯器是微軟公司開(kāi)發(fā)的C/C++編譯器,在Windows操作系統(tǒng)下被廣泛使用。與GCC和Clang等主流編譯器相比,MSVC對(duì)于弱函數(shù)的支持不太完善。
MSVC 中,可以通過(guò)使用#pragma weak來(lái)聲明弱函數(shù),但是這個(gè)特性只能在 x86 和 x64 平臺(tái)下使用,而在 ARM 平臺(tái)下是不支持的。
此外,在一些版本的MSVC編譯器中,#pragma weak 的功能也存在一些限制和bug,所以一般在 MSVC 中直接取消 weak 還會(huì)更實(shí)際一些。
用弱函數(shù)對(duì) Port 進(jìn)行分類
weak 的引入使得我們的 Port 可以根據(jù)實(shí)際的需求進(jìn)行劃分,而不是一股腦地必須實(shí)現(xiàn)所有的 Port 函數(shù)。
引入了 weak 之后,我們就有條件將 Port 劃分為以下的幾種:
1.核心且無(wú)默認(rèn)實(shí)現(xiàn)的 Port
這屬于必須實(shí)現(xiàn)的 Port,缺乏這個(gè) Port,模塊的核心功能就運(yùn)行不起來(lái)。例如模擬 IIC中對(duì) IO 的操作 Port 函數(shù),這種 Port 用不用 weak 的區(qū)別不大,屬于最硬的骨頭,在設(shè)計(jì)軟件時(shí)應(yīng)當(dāng)注意盡可能地減少這種 Port。
在設(shè)計(jì)軟件時(shí),可以直接取消這類 Port 的弱定義,讓編譯器在編譯時(shí)就拋出錯(cuò)誤。既然缺少了這類 Port 系統(tǒng)的核心功能就無(wú)法工作,那么編譯通過(guò)了也沒(méi)有什么意義。
- 核心且有默認(rèn)實(shí)現(xiàn)的 Port
這類 Port 可以直接定義一個(gè)默認(rèn)實(shí)現(xiàn),在大多數(shù)情況下,用戶就可以不用管這個(gè) Port 了,而在有定制需求的場(chǎng)合下,又可以靈活地定制。
例如弱定義一個(gè) port_printf 用來(lái)支持跨平臺(tái)軟件的打印輸出,默認(rèn)是直接使用平臺(tái)的 vprintf,對(duì)于大多數(shù)的用戶來(lái)說(shuō),只需要打印到平臺(tái)自帶的 printf 即可,因此對(duì)于大多數(shù)用戶來(lái)說(shuō),這個(gè) Port 不用實(shí)現(xiàn),就能正常使用系統(tǒng)了。
attribute((weak)) void port_printf(char* fmt, ...) {
va_list args;
va_start(args, fmt);
vprintf(fmt, args);
va_end(args);
}
而有定制需求的用戶可以通過(guò)自己重寫(xiě) port_printf() 來(lái)打印到其他的地方(比如輸出到 log,或者輸出到其他串口)。
在 printf 的例子中,我們默認(rèn)了 printf 是所有的平臺(tái)都提供了的,對(duì) printf 進(jìn)行這種假設(shè)是合理的,因?yàn)樗?libc 的標(biāo)準(zhǔn)函數(shù)。
然而,有些 Port 雖然有默認(rèn)實(shí)現(xiàn),卻不能支持所有的平臺(tái),例如線程操作的 Port,在常見(jiàn)的平臺(tái)中,如 linux、RT-Thread、FreeRTOS ,我們知道如何寫(xiě)默認(rèn)實(shí)現(xiàn),但是這些實(shí)現(xiàn)又不通用,這時(shí)我們可以在默認(rèn)實(shí)現(xiàn)中結(jié)合條件編譯,為常見(jiàn)的平臺(tái)提供默認(rèn)實(shí)現(xiàn),例如:
attribute((weak)) void port_thread_start(port_thread_t* thread) {
#ifdef __linux
pthread_mutex_lock(&(thread- >mutex));
pthread_cond_signal(&(thread- >cond));
pthread_mutex_unlock(&(thread- >mutex));
#elif USE_FREERTOS
vTaskResume(thread- >thread);
#else
#error "port_thread_start() 需要用戶實(shí)現(xiàn)"
#endif
}
這個(gè)例子中為 linux 和 FreeRTOS 提供了默認(rèn)的線程啟動(dòng) Port 的實(shí)現(xiàn),使用 linux 或者 FreeRTOS 的用戶可以通過(guò)條件編譯來(lái)直接使用默認(rèn)實(shí)現(xiàn)。
而既不用 linux 也不用 FreeRTOS 的用戶,則會(huì)在編譯時(shí)遇到 #error,這提示他們要自己實(shí)現(xiàn) port_thread_start()。
這種寫(xiě)法還有一種好處,就是在不支持 weak 的平臺(tái),例如 MSVC,就可以通過(guò) _WIN32 條件編譯來(lái)進(jìn)行跨平臺(tái)支持。
- 邊緣但無(wú)默認(rèn)實(shí)現(xiàn)的 Port
還有一類 Port,它們比較冷門,只有部分用戶會(huì)使用到,但是又難以提供默認(rèn)的實(shí)現(xiàn)。例如用 port_reboot() 來(lái)重啟硬件,每個(gè)硬件平臺(tái)重啟硬件的 API 都是不同的,我們無(wú)法提供一個(gè)默認(rèn)的實(shí)現(xiàn)。
但是,沒(méi)有這個(gè) Port,也不影響系統(tǒng)的核心功能,只是在某些時(shí)候(例如開(kāi)啟了超時(shí)自動(dòng)重啟功能),又有這個(gè) Port才行,這樣的 Port 就屬于是邊緣但無(wú)默認(rèn)實(shí)現(xiàn)的 Port。
這時(shí),就可以選擇將 Port 缺失的錯(cuò)誤延后到運(yùn)行時(shí),具體在操作時(shí),就可以編寫(xiě)一個(gè) weak 的實(shí)現(xiàn),而這個(gè)實(shí)現(xiàn)中拋出一個(gè)運(yùn)行時(shí)錯(cuò)誤,例如:
attribute((weak)) void port_reboot(void){
printf("Error: port_reboot() 需要用戶實(shí)現(xiàn)\\r\\n");
while(1);
}
這樣,只要不用到這個(gè) Port,都可以編譯通過(guò)且順利運(yùn)行,只有實(shí)際用到時(shí),才會(huì)在運(yùn)行時(shí)報(bào)錯(cuò)。
weak 在 GCC 鏈接靜態(tài)庫(kù)時(shí)的問(wèn)題
這里我想特別提示一種常見(jiàn)的 weak 失效的問(wèn)題,這種問(wèn)題目前我只發(fā)現(xiàn)在 gcc 鏈接靜態(tài)庫(kù)時(shí)包含 weak 會(huì)出現(xiàn)。
gcc 在鏈接靜態(tài)庫(kù)時(shí),默認(rèn)的行為是只要找到第一個(gè)(不管是不是弱符號(hào)),就會(huì)將其鏈接,然后停止繼續(xù)尋找,這樣一來(lái),如果你的 weak 是被第一個(gè)找到的,那么強(qiáng)定義的函數(shù)就失效了。
這個(gè)問(wèn)題有多種解決方案,我這里只提示一種,有更好的方案可以進(jìn)qq交流群:577623681 大家一起討論。
解決方案:使用 "-Wl,--whole-archive" 選項(xiàng)來(lái)解決。當(dāng)使用這個(gè)選項(xiàng)時(shí),鏈接器將整個(gè)庫(kù)文件都包含在鏈接輸出文件中,而不考慮這些庫(kù)文件是否實(shí)際上被使用了。這樣就可以保證弱符號(hào)在整個(gè)庫(kù)中得到了正確的鏈接,并且在可執(zhí)行文件或其他庫(kù)中保持有效。
需要注意的是,當(dāng)使用 "-Wl,--whole-archive" 選項(xiàng)時(shí),可能會(huì)將一些不必要的庫(kù)文件鏈接到最終的可執(zhí)行文件或庫(kù)中,這可能會(huì)增加最終文件的大小。因此,應(yīng)該僅在必要時(shí)使用這個(gè)選項(xiàng)。
-
函數(shù)
+關(guān)注
關(guān)注
3文章
4378瀏覽量
64611 -
驅(qū)動(dòng)代碼
+關(guān)注
關(guān)注
2文章
15瀏覽量
7733 -
符號(hào)
+關(guān)注
關(guān)注
0文章
55瀏覽量
4553
發(fā)布評(píng)論請(qǐng)先 登錄
嵌入式C語(yǔ)言的弱符號(hào)和弱引用
C語(yǔ)言的強(qiáng)、弱別名是什么作用?
什么叫做原理圖符號(hào),它的作用是什么?
電子元器件符號(hào)-電氣符號(hào)大全-電路圖符號(hào)大全

壓敏電阻符號(hào)怎么表示 壓敏電阻選型參數(shù)及作用

磁簧開(kāi)關(guān)符號(hào)_磁簧開(kāi)關(guān)的作用

符號(hào)在人工智能中的作用
C語(yǔ)言強(qiáng)/弱符號(hào)和強(qiáng)/弱引用的作用
關(guān)于有符號(hào)數(shù)據(jù)類型的示例
弱相互作用對(duì)有機(jī)光電性質(zhì)調(diào)控的理論研究

評(píng)論