有一次接口設計
小李最近手頭在做的task,需要暴露新的接口出去給客戶。
========================我是正文分割線=============================
<<<<<<<需求>>>>>>>
----------------------------------------------------------------------------------------------------------
需要暴露一個汽車特征點的接口,輸入是一張圖像,輸出是汽車上的特征點,landmark。
----------------------------------------------------------------------------------------------------------
so easy?輸入基本不用管,輸出那就定義一個結構體不就完事了嗎?假設這一代的算法支持一輛車100個ladmark點。over了。
版本1
typed struct LM
{
uint x[100];
uint y[100];
}
方法很簡單,問題很明顯:
1.如果算法升級了,支持的特征點數變多了,怎麽破?直接把點數寫死是不是不太好?
2.將來怎麽擴展?如果需要支持更多的東西,比如車的顏色,型號等等呢?
好,所以小李就跑去跟同事討論,最後得到了一個高靈活度的接口
版本2
typedef struct LM
{
struct Header
{
uint LMCount;
uint x_offset; //4 bytes for each x
uint y_offset; //4 bytes for each y
}
UINT nValue;
}
定義了一個頭,一個數據段,在header裏面指定了landmark的個數,然後是x坐標相對於數據段起始地址的偏移量,這樣,用戶在調用接口的時候,拿到這個結構之後,一看,哦,有100個特征點,然後從nValue開始第0x00就是x的坐標,一個x占用4 bytes,一共去拿100個x,嗯,y也是一樣的,就結束了。
小李覺得不錯,你看,將來不管算法是支持多少個特征點,都不會存在兼容性的問題
老板一看,小李你這搞得啥玩意,根本不知道這個x和y是在內存中怎麽layout的,不就是個xy坐標嗎,整的這麽復雜,我都看不懂用戶怎麽看得懂,打回去,讓小李搞個簡單點的。
郁悶的小李又開始苦思冥想,不讓用offset的方式,又不能把數量寫死,那就這樣吧
版本3
typed struct LM
{
uint reserved[4];
struct LMdata
{
uint x;
uint y;
}data[1];
}
讓用戶通過一個新的接口先去拿一下landmark的數量,然後拿到數量後,分配內存,之後用戶拿到這個結構體之後,就可以去拿[x,y]了。這樣,數量沒有寫死,結構清晰,還加了一個reserved,便於後續擴展。
老板覺得小李的方法不錯,然後reserved[4]的可擴展性有限,建議改成指針,於是就有了版本4
版本4
typed struct LM
{
struct LMdata
{
uint x;
uint y;
}data[1];
void* future;
}
小李和老板都沒有看出這個結構體有啥問題,就決定找技術老王來看看,沒啥問題就done了。
老王很快指出了4個問題
1.當用戶在使用這個結構體的時候,定義LM plandmark,那麽plandmark.future是啥?應該是第二個LMdata,而不是真正的furture,這樣的定義一定是不可以接受的
2.這個結構體用戶在拿去用的時候,不知道是什麽樣的layout,不知道lmdata究竟又多少個,這個結構體本身不獨立
3.在用戶和算法的dll之間傳數據的時候,future是作為一個指針存在的,那麽用戶的這個指針是在用戶的進程裏面有效的,如果我們這個dll不跟用戶在同一個進程裏面,那這個指針傳遞是很不靠譜的
4.void在32位系統裏面是4個byte,在64位系統裏面是8個byte,如果恰好是app和dll之間的位數不一致,那麽對於這個地址的解析也會不一樣,肯定是有大問題的
小李傻了,原來把一個指針引入到接口的結構體裏面有這麽多的問題。啥也不說了,開始改吧。
1好說,直接把future指針放到結構體開頭就好了;2也好說,加一個LMcount就行,3的話目前是可以保證的,4的話可以用void64解決
版本5
typed struct LM
{
pvoid64 future;// provide by user, numm for now
uint LMcount;
struct LMdata
{
uint x;
uint y;
}data[1];
}
這個結構體出來之後,做linux的小林路過看了一眼,pvoid64這個在linux裏面並沒有定義,作為一個跨OS的接口,這樣顯然不合適。
小李(幾乎崩潰,思維不清):那則麽辦啊,用unsigned long long *可以嗎?
小林:當然不行了,unsigned long long*是什麽意思啊,他指的是指向的是一個long long類型的變量,但是指針本身的長度不變啊,32位裏面是32位,64位裏面是64位。
小李:不是把,讓我想想。。。。我們為什麽一定要用void指針啊?
小林:void指針又叫做無類型指針,可以通過強轉轉成其他類型的指針,這個pvoid和pvoid64其實是說指針的長度,像這種定義其他的類型都是做不到的。
小李:oh my gosh,為啥linux裏面沒有這麽好用的pvoid64?那現在怎麽辦?
小林:只好把指針定義為unsigned long long類型,用的時候再轉成指針了
版本6
typed struct LM
{
unsigned long long future_ptr;// provide by user, numm for now
uint LMcount;
struct LMdata
{
uint x;
uint y;
}data[1];
}
小李,老板,老王, 小林一起review了這個接口,終於通過了。
=================我是幹貨==========================
1.定義這種結構體需要考慮存儲的有效性,即structure盡量是4個bytes對齊,剩下的可以用reserved去填充
2.基本的要求是盡量直接傳數據,然後用戶能夠清晰的知道結構的的layout,不能太復雜;用戶有方式知道分配多大的buffer,然後拿到這個Buffer之後能夠簡單的解析,比如A.b需要就是指向b的,不能有歧義,所以可擴展大小的放在結構體最後,之後不要再加其他的了
3.可擴展性,如果有些參數隨著算法升級會有變化,就需要考慮可擴展性
4.API中不能出現pviod這種含混不清的字眼,極有可能在32位和64位相互調用的時候出錯
5.結構體的獨立性,用戶拿到這個結構體就可以開始解析,不需要借助其他的接口再去拿什麽值
6.跨平臺的接口,考慮linux,比如pvoid64這個在linux裏面就沒有,要使用常見的類型
7.使用指針要慎重!!!!要考慮是否能保證在一個進程裏面
====================我是花絮========================
review完後,小李心想,這個接口雖然定義了這麽久,但是其實定義的並不好,老王之前提到的不在同一個進程當中,就是一個很嚴重的潛在問題,所以在未來的接口定義中盡量不要使用指針。
小李上網查了查別人定義的接口,發現在java下定義接口好簡單啊,不用考慮內存分配的問題,C++果然需要考慮的很多。在c++下有什麽好的解決這種類似問題的方法嗎?學習一下微軟,發現可以采用定義version的方法,其實每次根據這個version就可以知道是算法的第幾個版本,然後用不同的struct去轉換,這個方法應該是比較好的。
typed struct LM
{
struct header
{
uint version;
uint size;
}
uint data;
}
typed struct LM_1
{
struct header
{
uint version;
uint size;//read only
}
UINT type;
UINT position;//add whatever future feature here
struct LMdata
{
uint x;
uint y;
}data[100];//could be 200 in v2, 300 in v3, as you wish :)
}
在code裏面這樣轉換就好了
if version == 1
(LM_1*) plm = (LM_1*)p_landmark
else if .....
===============我是彩蛋=======================
有一次接口設計