小李最近手头在做的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 .....
===============我是彩蛋=======================