我们假设,我有一个C结构,DynApiArg_t.
typedef struct DynApiArg_s {
uint32_t m1;
...
uint32_t mx;
} DynApiArg_t;
这个结构的指针作为arg传递给函数说
void DynLibApi(DynApiArg_t *arg)
{
arg->m1 = 0;
another_fn_in_the_lib(arg->mold); /* May crash here. (1) */
}
它存在于动态库libdyn.so中.通过调用的dlopen / dlsym过程从可执行文件调用此API.
如果此动态库更新到版本2,其中DynApiArg_t现在有新成员,比如m2,如下所示:
typedef struct DynApiArg_s {
uint32_t m1;
OldMbr_t *mold;
...
uint32_t mx;
uint32_t m2;
NewMbr *mnew;
} DynApiArg_t;
如果没有完全重建通过dlopen / dlsym调用此API的可执行文件或其他库,每次调用此API时,由于结构中任何成员的某些取消引用,我看到该进程崩溃.我知道访问m2可能是个问题.但是看到像下面这样的成员模具会导致崩溃.
typedef void (*fnPtr_t)(DynApiArg_t*);
void DynApiCaller(DynApiArg_t *arg)
{
void *libhdl = dlopen("libdyn.so", RTLD_LAZY | RTLD_GLOBAL);
fnPtr_t fptr = dlsym(libhdl, "DynLibApi");
fnptr(arg); /* actual call to the dynamically loaded API (2) */
}
在通过fnptr调用API时,在标记为(2)的行中,当旧的/现有成员(在lib的v1中,当最初编译DynApiCaller时)在(1)处被访问时,它恰好是任何垃圾值甚至是有时为NULL.
每次更新从属库时,如果没有完整重新编译可执行文件,处理此类更新的正确方法是什么?
我已经看到libs用symliks命名,版本号如libsolid.so.4.是否有与此版本控制系统相关的内容可以帮助我?如果是这样的话,你可以指出我的正确文件吗?
最佳答案 有许多方法可以解决这个问题:
>在动态库名称中包含API版本.
而不是dlopen(“libfoo.so”),您使用dlopen(“libfoo.so.4”).该库的不同主要版本基本上是分开的,可以在同一系统上共存;因此,该库的包名称将是例如libfoo的-4.您可以同时安装libfoo.so.4和libfoo.so.5.次要版本,比如libfoo-4.2,安装libfoo.so.4.2,symlink libfoo.so.4到libfoo.so.4.2.
>最初使用零填充定义结构(在早期版本的库中要求为零),并使更高版本重用填充字段,但保持结构大小相同.
>使用版本化的符号名称.这是Linux扩展,使用dlvsym()
.单个共享库二进制文件可以实现相同动态符号的多个版本.
>使用resolver functions确定加载时的符号.这允许例如硬件体系结构优化的函数变体在运行时选择,但对于基于dlopen()的方法则不太有用.
>使用结构来描述库API,以及用于获取/初始化该API的版本化函数.
例如,您的库的第4版可以实现
struct libfoo_api {
int (*func1)(int arg1, int arg2);
double *data;
void (*func2)(void);
/* ... */
};
并且只导出一个符号,
int libfoo_init(struct libfoo_api *const api, const int version);
调用该函数将使用支持的符号初始化api结构,并假设结构对应于指定的版本.单个共享库可以支持多个版本.如果不支持某个版本,则可能会返回失败.
这对于插件类型的接口特别有用(尽管_init函数更可能调用应用程序提供的功能注册函数,而不是填充结构),因为单个文件可以包含针对多个版本的优化功能,优化适用于多种兼容的硬件架构(例如,支持不同SSE / AVX / AVX2 / AVX512的AMD / Intel架构).
请注意,上述实现细节可以“隐藏”在头文件中,使得使用共享库的实际C代码更加简单.它还有助于使相同的API跨多个操作系统工作,只需更改头文件即可使用最适合该操作系统的方法,同时保持实际的C接口不变.