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

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

  •  
  •   Gorvery · 85 天前 · 2296 次点击
    这是一个创建于 85 天前的主题,其中的信息可能已经有所发展或是发生改变。

    背景

    我有一个静态库 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 条附言  ·  85 天前
    更新下为什么选择用这么不可靠的方式来解决问题的原因,是因为屎山代码,还有有另外一个静态库 C 依赖了新的结构体定义,如果结构体定义修改回滚或者再封装一层,需要修改的地方更多。静态库 C 和静态库 B 同样都是不可重新构建了。

    假如 memcpy 的方式不可实现,可以通过指针偏移读取其中的一两个关键值吗?
    或者通过二进制修改的方式,修改掉静态库 B 中的符号,用一个新的 namespace 进行隔离,然后在静态库 A 中补充上这个新的 namespace 隔离后的定义,这个可以么?
    37 条回复    2024-08-20 17:48:11 +08:00
    nagisaushio
        1
    nagisaushio  
       85 天前 via Android
    你成员顺序变了啊
    ysc3839
        2
    ysc3839  
       85 天前
    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  
       85 天前 via Android
    memcpy 是按照内存来拷贝,他不管你结构是什么样子的。
    8620
        4
    8620  
       85 天前 via Android
    虽然理论上通过偏移能获得更改后的结构体中原来元素的位置,但是一来 B 编译时知晓的结构和内部元素已经发生了一定的改变,二来在 C++进行基本的内存操作,尤其是对一个类的实体进行,本身就是一种比较危险的行为。如果编译 B 真的那么麻烦,A 的更改不如回滚。
    binsys
        5
    binsys  
       85 天前
    上 protobuf 吧,解决你的需求。
    CapNemo
        6
    CapNemo  
       85 天前
    把 A 中的 score 声明在最后一个就行了
    diivL
        7
    diivL  
       85 天前
    你这是在自己给自己埋坑. 最好重新构建 B, 要么再封装一层 A.
    zzzyk
        8
    zzzyk  
       85 天前
    两个结构体前面的成员类型保持一致。
    yolee599
        9
    yolee599  
       85 天前 via Android
    要在结构体后面加,但是也要考虑对齐和 padding
    leonshaw
        10
    leonshaw  
       85 天前 via Android
    这样 memcpy 就算能读,string 不会被析构掉吗?
    Skifary
        11
    Skifary  
       85 天前
    搞这么复杂会埋一堆坑,如果 B 无法重新编译,A 和 app 可以,为什么不在 A 里面重新定一个新结构包含原有结构?
    StarsunYzL
        12
    StarsunYzL  
       85 天前
    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  
       85 天前
    要 std::is_trivially_copyable_v 的 struct 才能 memcpy, 更别说你这是 2 个不同的 struct 了.
    Gorvery
        14
    Gorvery  
    OP
       85 天前 via Android
    @nagisaushio 成员顺序是变了,但是我定义了一个 old::Struct 和原结构体是一致的了
    Gorvery
        15
    Gorvery  
    OP
       85 天前 via Android
    @ysc3839 嗯嗯,我大概理解你说的。
    但我的 old::Struct 和原来的 Struct 定义和内存布局是一样的,函数方法传入的类型是引用类型,取地址符后,和你例子中的指针类型不是一样的了吗😂

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

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

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

    如果是那种 这个 .a 没源码有不可控的情况,这种模块也应该再包一层把它隔离掉
    Gorvery
        22
    Gorvery  
    OP
       85 天前 via Android
    @StarsunYzL 如果不拷贝,只做指针偏移应该怎么处理,我只要能读到里面的一两个成员属性的值就可以了
    Gorvery
        23
    Gorvery  
    OP
       85 天前 via Android
    @MoYi123 求问,不考虑 copy 的情况通过指针偏移能做吗
    greycell
        24
    greycell  
       85 天前
    以为这样做省事,其实是浪费时间
    Gorvery
        25
    Gorvery  
    OP
       85 天前 via Android
    @mightybruce 假如我可以区分是新老定义调用的这个方法,不考虑 copy 的情况可以通过指针偏移手段,读到使用老定义调用函数时,传入结构体中的部分属性么
    Gorvery
        26
    Gorvery  
    OP
       85 天前
    @thevita 还有另外一个静态库 C 依赖了新的结构体定义,如果结构体回滚或者再封装一层,这个影响更大。静态库 C 也是不可重新构建了
    Skifary
        27
    Skifary  
       85 天前
    @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  
       85 天前   ❤️ 1
    结构体里面定义好,数据版本号, 不同的版本号,拷贝不同的大小。
    leonshaw
        29
    leonshaw  
       85 天前
    直接 reinterpret_cast 成老 struct 的指针
    txhwind
        30
    txhwind  
       85 天前   ❤️ 1
    工厂方法虚基类应该是 best practice 了,不过不知道屎山代码好不好改
    Gorvery
        31
    Gorvery  
    OP
       85 天前
    @Skifary 不知道为啥我这边试了不好使,看汇编和逆向后的代码感觉没问题,也通过 offsetof 函数动态计算偏移了,还是取不到数据。😭
    Gorvery
        32
    Gorvery  
    OP
       85 天前
    @leonshaw 这个方法我也试了,不行……我也不知道为啥
    leonshaw
        33
    leonshaw  
       84 天前
    @Gorvery 那考虑是不是标准库实现或者 ABI 不一致了
    Skifary
        34
    Skifary  
       84 天前
    @Gorvery 试过 msvc 那段函数是可以用的,这里还有一个 online 的版本用的 g++ https://wandbox.org/permlink/9VbIO1IZ4gV9Vzkb
    sampeng
        35
    sampeng  
       84 天前
    这种东西重构静态库 B 是最优解。并且暴露出的不是 struct ,而是接口的方法是最优解。。。把所有不对的地方都重构一遍。

    从工程的角度来说,个人建议。。别这么干拷贝内存的事。不然就是下一个接手的人跳脚骂人。因为这就是副作用,也就是在偷偷摸摸的干了一些外部不知道的事。
    daimen
        36
    daimen  
       84 天前
    变量申明顺序一致,对齐规则一致,编译环境一致,应该是可以的
    shapper
        37
    shapper  
       84 天前
    B 要重新编译,B 中 A 的结构和符号还是之前没变化的原型,现在新 A 虽然变化了。假设 B 的结构跟着变为新 A 的,B 里原来的位置肯定 corrupt ,后面一连串的 corrup 。要想更新快改动态链接
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5365 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 07:15 · PVG 15:15 · LAX 23:15 · JFK 02:15
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.