2014-02-27 45 views
3

我正在阅读如何使用GCC的ld版本脚本在ELF共享库中版本化符号,并且我知道可以使用GCC的ld版本脚本导出不同版本的相同符号指令,如:GNU LD符号版本控制和C++二进制向后兼容性

__asm__(".symver original_foo,[email protected]_1.1"); 

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

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

编辑:我做了一个测试用例,似乎通过重命名一个类的所有符号到另一个类的工作。这看起来像我希望的那样工作,但是它保证工作还是我只是运气好?代码如下:

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

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

此技术是否可确保Linux上C++共享库中的类的二进制向后兼容性,而不管虚拟函数的差异如何?如果没有,为什么不呢? (一个反例会很棒)。

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,[email protected]_0"); 
__asm__(".symver _ZN7OldTestD0Ev,[email protected]_0"); 
__asm__(".symver _ZN7OldTest7doThingEv,[email protected]_0"); 
__asm__(".symver _ZN7OldTestD2Ev,[email protected]_0"); 
__asm__(".symver _ZTI7OldTest,[email protected]_0"); 
__asm__(".symver _ZTV7OldTest,[email protected]_0"); 
__asm__(".symver _ZN7OldTest2f1Ev,[email protected]_0"); 
__asm__(".symver _ZN7OldTest2f2Ev,[email protected]_0"); 
__asm__(".symver _ZN7OldTest2f3Ev,[email protected]_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,[email protected]_0"); 
__asm__(".symver _ZN4TestD0Ev,[email protected]_0"); 
__asm__(".symver _ZN4Test7doThingEv,[email protected]_0"); 
__asm__(".symver _ZN4Test2f1Ev,[email protected]_0"); 
__asm__(".symver _ZN4Test2f2Ev,[email protected]_0"); 
__asm__(".symver _ZN4Test2f3Ev,[email protected]_0"); 
__asm__(".symver _ZN4TestD2Ev,[email protected]_0"); 
__asm__(".symver _ZTI4Test,[email protected]_0"); 
__asm__(".symver _ZTV4Test,[email protected]_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; 
} 

回答

4

V表符号和/或版本是相当不重要无论对于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()函数扩展为也接受数字版本,您实际上也可以使用它来检索对象的其他接口。

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

+0

这看起来很有用,谢谢。但我认为我已经通过创建一个新类并将与之相关联的所有符号重命名为以前版本中的其他类来按照我想要的方式进行符号版本控制。我不确定我明白为什么这不应该起作用。 – Xeno

+0

这个.symver构造有趣的事情!感谢分享这个。但是我猜你确实很幸运:我认为这会发生:C++编译器在构建MyClass的虚拟表时并不关心symver语句。它只是查看头文件,并且头文件对于test0.cpp和test1.cpp是相同的,并且确定vtable布局的类始终为Orig。在这两种情况下,test()函数总是获得相同的vtable索引,所以很明显,这可以正常工作。在test0.cpp中,由于映射到LIB_0的符号,MyClass构造函数将调用... –

+0

... class old的基类构造函数。所以mc会指向Old的二进制vtable,但在你的代码中'mc'将使用C++编译器在标题中看到的内容,这是Orig的vtable。当你在这两个类定义中以不同的顺序插入函数f1(),f2()和f3()时,你会看到你不能映射函数,以至于Orig :: f1()调用Old :: f1()他们偶然处于同一个vtable位置。 –