GNU LD符号版本控制和C ++二进制向后兼容性

我正在阅读如何在ELF共享库中使用GCC的ld版本脚本来版本化符号,并且我知道可以使用如下指令导出同一个符号的不同版本:

__asm__(".symver original_foo,foo@VERS_1.1"); 

如果函数的语义发生了变化,这是非常有用的,但是库仍然应该导出旧版本,以便使用该库的旧应用程序仍然可以使用新版本。

但是对于C ++库, vtable for MyClass的符号vtable for MyClass将被导出。 如果以后我通过添加更多的虚拟函数来改变这个类,除了新版本的vtable之外,如何导出包含原始vtable符号的原始类?

编辑:我做了一个testing案例,似乎通过重命名一个类的所有符号到另一个的工作。 这似乎按照我所希望的方式工作,但保证工作,还是我运气好? 代码如下:

编辑2:我改变了类的名称(希望)不那么困惑,并将定义分成2个文件。

编辑3:它似乎与铿锵++也很好。 我将澄清我所问的整体问题:

这种技术是否可以保证Linux上C ++共享库中的类的二进制向后兼容性,而不pipe虚函数的差异如何? 如果没有,为什么不呢? (一个反例将是伟大的)。

libtest.h:

 struct Test { virtual void f1(); virtual void doNewThing(); virtual void f2(); virtual void doThing(); virtual void f3(); virtual ~Test(); }; 

libtest_old.h:

 // This header would have been libtest.h when test0 was theoretically developed. struct Test { virtual void f3(); virtual void f1(); virtual void doThing(); virtual void f2(); virtual ~Test(); }; 

libtest.cpp:

 #include "libtest.h" #include <cstdio> struct OldTest { virtual void f3(); virtual void f1(); virtual void doThing(); virtual void f2(); virtual ~OldTest(); }; __asm__(".symver _ZN7OldTestD1Ev,_ZN4TestD1Ev@LIB_0"); __asm__(".symver _ZN7OldTestD0Ev,_ZN4TestD0Ev@LIB_0"); __asm__(".symver _ZN7OldTest7doThingEv,_ZN4Test7doThingEv@LIB_0"); __asm__(".symver _ZN7OldTestD2Ev,_ZN4TestD2Ev@LIB_0"); __asm__(".symver _ZTI7OldTest,_ZTI4Test@LIB_0"); __asm__(".symver _ZTV7OldTest,_ZTV4Test@LIB_0"); __asm__(".symver _ZN7OldTest2f1Ev,_ZN4Test2f1Ev@LIB_0"); __asm__(".symver _ZN7OldTest2f2Ev,_ZN4Test2f2Ev@LIB_0"); __asm__(".symver _ZN7OldTest2f3Ev,_ZN4Test2f3Ev@LIB_0"); void OldTest::doThing(){ puts("OldTest doThing"); } void OldTest::f1(){ puts("OldTest f1"); } void OldTest::f2(){ puts("OldTest f2"); } void OldTest::f3(){ puts("OldTest f3"); } OldTest::~OldTest(){ } void Test::doThing(){ puts("New Test doThing from Lib1"); } void Test::f1(){ puts("New f1"); } void Test::f2(){ puts("New f2"); } void Test::f3(){ puts("New f3"); } void Test::doNewThing(){ puts("Test doNewThing, this wasn't in LIB0!"); } Test::~Test(){ } 

libtest.map:

 LIB_0 { global: extern "C++" { Test::doThing*; Test::f*; Test::Test*; Test::?Test*; typeinfo?for?Test*; vtable?for?Test* }; local: extern "C++" { *OldTest*; OldTest::*; }; }; LIB_1 { global: extern "C++" { Test::doThing*; Test::doNewThing*; Test::f*; Test::Test*; Test::?Test*; typeinfo?for?Test*; vtable?for?Test* }; } LIB_0; 

Makefile文件:

 all: libtest.so.0 test0 test1 libtest.so.0: libtest.cpp libtest.h libtest.map g++ -fPIC -Wl,-s -Wl,--version-script=libtest.map libtest.cpp -shared -Wl,-soname,libtest.so.0 -o libtest.so.0 test0: test0.cpp libtest.so.0 g++ test0.cpp -o test0 ./libtest.so.0 test1: test1.cpp libtest.so.0 g++ test1.cpp -o test1 ./libtest.so.0 

test0.cpp:

 #include "libtest_old.h" #include <cstdio> // in a real-world scenario, these symvers would not be present and this file // would include libtest.h which would be what libtest_old.h is now. __asm__(".symver _ZN4TestD1Ev,_ZN4TestD1Ev@LIB_0"); __asm__(".symver _ZN4TestD0Ev,_ZN4TestD0Ev@LIB_0"); __asm__(".symver _ZN4Test7doThingEv,_ZN4Test7doThingEv@LIB_0"); __asm__(".symver _ZN4Test2f1Ev,_ZN4Test2f1Ev@LIB_0"); __asm__(".symver _ZN4Test2f2Ev,_ZN4Test2f2Ev@LIB_0"); __asm__(".symver _ZN4Test2f3Ev,_ZN4Test2f3Ev@LIB_0"); __asm__(".symver _ZN4TestD2Ev,_ZN4TestD2Ev@LIB_0"); __asm__(".symver _ZTI4Test,_ZTI4Test@LIB_0"); __asm__(".symver _ZTV4Test,_ZTV4Test@LIB_0"); struct MyClass : public Test { virtual void test(){ puts("Old Test func"); } virtual void doThing(){ Test::doThing(); puts("Override of Old Test::doThing"); } }; int main(void){ MyClass* mc = new MyClass(); mc->f1(); mc->f2(); mc->f3(); mc->doThing(); mc->test(); delete mc; return 0; } 

test1.cpp:

 #include "libtest.h" #include <cstdio> struct MyClass : public Test { virtual void doThing(){ Test::doThing(); puts("Override of New Test::doThing"); } virtual void test(){ puts("New Test func"); } }; int main(void){ MyClass* mc = new MyClass(); mc->f1(); mc->f2(); mc->f3(); mc->doThing(); mc->doNewThing(); mc->test(); delete mc; return 0; } 

vtable符号和/或版本对API和ABI来说都是不重要的。 重要的是哪个vtable索引有哪些语义。 vtable的名称和/或版本无关紧要。

通过使用一些轻量级运行时机制来获取特定接口的特定版本,可以实现向后兼容性。 假设你有:

 class MyThing: public VersionedInterface {...}; // V1 class MyThingV1: public MyThing {...}; class MyThingV2: public MyThingV1 {...}; 

你可能有一些功能来创建MyThings:

 VersionedInterface *createMyThing(); 

然后这个VersionedInterface需要你想要的接口版本(你的代码可以理解):

 // Old code will ask for MyThing: VersionedInterface *vi = createMyThing(); MyThing *myThing = static_cast<MyThing*>(vi->getInterface("MyThing")); // New code may ask for MyThingV2: VersionedInterface *vi = createMyThing(); MyThingV2 *myThing = static_cast<MyThingV2*>(vi->getInterface("MyThingV2")); // New code may or may not get the newer interface: if (!myThing) { // We did not get the interface version we wanted. // We can either consciously fall back to an older version or simply fail. ... } 

VersionedInterface只提供getInterface()函数:

 class VersionedInterface { public: virtual ~VersionedInterface() {} virtual VersionedInterface *getInterface(const char *interfaceName) = 0; }; 

这种方法的优点是它允许以一种干净和便携的方式对vtable进行任意改变(重新排序函数,插入和删除函数,改变函数原型)。

您可以扩展getInterface()函数以接受数字版本,事实上也可以使用它来检索对象的其他接口。

您可以稍后添加接口到对象,而不会破坏现有的二进制代码。 这是主要的优势。 当然,获得界面的代码样本的代价是。 而维护同一个接口的多个版本当然也有自己的成本。 应该很好的考虑这个努力是否值得。