头文件向下兼容,如何优雅实现?

2021-12-01 10:30:40 +08:00
 FranzKafka95
因为项目中经常与第三方合作,两者间通过接口实现交互,所以有定义公共使用的头文件,用于定义接口和公共使用的东西。

但是由于项目一直在发展,经常需要变更头文件,怎样能优雅的解决头文件变动而不影响已经量产的环境呢?

如果只是单纯的增加接口,影响是有限的,主要是以前用的一些结构体,会存在增加成员的情况,这个改动会导致新头文件无法运用到已经量产的项目,比较头疼。

不知各位有没有好的解决方案?
2139 次点击
所在节点    C
19 条回复
wzzzx
2021-12-01 10:32:13 +08:00
这里可以借鉴一下 C++的 Pimpl 思想
ysc3839
2021-12-01 10:37:02 +08:00
把函数指针都放在一个 struct 里面,struct 头部存储当前版本的 sizeof ,新增函数都放到最后,结构体变了就新增一个函数
newmlp
2021-12-01 10:41:50 +08:00
Pimpl +1 类似于 Qt 的 d 指针,类的私有成员包装成另一个类,然后通过指针去访问,这样对外的类就只包含 public 的成员和一个私有的指针,更改结构体就不会影响对外类的内存布局了
FranzKafka95
2021-12-01 10:50:53 +08:00
我可能没太说清楚,举个例子吧。
如在头文件版本 1 中,有这样一个结构体(这个结构体我们自己与第三方都有使用):

struct A{
int a;
float b;

}

在头文件版本 2 中,需要在结构体 A 中增加成员 c ,增加取下:

struct A{
int a;
float b;
double c;

}

第三方程序最终会编译成一个动态库(so)。现在的构想是头文件变动(如结构体成员增加),但是第三方 so 不变,怎样实现头文件向下兼容呢
FranzKafka95
2021-12-01 10:59:49 +08:00
@newmlp pimpl 好像不太使用,这里的接口都是第三方实现的,由我们调用,而且是我们制定了接口,包括接口传参,现在就是传参是一个结构体,这个结构体包含很多第三方需要的信息,我要改变这个结构体(追加成员),而第三方程序不做改变,实现向下兼容
FranzKafka95
2021-12-01 11:02:22 +08:00
其实我很好奇,程序中索引结构体成员不是根据符号索引的(没有符号?)貌似是根据类型直接取内存块里的数据,暂且不论关闭结构体对齐与否都会存在问题(因为还有结构体嵌套),感觉无解了😭
xylxAdai
2021-12-01 11:15:37 +08:00
@FranzKafka95 。。。不太理解,第三方如果用不到这个新加的结构体变量的话,那么你直接内部用自己的结构体,用到它们接口再转过去就好了嘛。。
3dwelcome
2021-12-01 11:21:23 +08:00
传什么 struct 哦,太落后了。

学 JS 理念,交互接口就只传一个 json 对象,以后怎么变都没问题。
FranzKafka95
2021-12-01 11:22:24 +08:00
@xylxAdai 是这样的,这一份头文件会对接多个第三方,新增加的成员对新的第三方库有用,但是对已经量产的第三方库不会使用。我想保持一份头文件。
FranzKafka95
2021-12-01 11:32:02 +08:00
@3dwelcome 得分具体应用场景,我的应用与第三方库是存在大量数据交互的,传 Json 对象一点不实用
Sephirothictree
2021-12-01 11:40:07 +08:00
需要使用新结构体的地方,可以单独定义一下扩展参数,例如,旧头文件 struct A;
新第三方库,内部添加一个 struct B {
struct A;
double c;
};

这样对于新第三方库可以传参后强制转换 ,test(A*t){
struct B * b = (struct B *)t;
....
}
3dwelcome
2021-12-01 11:42:56 +08:00
json 实用啊,意味着你头文件只需要提供一个获取函数就可以,不用提供结构变量给第三方访问。

比如就一个函数 void get_keyvalue(char* inname, char* outarray, int* outarraycount);

对方获取内容时,就一句 get_keyvalue, 不同版本可以把 inname 的属性无限扩展,就和 json 一毛一样。
Sephirothictree
2021-12-01 11:43:15 +08:00
前提就是新第三方 so ,要按照 struct B 来编写代码
weidaizi
2021-12-01 11:43:49 +08:00
楼主是给用户头文件和 so ,来连接你们的服务的意思吗? 如果是的话,兼容主要做在服务里就可以了,比如最简单的方法是在传输协议的头中带上版本号,注册回调函数的时候,应该是消息+版本号确定唯一的回调函数,比如你上面自己举的例子:
------------------------------------
版本 1.0.0:
struct A{
int a;
float b;

};
那么传输的包中,应该类似于这样 | v1.0.0 | msg_type | payload |
------------------------------------
版本 1.0.1:
struct A{
int a;
float b;
double c;

}
那么传输的包中,应该类似于这样 | v1.0.1 | msg_type | payload |
------------------------------------

但是问题又来了,你现在已经发出去了 so ,里面没有版本号怎么办? 其实也简单,可以设置一些特殊的魔法字来识别,比如旧的版本,你的自定义头是这样
| msg_type(int32) | payload |
然后在新版本中,你的自定义头应该是这样
| 特定的 msg_type(int32) | version | msg_type(int32) | payload |
当你读到 特定的 msg_type 时,你知道这是带上来 version 版本的传输协议,接着转而解析 version + msg_type 即可
learningman
2021-12-01 11:48:36 +08:00
#IFDEF VERSION_XXX
....
#ENDIF
icylogic
2021-12-01 16:41:51 +08:00
> 第三方程序最终会编译成一个动态库(so)。现在的构想是头文件变动(如结构体成员增加),但是第三方 so 不变,怎样实现头文件向下兼容呢

source/binary compatibility 的问题看 kde 这篇就好

https://community.kde.org/Policies/Binary_Compatibility_Issues_With_C%2B%2B#The_Do.27s_and_Don.27ts

还有一个例子是 v4l2 的 api 在三年前添加了一个 data member

https://github.com/torvalds/linux/commit/f35f5d7
GeruzoniAnsasu
2021-12-11 16:55:44 +08:00
#ifndef __PRODUCT_VER__
typedef struct S_EssentialStruct_v0 S_EssentialStruct
#elif __PRODUCT_VER__ > 1
typedef struct S_Essential Struct_01 S_EssentialStruct
#endif

你们不会在代码里直接写 struct S_EssentialStruct_v0 吧
FranzKafka95
2021-12-13 07:55:21 +08:00
@GeruzoniAnsasu 我们还真是的,一开始就没有考虑兼容性,后面发现需要跟好几家第三方合作,而第三方的需求都不完全一样,所以才出现了这个问题。另外按照答主的思路,定义一个头文件版本的宏,根据宏去控制结构体成员貌似是个不错的想法。
GeruzoniAnsasu
2021-12-13 13:09:28 +08:00
@FranzKafka95 原项目里用的结构用的是 typedef 的 type 就很方便偷梁换柱,如果都写 `struct S` 那就…… 来一次全员搜索替换吧

另外动态库层面还有这个
https://sourceware.org/binutils/docs/ld/VERSION.html

用来控制 API 导出版本

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/819230

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX