V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
FranzKafka95
V2EX  ›  C

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

  •  
  •   FranzKafka95 · 2021-12-01 10:30:40 +08:00 via Android · 2117 次点击
    这是一个创建于 1096 天前的主题,其中的信息可能已经有所发展或是发生改变。
    因为项目中经常与第三方合作,两者间通过接口实现交互,所以有定义公共使用的头文件,用于定义接口和公共使用的东西。

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

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

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

    struct A{
    int a;
    float b;

    }

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

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

    }

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

    学 JS 理念,交互接口就只传一个 json 对象,以后怎么变都没问题。
    FranzKafka95
        9
    FranzKafka95  
    OP
       2021-12-01 11:22:24 +08:00 via Android
    @xylxAdai 是这样的,这一份头文件会对接多个第三方,新增加的成员对新的第三方库有用,但是对已经量产的第三方库不会使用。我想保持一份头文件。
    FranzKafka95
        10
    FranzKafka95  
    OP
       2021-12-01 11:32:02 +08:00 via Android
    @3dwelcome 得分具体应用场景,我的应用与第三方库是存在大量数据交互的,传 Json 对象一点不实用
    Sephirothictree
        11
    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
        12
    3dwelcome  
       2021-12-01 11:42:56 +08:00
    json 实用啊,意味着你头文件只需要提供一个获取函数就可以,不用提供结构变量给第三方访问。

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

    对方获取内容时,就一句 get_keyvalue, 不同版本可以把 inname 的属性无限扩展,就和 json 一毛一样。
    Sephirothictree
        13
    Sephirothictree  
       2021-12-01 11:43:15 +08:00
    前提就是新第三方 so ,要按照 struct B 来编写代码
    weidaizi
        14
    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
        15
    learningman  
       2021-12-01 11:48:36 +08:00
    #IFDEF VERSION_XXX
    ....
    #ENDIF
    icylogic
        16
    icylogic  
       2021-12-01 16:41:51 +08:00 via iPhone
    > 第三方程序最终会编译成一个动态库(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
        17
    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
        18
    FranzKafka95  
    OP
       2021-12-13 07:55:21 +08:00 via Android
    @GeruzoniAnsasu 我们还真是的,一开始就没有考虑兼容性,后面发现需要跟好几家第三方合作,而第三方的需求都不完全一样,所以才出现了这个问题。另外按照答主的思路,定义一个头文件版本的宏,根据宏去控制结构体成员貌似是个不错的想法。
    GeruzoniAnsasu
        19
    GeruzoniAnsasu  
       2021-12-13 13:09:28 +08:00
    @FranzKafka95 原项目里用的结构用的是 typedef 的 type 就很方便偷梁换柱,如果都写 `struct S` 那就…… 来一次全员搜索替换吧

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

    用来控制 API 导出版本
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2597 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 10:36 · PVG 18:36 · LAX 02:36 · JFK 05:36
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.