在g++中如何让类成员符号在运行时而不是链接时解析?
问题场景
手里有个厂商提供的不可修改的库,项目里只有小部分用到它,但存在不能引入该库的场景。之前要编译两个版本(链接/不链接库),维护成本极高,想通过运行时动态加载来解决这个问题。
之前修改库的头文件,给所有符号加上__attribute__((weak))的方法可行,但现在不能碰原厂商的头文件。非成员函数可以用#pragma weak或者外部弱声明搞定,但类的成员函数这么做会报错:
- 用
#pragma weak myclass::myfunc会被编译器判定为无效语法 - 外部声明
void myclass::myfunc(void) __attribute__((weak));会被当成非法的类外声明
可行解决方案
方案1:利用C++名字修饰直接绑定弱符号
GCC/Clang这类编译器会对C符号做名字修饰,类成员函数的修饰名是固定格式的。你可以先拿到修饰后的符号名,再用#pragma weak指定,就能绕过C语法层面的限制。
步骤:
获取成员函数的修饰名
用nm工具从厂商库中导出目标符号的修饰名:nm -D libdummy.so | grep myclass::myfunc输出类似
_ZN7myclass6myfuncEv(不同编译器/版本可能略有差异),把这个名字记下来。在wrapper代码中添加弱声明并调用
直接包含原头文件,针对每个成员函数用修饰名做弱声明,之后就能像正常一样调用类的方法:#include <dlfcn.h> #include <cstdio> #include "libdummy.hpp" // 直接用原头文件,不修改 // 用#pragma weak指定修饰后的符号名 #pragma weak _ZN7myclassC1Ev // myclass() 构造函数 #pragma weak _ZN7myclassC1Ei // myclass(int) 构造函数 #pragma weak _ZN7myclass6myfuncEv // myclass::myfunc() #pragma weak _ZN7myclass7myfunc2Ev // myclass::myfunc2() #pragma weak _ZN7myclass7myfunc2Eii// myclass::myfunc2(int,int) #pragma weak _Z6myfuncv // 非成员myfunc() void call_wrapped_library(void) { void *handle = dlopen("libdummy.so", RTLD_NOW|RTLD_GLOBAL); if (!handle) { fprintf(stderr, "dlopen失败: %s\n", dlerror()); return; } // 弱符号在库加载后会被替换为强符号,直接正常调用即可 myclass object; myfunc(); object.myfunc(); object.myfunc2(); object.myfunc2(1, 2); myclass object2(20); object2.myfunc(); dlclose(handle); }
方案2:封装代理类,手动绑定动态符号
如果不想依赖编译器的名字修饰(比如担心跨版本兼容性),可以写一个代理类,通过dlsym手动获取每个成员函数的地址,用函数指针调用:
#include <dlfcn.h> #include <cstdio> // 封装代理类,对外提供和原类一致的接口 class MyClassProxy { private: void* m_instance; // 存储原类对象的指针 // 定义对应成员函数的函数指针类型 using CtorFunc = void*(*)(); using CtorIntFunc = void*(*)(int); using MyFuncFunc = void(*)(void*); using MyFunc2Func = void(*)(void*); using MyFunc2IntFunc = void(*)(void*, int, int); using DtorFunc = void(*)(void*); CtorFunc m_ctor; CtorIntFunc m_ctor_int; MyFuncFunc m_myfunc; MyFunc2Func m_myfunc2; MyFunc2IntFunc m_myfunc2_int; DtorFunc m_dtor; public: // 默认构造代理 MyClassProxy(void* handle) { // 通过dlsym获取每个符号的地址 m_ctor = reinterpret_cast<CtorFunc>(dlsym(handle, "_ZN7myclassC1Ev")); m_ctor_int = reinterpret_cast<CtorIntFunc>(dlsym(handle, "_ZN7myclassC1Ei")); m_myfunc = reinterpret_cast<MyFuncFunc>(dlsym(handle, "_ZN7myclass6myfuncEv")); m_myfunc2 = reinterpret_cast<MyFunc2Func>(dlsym(handle, "_ZN7myclass7myfunc2Ev")); m_myfunc2_int = reinterpret_cast<MyFunc2IntFunc>(dlsym(handle, "_ZN7myclass7myfunc2Eii")); m_dtor = reinterpret_cast<DtorFunc>(dlsym(handle, "_ZN7myclassD1Ev")); if (!m_ctor) { fprintf(stderr, "获取构造函数失败: %s\n", dlerror()); m_instance = nullptr; return; } m_instance = m_ctor(); } // 带参数的构造代理 MyClassProxy(int x, void* handle) { if (!m_ctor_int) { m_ctor_int = reinterpret_cast<CtorIntFunc>(dlsym(handle, "_ZN7myclassC1Ei")); if (!m_ctor_int) { fprintf(stderr, "带参构造函数获取失败: %s\n", dlerror()); m_instance = nullptr; return; } } m_instance = m_ctor_int(x); } // 析构代理 ~MyClassProxy() { if (m_instance && m_dtor) { m_dtor(m_instance); } } // 封装原类的成员函数 void myfunc() { if (m_instance && m_myfunc) { m_myfunc(m_instance); } } void myfunc2() { if (m_instance && m_myfunc2) { m_myfunc2(m_instance); } } void myfunc2(int x, int y) { if (m_instance && m_myfunc2_int) { m_myfunc2_int(m_instance, x, y); } } }; // 非成员函数的动态绑定 using MyFunc = void(*)(); void call_wrapped_library(void) { void *handle = dlopen("libdummy.so", RTLD_NOW|RTLD_GLOBAL); if (!handle) { fprintf(stderr, "dlopen失败: %s\n", dlerror()); return; } // 调用非成员函数 MyFunc myfunc = reinterpret_cast<MyFunc>(dlsym(handle, "_Z6myfuncv")); if (myfunc) { myfunc(); } // 使用代理类调用成员函数 MyClassProxy obj(handle); obj.myfunc(); obj.myfunc2(); obj.myfunc2(1, 2); MyClassProxy obj2(20, handle); obj2.myfunc(); dlclose(handle); }
这种方式完全不依赖弱链接,手动管理所有符号的绑定,兼容性更强,但需要逐个维护成员函数的指针定义。
方案3:构建中间包装库(适合大型项目)
如果项目规模大,可以做一个中间包装库,把厂商库的符号全部声明为弱符号,项目只链接这个包装库:
编写包装库代码(
libdummy_wrap.cc):// 自己定义宏注入弱属性 #define EXTERNAL_SYMBOL __attribute__((weak)) #include "libdummy.hpp" // 不需要实现任何函数,仅声明弱符号编译包装库:
g++ -g -shared -fPIC -o libdummy_wrap.so libdummy_wrap.cc项目链接包装库:
项目代码正常调用原类的接口,编译时链接libdummy_wrap.so,运行时如果需要加载厂商库,直接调用dlopen("libdummy.so", RTLD_GLOBAL)即可,此时包装库的弱符号会被厂商库的强符号覆盖。
这种方式不需要修改项目代码,只需要维护一个轻量的包装库,适合需要长期维护的大型项目。
内容的提问来源于stack exchange,提问作者Compholio

