作者: GorgonMeducer 傻孩子
首發(fā):裸機思維
【說在前面的話】
在本系列的前一篇文章《真刀真槍模塊化(2)——圖解Service模型》中,我們介紹了一種模塊化封裝的模型——Service模型。該模型的設計理念實際上服務于一個叫做“黑盒子哲學”的設計思維,其核心思想是:
- 將模塊視作一個黑盒子:模塊的設計者不用向外透露黑盒子的實現(xiàn)細節(jié);同時模塊的使用者也無法看到黑盒子的內部。
- 模塊的設計者和模塊的使用者完全通過“接口”來進行約定和溝通。這里所有的接口約定都是通過接口頭文件來進行描述和傳遞的。
- 接口(及接口頭文件)遵循“最小信息公開原則”,即,任何跟使用模塊所提供的服務無關的、或者非必要的(可有可無)信息都應該從接口頭文件中刪除。
實踐中,要想實現(xiàn)黑盒子,我們實際上要完成兩大任務:
- 如何隱藏模塊的實現(xiàn),或者說隱藏源代碼;
- 接口頭文件中數(shù)據(jù)結構的保護,或者說如何阻止用戶繞開模塊所提供的API而直接訪問關鍵結構體的內部(私有)成員;
對于第一條來說,我們只需要把模塊編譯成library,連同接口頭文件一起提供給客戶使用就可以做到;而對于第二條要想實現(xiàn)起來卻并非那么簡單——雖然我們常常說C語言可以通過結構體來模擬類的概念,但它卻無法像C++的類那樣提供對私有(private)和受保護(protected)成員的隱藏。換句話說,在實踐“最小信息公開原則”的時候,如果用戶調用服務的時候,確實需要用到結構體(這個結構體是最小信息),如何防止結構體的定義信息被“非法使用”,就成了一個切實的難題。
為了讓后續(xù)的討論更為清晰,我們不妨具體的定義一下我們的任務:
- 只允許用戶使用結構體的大小和對齊信息——這樣用戶可以自由的定義變量,或是通過malloc這樣的函數(shù)進行動態(tài)分配;
- 以某種“通過實際手段強制了的君子協(xié)定”的形式——僅在語法層面——阻止用戶直接訪問結構體的成員。
要想同時做到以上兩點,離不開今天索要介紹的主角:掩碼結構體(Masked Structure)。
【什么是掩碼結構體】
要想理解掩碼結構體,拋開復雜和抽象的文字描述,我們不妨來看一個具體的例子:假設我們做了一個字節(jié)隊列的模塊,其中最核心的結構體 byte/_queue/_t 的定義如下:
typedef struct byte_queue_t byte_queue_t;
針對這一結構體(或者叫類)我們提供一系列API(或者叫類的方法),比如:
typedef struct byte_queue_cfg_t {
為了保證模塊的正常工作,防止運行期間,用戶為了自身的便利,直接”外科手術式的“訪問 byte/_queue/_t 的成員導致不必要的問題(比如用戶說:我知道你遵循的是最小信息公開原則,也就是說,只要你放了結構體在接口頭文件里,我當然理解為我可以任意使用咯?),我們想將整個 byte/_queue/_t 都保護起來——這就好比,我們試圖引入一個“蒙版”,遮住結構體的成員信息然后在客戶的耳邊念起魔咒:
你什么都看不到,你看到了也沒法用……
你什么都看不到,你看到了也沒法用……
你什么都看不到,你看到了也沒法用……
...
要想實現(xiàn)這樣的“蒙版效果”其實并不困難,只需要知道要屏蔽的部分實際占用memory的大小,再根據(jù)這一大小來定義數(shù)組即可,因此,我們可以修改對應的定義為:
typedef struct byte_queue_t byte_queue_t;
這里,我們實際上是給原來的類型重命名為/_/_byte/_queue/_t,并建立了一個內部只使用數(shù)組來“濫竽充數(shù)”的替身——也就是我們所說的掩碼結構體。
如果你看過我之前的文章《漫談C變量——對齊(3)》,你會注意到,上述替身實際上丟失了結構體 /_/_byte/_queue/_t 的對齊信息——容易注意到 struct /_/_byte/_queue/_t 的結構體整體是對齊到 4 字節(jié)的,而掩碼結構體中數(shù)組chMask本身是對齊到字節(jié)的——這會導致當用戶使用掩碼結構體來定義變量時,由編譯器分配的空間可能無法滿足原結構體對對齊的要求,造成非對齊訪問——輕則性能下降,重則hardfault。
要解決這一問題也并不復雜,只需要借助GCC擴展的運算符 /_/_alignof/_/_() 提取目標類型的對齊信息,再使用/_/_attribute/_/_((aligned())) 來設置掩碼數(shù)組的對齊要求就可以了:
typedef struct byte_queue_t byte_queue_t;
至此,掩碼結構體 byte/_queue/_t 擁有了和原本的結構體 struct /_/_byte/_queue/_t 一樣的尺寸和對齊;同時還在“語法”層面阻止了用戶直接訪問結構體成員的可能(當然,這也只能防君子不防小人),我們原本設立的兩個目標都已成功達成。然而,聰明的你會在腦海里浮現(xiàn)出一個疑問——要想掩碼結構體能正常工作,上述信息都必須放置到接口頭文件中,難道用戶是傻子,看不到結構體 /_/_byte/_queue/_t 么?
借助宏的力量,我們可以成功的隱藏住 struct /_/_byte/_queue/_t 的存在。
下面的宏只是為了演示一種簡單的實現(xiàn)方法,暫時的打消你的疑慮,而實際在后面我們將要介紹的PLOOC模板中所使用的技法則更為復雜。由于本文只是著重于實際工程實踐中如何簡單的應用掩碼結構體,而不在于介紹復雜的宏技巧,因此我們將不在討論 PLOOC的實現(xiàn)細節(jié)。
#definedeclare_class(__name) /
借助上述宏,我們可以將接口頭文件 byte/_queue.h 中代碼簡化為:
...
而模塊源代碼中,則可以使用 class/_internal() 來獲取原本的結構體類型:
...
【如何使用PLOOC來簡化開發(fā)】
PLOOC是 Protected Low-overheadObject-Oriented programming with ANSI-C的英文縮寫,意為:為(類)提供保護的、低開銷的、面向對象C語言開發(fā)。它是我在 Github 上的一個開源項目(https://github.com/GorgonMedu...)。PLOOC 是目前已知唯一使用掩碼結構體對私有(private)和受保護(protected)的成員提供隱藏的OOPC模板;除此以外,通過幾近于0的額外資源消耗來實現(xiàn)面向對象封裝特性,也是PLOOC的一大賣點。
雖然PLOOC自帶的 MDK 例子工程演示了常見的面向對象特性,但處于時間問題,仍然沒有來得及提供一份簡單直接的手把手使用教程。這里我們仍然以 byte/_queue/_t 為例,為大家介紹一下如何在自己的工程中部署 PLOOC,并應用到 service模型中。
準備階段:
- 從Github上下載最新的 release 版本。
- 解壓縮后重命名目錄為 PLOOC,并復制到你的目標工程中
- 在你的工程中添加對PLOOC目錄的引用
- 在工程配置中打開對 C99 的支持,如果可能,直接開啟 C11和GNU擴展的支持:
- 如果你使用的是 gcc, clang 或是 arm compiler 6,你還需要打開對微軟擴展的支持(-fms-extensions)并屏蔽一些惱人且無害的 warning:
-fms-extensions -Wno-microsoft-anon-tag -Wno-empty-body
NOTE:如果你使用的是 arm compiler 6,在開啟微軟擴展以后,還需要額外定義一個宏 /_MSC/_VER 來避免底層庫中的一些不必要的編譯錯誤。
至此,我們就完成了 PLOOC 在你工程中的部署。
如何在模塊中部署:
仍以 byte/_queue 模塊為例,假設你已經根據(jù) service 模型構建好了目錄結構:
- 打開接口頭文件 byte/_queue.h 并在靠近結構體定義的地方其中添加以下內容:
/*! /NOTE: Make sure #include "plooc_class.h" is close to the class definition
這里,我們定義了兩個很重要的宏 /_/_BYTE/_QUEUE/_CLASS/_IMPLEMENT 和/_/_BYTE/_QUEUE/_CLASS/_INHERIT/_/_。容易看出,他們分別是根據(jù)
__<模塊名稱>_CLASS_IMPLEMENT
和
__<模塊名稱>_CLASS_INHERIT__
的形式改寫而成的。前者的作用是給 C 源代碼標記“我是這個類的實現(xiàn),我是類的主人”的身份用的;后者的作用是給 C代碼標記“我是派生類的實現(xiàn),我派生自基類”。具體使用方法,后面會具體介紹。
需要特別強調的是,一定不要忘記在接口頭文件的尾部將這兩個宏都undef掉:
...
- 在 byte/_queue.h 里定義目標類:
//! /name class byte_queue_t
值得注意的是,這里我們用 private/_member() 和 protected/_member()的形式規(guī)定了成員變量的屬性:其中private的成員是只有類的主人自己可見;而 protected的成員是類的主人以及派生類都可見。如果你想指定某些成員是公共可見的,則可以使用 public/_member()。
- 打開 byte/_queue.c,在文件的最開始通過定義宏 /_/_BYTE/_QUEUE/_CLASS/_IMPLEMENT 來標記自己“類主人”的身份,當然,別忘記包含自己的接口頭文件:
#define __BYTE_QUEUE_CLASS_IMPLEMENT
- 在 byte/_queue.c 中,如果某個函數(shù)(類的方法)試圖訪問類的成員,則應該首先借助 class/_internal() 來“脫下馬甲”。方法跟前文一樣,這里就不再贅述。
完整的例子在 PLOOC 的example目錄下:諸如派生類應該如何處理,函數(shù)重載應該如何實現(xiàn)等等問題,大家可以打開MDK的例子工程后“細品”。
【后記】
掩碼結構體是一種全新的方法,可以在語法層面上限制模塊的使用者對關鍵的結構體(類)成員的訪問。相比大家熟悉的“不完全類型”,掩碼結構體攜帶了足夠的信息(大小信息和對齊信息),從而允許模塊的使用者自由的定義變量或是動態(tài)分配,這與“不完全類型”必須依賴動態(tài)分配的缺點形成了鮮明的對比。
曾幾何時,掩碼結構體還有“模塊的.c不能包含模塊的接口頭文件” 這樣的限定,在最新的PLOOC中,這一問題已經得到了徹底的解決——再也不用擔心 ".c" 和 ".h" 中的類型描述不一致導致的運行時錯誤。
最后,需要強調一下,對 service 模型來說,掩碼結構體,或者說PLOOC的使用只是“錦上添花”——并非必須。讀者完全可以根據(jù)自己的喜好來決定模塊的實現(xiàn)方式。如果你喜歡或者對PLOOC使用有什么建議,歡迎在 github上提交你的issue。
審核編輯 黃昊宇
-
深度學習
+關注
關注
73文章
5512瀏覽量
121410
發(fā)布評論請先 登錄
相關推薦
評論