我们假设,我有一个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可能是个问题。但是,如下所示访问会员mold
会导致崩溃。
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
。是否有与此版本控制系统相关的内容可以帮助我?如果是这样,你可以指出我为这些文件提供正确的文件吗?
答案 0 :(得分:1)
有很多方法可以解决此问题:
在动态库名称中包含API版本。
使用dlopen("libfoo.so")
代替dlopen("libfoo.so.4")
。该库的不同主要版本本质上是分开的,并且可以在同一系统上共存。因此,该库的软件包名称为libfoo-4
。您可以同时安装libfoo.so.4
和libfoo.so.5
。次版本,例如libfoo-4.2
,安装libfoo.so.4.2
,并将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代码更加简单。只需更改头文件以使用最适合该OS的方法,同时保持实际C接口相同,它还有助于使相同的API在多个OS上都能正常工作。