我有一个静态库 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
nagisaushio 123 天前 via Android
你成员顺序变了啊
|
2
ysc3839 123 天前
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)); } ``` |
3
levelworm 123 天前 via Android
memcpy 是按照内存来拷贝,他不管你结构是什么样子的。
|
4
8620 123 天前 via Android
虽然理论上通过偏移能获得更改后的结构体中原来元素的位置,但是一来 B 编译时知晓的结构和内部元素已经发生了一定的改变,二来在 C++进行基本的内存操作,尤其是对一个类的实体进行,本身就是一种比较危险的行为。如果编译 B 真的那么麻烦,A 的更改不如回滚。
|
5
binsys 123 天前
上 protobuf 吧,解决你的需求。
|
6
CapNemo 123 天前
把 A 中的 score 声明在最后一个就行了
|
7
diivL 123 天前
你这是在自己给自己埋坑. 最好重新构建 B, 要么再封装一层 A.
|
8
zzzyk 123 天前
两个结构体前面的成员类型保持一致。
|
9
yolee599 123 天前 via Android
要在结构体后面加,但是也要考虑对齐和 padding
|
10
leonshaw 123 天前 via Android
这样 memcpy 就算能读,string 不会被析构掉吗?
|
11
Skifary 123 天前
搞这么复杂会埋一堆坑,如果 B 无法重新编译,A 和 app 可以,为什么不在 A 里面重新定一个新结构包含原有结构?
|
12
StarsunYzL 123 天前
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 结构是不行的 |
13
MoYi123 123 天前
要 std::is_trivially_copyable_v 的 struct 才能 memcpy, 更别说你这是 2 个不同的 struct 了.
|
14
Gorvery OP @nagisaushio 成员顺序是变了,但是我定义了一个 old::Struct 和原结构体是一致的了
|
15
Gorvery OP @ysc3839 嗯嗯,我大概理解你说的。
但我的 old::Struct 和原来的 Struct 定义和内存布局是一样的,函数方法传入的类型是引用类型,取地址符后,和你例子中的指针类型不是一样的了吗😂 我现在有办法区分是使用新的结构体传参调用的这个方法,还是老的 |
17
mightybruce 123 天前
成员顺序变了是不可以 copy 的, 内存对齐和指针寻找每个成员的地址都不一样了。
另外用静态库的方式导出,不要使用任何 stl 容器相关的类型,string 这种肯定是不如 char 数组的或 wchar 数组的 |
19
Gorvery OP @leonshaw string 真实值都很短,就两三个字符,理论上编译器优化的时候,用栈上的 3 字节空间就够存了,我理解算值拷贝?
|
20
Gorvery OP @Skifary 因为屎山工程,还有另外一个静态库 C 依赖了新的结构体定义,如果结构体回滚或者再封装一层,这个影响更大。静态库 C 也是不可重新构建了
|
21
thevita 123 天前
怎么会 A/B 的共同依赖有变更了, A/B 中只变更其中一个,这不是给自己找不痛快么
不要纠结于 什么 新 Struct / 老 Struct , 就一个问题, A/B 的依赖关系怎么处理 1) 保持依赖关系, 那就更新 B ,(这也交给你的构建系统去更新啊,又不耽误你自己) 2) 拆掉依赖关系, 通过一些其他约定/API , 拆掉 A/B 间的依赖关系 如果是那种 这个 .a 没源码有不可控的情况,这种模块也应该再包一层把它隔离掉 |
22
Gorvery OP @StarsunYzL 如果不拷贝,只做指针偏移应该怎么处理,我只要能读到里面的一两个成员属性的值就可以了
|
24
greycell 123 天前
以为这样做省事,其实是浪费时间
|
25
Gorvery OP @mightybruce 假如我可以区分是新老定义调用的这个方法,不考虑 copy 的情况可以通过指针偏移手段,读到使用老定义调用函数时,传入结构体中的部分属性么
|
27
Skifary 123 天前
@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; } |
28
jones2000 123 天前 1
结构体里面定义好,数据版本号, 不同的版本号,拷贝不同的大小。
|
29
leonshaw 123 天前
直接 reinterpret_cast 成老 struct 的指针
|
30
txhwind 123 天前 1
工厂方法虚基类应该是 best practice 了,不过不知道屎山代码好不好改
|
34
Skifary 123 天前
@Gorvery 试过 msvc 那段函数是可以用的,这里还有一个 online 的版本用的 g++ https://wandbox.org/permlink/9VbIO1IZ4gV9Vzkb
|
35
sampeng 123 天前
这种东西重构静态库 B 是最优解。并且暴露出的不是 struct ,而是接口的方法是最优解。。。把所有不对的地方都重构一遍。
从工程的角度来说,个人建议。。别这么干拷贝内存的事。不然就是下一个接手的人跳脚骂人。因为这就是副作用,也就是在偷偷摸摸的干了一些外部不知道的事。 |
36
daimen 123 天前
变量申明顺序一致,对齐规则一致,编译环境一致,应该是可以的
|
37
shapper 123 天前
B 要重新编译,B 中 A 的结构和符号还是之前没变化的原型,现在新 A 虽然变化了。假设 B 的结构跟着变为新 A 的,B 里原来的位置肯定 corrupt ,后面一连串的 corrup 。要想更新快改动态链接
|