[讨论] 如何将一个结构体的 buffer 数据,拷贝到另一个结构体的实例中 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
Gorvery
V2EX    程序员

[讨论] 如何将一个结构体的 buffer 数据,拷贝到另一个结构体的实例中

  •  
  •   Gorvery 2024-08-20 04:15:23 +08:00 2837 次点击
    这是一个创建于 418 天前的主题,其中的信息可能已经有所发展或是发生改变。

    背景

    我有一个静态库 A ,提供的头文件中有 public 的结构体和方法接口,以供别人调用

    namespace api { struct MyStruct { std::string text; int text_length; }; std::string getStructInfo(const MyStruct &struct); } 

    另一个静态库 B ,依赖了静态库 A ,其源码内实例化了一个 MyStruct 结构体对象,并调用了 getStructSize 方法获取了结果

    静态库 A 和静态库 B 均以 二进制.a 形式集成到工程 App 中使用

    问题

    某一天,静态库 A 中的头文件 MyStruct 定义发生变化:

    struct MyStruct { int text_length; float score; std::string full_text; }; 

    我想在不重新构建静态库 B ,保持 MyStruct 命名不变,使用新定义的情况下,通过指针偏移或二进制适配手段,使静态库 B 内的代码逻辑能正常运行,应该如何进行处理?

    我的想法是,由于 getStructSize 的参数是引用,所以打算通过上测算出老的结构体定义的大小,然后 memcpy 对应长度的 buffer 到一个和原结构体定义完全一致,但命名空间不一致的的结构体中,随后进行提取:

    第一步:定义一个结构体,和原来的 MyStruct 完全一致,但通过命名空间隔离:

    namespace old { struct MyStruct { std::string text; int text_length; }; } 

    第二步:在 getStructInfo 内部,进行转换

    int getStructInfo(const MyStruct &struct) { size_t old_size = sizeof(old::MyStruct); old::MyStruct old_struct; memcpy(&old_struct, &struct, old_size); printf("old struct text:%s", old_struct.text.c_str()); …… } 

    总觉得这个方法是可以的,但实际操作的过程中就不行,无法获得 text 的值,这是为什么呢?求大神赐教下

    补充信息:iOS 系统,arm64 架构

    第 1 条附言    2024-08-20 10:57:17 +08:00
    更新下为什么选择用这么不可靠的方式来解决问题的原因,是因为屎山代码,还有有另外一个静态库 C 依赖了新的结构体定义,如果结构体定义修改回滚或者再封装一层,需要修改的地方更多。静态库 C 和静态库 B 同样都是不可重新构建了。

    假如 memcpy 的方式不可实现,可以通过指针偏移读取其中的一两个关键值吗?
    或者通过二进制修改的方式,修改掉静态库 B 中的符号,用一个新的 namespace 进行隔离,然后在静态库 A 中补充上这个新的 namespace 隔离后的定义,这个可以么?
    37 条回复    2024-08-20 17:48:11 +08:00
    nagisaushio
        1
    nagisaushio  
       2024-08-20 04:39:55 +08:00 via Android
    你成员顺序变了啊
    ysc3839
        2
    ysc3839  
       2024-08-20 05:01:17 +08:00
    memcpy 是原样拷贝其中的值,MyStruct 和 old::MyStruct 的内存结构都不一样,原样拷贝还是不一样的,并不能进行转换,当然是不行的。
    这个例子最大的问题还不是转换,而是没办法得知外部传进来的是新的还是旧的。

    这种情况的正确做法是,结构体开头用一个字段保存结构体大小,然后当结构体发生改变时,要确保与之前版本的大小都不一样,这样就可以通过大小来区分不同版本了。
    ```
    #include <cstdio>

    struct MyStruct_V1 {
    size_t size;
    int a;
    float b;
    };

    struct MyStruct_V2 {
    size_t size;
    float b;
    int a;
    double c;
    };

    void PrintMyStruct(const void* p) {
    auto size = *reinterpret_cast<const size_t*>(p);
    if (size == sizeof(MyStruct_V1)) {
    auto structV1 = reinterpret_cast<const MyStruct_V1*>(p);
    printf("MyStruct_V1: a=%d b=%f\n", structV1->a, structV1->b);
    } else if (size == sizeof(MyStruct_V2)) {
    auto structV2 = reinterpret_cast<const MyStruct_V2*>(p);
    printf("MyStruct_V2: b=%f a=%d c=%f\n", structV2->b, structV2->a, structV2->c);
    }
    }

    int main() {
    MyStruct_V1 structV1 = {
    sizeof(structV1),
    233,
    466,
    };
    PrintMyStruct(reinterpret_cast<void*>(&structV1));
    MyStruct_V2 structV2 = {
    sizeof(structV2),
    233,
    466,
    699,
    };
    PrintMyStruct(reinterpret_cast<void*>(&structV2));
    }
    ```
    levelworm
        3
    levelworm  
       2024-08-20 07:13:59 +08:00 via Android
    memcpy 是按照内存来拷贝,他不管你结构是什么样子的。
    8620
        4
    8620  
       2024-08-20 07:53:07 +08:00 via Android
    虽然理论上通过偏移能获得更改后的结构体中原来元素的位置,但是一来 B 编译时知晓的结构和内部元素已经发生了一定的改变,二来在 C++进行基本的内存操作,尤其是对一个类的实体进行,本身就是一种比较危险的行为。如果编译 B 真的那么麻烦,A 的更改不如回滚。
    binsys
        5
    binsys  
       2024-08-20 07:59:28 +08:00
    上 protobuf 吧,解决你的需求。
    CapNemo
        6
    CapNemo  
       2024-08-20 08:32:28 +08:00
    把 A 中的 score 声明在最后一个就行了
    diivL
        7
    diivL  
       2024-08-20 08:57:04 +08:00
    你这是在自己给自己埋坑. 最好重新构建 B, 要么再封装一层 A.
    zzzyk
        8
    zzzyk  
       2024-08-20 09:07:03 +08:00
    两个结构体前面的成员类型保持一致。
    yolee599
        9
    yolee599  
       2024-08-20 09:14:00 +08:00 via Android
    要在结构体后面加,但是也要考虑对齐和 padding
    leonshaw
        10
    leonshaw  
       2024-08-20 09:25:39 +08:00 via Android
    这样 memcpy 就算能读,string 不会被析构掉吗?
    Skifary
        11
    Skifary  
       2024-08-20 09:36:33 +08:00
    搞这么复杂会埋一堆坑,如果 B 无法重新编译,A 和 app 可以,为什么不在 A 里面重新定一个新结构包含原有结构?
    StarsunYzL
        12
    StarsunYzL  
       2024-08-20 09:40:23 +08:00
    1 、简单点可以学微软 Win32 SDK ,结构第一个成员是结构大小,要求使用结构的人必须初始化这个成员,你的接口内通过这个成员的数值大小来判断结构是新是旧,缺点是新增结构成员只能加在结构最末尾:
    ```cpp
    struct OldMyStruct {
    uint32_t struct_size;
    int a;
    };

    struct MyStruct {
    uint32_t struct_size;
    int a;
    int new_a;
    };

    int getStructInfo(const MyStruct &struct) {
    if (struct.struct_size == sizeof(OldMyStruct)) {
    struct.a; // 只访问旧结构成员
    } else if (struct.struct_size == sizeof(MyStruct)) {
    struct.new_a; // 访问新结构成员
    } else {
    // 错误,未正确初始化结构
    }
    }

    // 使用者
    MyStruct my_struct;
    my_struct.struct_size = sizeof(my_struct);
    getStructInfo(my_struct);
    ```

    2 、该说不说,std::string 这种动态分配内存的结构成员,memcpy 拷贝 MyStruct 结构是不行的
    MoYi123
        13
    MoYi123  
       2024-08-20 10:26:47 +08:00
    要 std::is_trivially_copyable_v 的 struct 才能 memcpy, 更别说你这是 2 个不同的 struct 了.
    Gorvery
        14
    Gorvery  
    OP
       2024-08-20 10:26:56 +08:00 via Android
    @nagisaushio 成员顺序是变了,但是我定义了一个 old::Struct 和原结构体是一致的了
    Gorvery
        15
    Gorvery  
    OP
       2024-08-20 10:33:48 +08:00 via Android
    @ysc3839 嗯嗯,我大概理解你说的。
    但我的 old::Struct 和原来的 Struct 定义和内存布局是一样的,函数方法传入的类型是引用类型,取地址符后,和你例子中的指针类型不是一样的了吗

    我现在有办法区分是使用新的结构体传参调用的这个方法,还是老的
    Gorvery
        16
    Gorvery  
    OP
       2024-08-20 10:35:48 +08:00 via Android
    @8620 因为还有另外一个静态库 C 依赖了新的结构体定义,如果结构体回滚,这个影响更大。静态库 C 也是不可重新构建了
    mightybruce
        17
    mightybruce  
       2024-08-20 10:36:37 +08:00
    成员顺序变了是不可以 copy 的, 内存对齐和指针寻找每个成员的地址都不一样了。
    另外用静态库的方式导出,不要使用任何 stl 容器相关的类型,string 这种肯定是不如 char 数组的或 wchar 数组的
    Gorvery
        18
    Gorvery  
    OP
       2024-08-20 10:43:02 +08:00 via Android
    @diivL 哎,屎山工程,无法重新构建 B 了
    Gorvery
        19
    Gorvery  
    OP
       2024-08-20 10:44:26 +08:00 via Android
    @leonshaw string 真实值都很短,就两三个字符,理论上编译器优化的时候,用栈上的 3 字节空间就够存了,我理解算值拷贝?
    Gorvery
        20
    Gorvery  
    OP
       2024-08-20 10:45:51 +08:00 via Android
    @Skifary 因为屎山工程,还有另外一个静态库 C 依赖了新的结构体定义,如果结构体回滚或者再封装一层,这个影响更大。静态库 C 也是不可重新构建了
    thevita
        21
    thevita  
       2024-08-20 10:47:21 +08:00
    怎么会 A/B 的共同依赖有变更了, A/B 中只变更其中一个,这不是给自己找不痛快么

    不要纠结于 什么 新 Struct / 老 Struct , 就一个问题, A/B 的依赖关系怎么处理

    1) 保持依赖关系, 那就更新 B ,(这也交给你的构建系统去更新啊,又不耽误你自己)
    2) 拆掉依赖关系, 通过一些其他约定/API , 拆掉 A/B 间的依赖关系

    如果是那种 这个 .a 没源码有不可控的情况,这种模块也应该再包一层把它隔离掉
    Gorvery
        22
    Gorvery  
    OP
       2024-08-20 10:47:50 +08:00 via Android
    @StarsunYzL 如果不拷贝,只做指针偏移应该怎么处理,我只要能读到里面的一两个成员属性的值就可以了
    Gorvery
        23
    Gorvery  
    OP
       2024-08-20 10:48:31 +08:00 via Android
    @MoYi123 求问,不考虑 copy 的情况通过指针偏移能做吗
    greycell
        24
    greycell  
       2024-08-20 10:49:02 +08:00
    以为这样做省事,其实是浪费时间
    Gorvery
        25
    Gorvery  
    OP
       2024-08-20 10:51:13 +08:00 via Android
    @mightybruce 假如我可以区分是新老定义调用的这个方法,不考虑 copy 的情况可以通过指针偏移手段,读到使用老定义调用函数时,传入结构体中的部分属性么
    Gorvery
        26
    Gorvery  
    OP
       2024-08-20 10:57:51 +08:00
    @thevita 还有另外一个静态库 C 依赖了新的结构体定义,如果结构体回滚或者再封装一层,这个影响更大。静态库 C 也是不可重新构建了
    Skifary
        27
    Skifary  
       2024-08-20 11:13:06 +08:00
    @Gorvery 可以通过指针偏移的方式做到,但是这种方式要严格保证结构体成员顺序。

    int getStructInfo(const MyStruct& s)
    {
    auto sizeOfInt = sizeof(int);
    auto sizeOfFloat = sizeof(float);

    char* pointer = (char*)&s;

    old::MyStruct old;
    old.text_length = *((int*)(pointer));
    old.text = *((std::string*)(pointer + sizeOfInt + sizeOfFloat));

    return 0;
    }
    jones2000
        28
    jones2000  
       2024-08-20 11:25:03 +08:00   1
    结构体里面定义好,数据版本号, 不同的版本号,拷贝不同的大小。
    leonshaw
        29
    leonshaw  
       2024-08-20 11:26:16 +08:00
    直接 reinterpret_cast 成老 struct 的指针
    txhwind
        30
    txhwind  
       2024-08-20 14:07:20 +08:00   1
    工厂方法虚基类应该是 best practice 了,不过不知道屎山代码好不好改
    Gorvery
        31
    Gorvery  
    OP
       2024-08-20 15:04:32 +08:00
    @Skifary 不知道为啥我这边试了不好使,看汇编和逆向后的代码感觉没问题,也通过 offsetof 函数动态计算偏移了,还是取不到数据。
    Gorvery
        32
    Gorvery  
    OP
       2024-08-20 15:05:12 +08:00
    @leonshaw 这个方法我也试了,不行……我也不知道为啥
    leonshaw
        33
    leonshaw  
       2024-08-20 15:28:24 +08:00
    @Gorvery 那考虑是不是标准库实现或者 ABI 不一致了
    Skifary
        34
    Skifary  
       2024-08-20 15:33:20 +08:00
    @Gorvery 试过 msvc 那段函数是可以用的,这里还有一个 online 的版本用的 g++ https://wandbox.org/permlink/9VbIO1IZ4gV9Vzkb
    sampeng
        35
    sampeng  
       2024-08-20 15:36:45 +08:00
    这种东西重构静态库 B 是最优解。并且暴露出的不是 struct ,而是接口的方法是最优解。。。把所有不对的地方都重构一遍。

    从工程的角度来说,个人建议。。别这么干拷贝内存的事。不然就是下一个接手的人跳脚骂人。因为这就是副作用,也就是在偷偷摸摸的干了一些外部不知道的事。
    daimen
        36
    daimen  
       2024-08-20 16:52:09 +08:00
    变量申明顺序一致,对齐规则一致,编译环境一致,应该是可以的
    shapper
        37
    shapper  
       2024-08-20 17:48:11 +08:00
    B 要重新编译,B 中 A 的结构和符号还是之前没变化的原型,现在新 A 虽然变化了。假设 B 的结构跟着变为新 A 的,B 里原来的位置肯定 corrupt ,后面一连串的 corrup 。要想更新快改动态链接
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2745 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 29ms UTC 12:30 PVG 20:30 LAX 05:30 JFK 08:30
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86