通过 C ABI 通过 void 指针传递 C++ 对象(可能具有多重虚拟继承)

Pass C++ object (with possible multiple virtual inheritance) through a C ABI via void pointer

我对类型转换的安全性有些担忧我正在设计一个抽象接口, 将由导出面向对象的 C ABI 的插件支持,即指向对象的指针和 C 样式函数形式 func(void *this, ...) 而不是 C++ 风格的成员函数,然后这些将被打包到表示对象实现的结构中。但是我的一些底层框架使用了多重虚拟继承。

简化示例

class A
{
    public:
        virtual void doA()
}

class B
{
    public:
        virtual void doB()
}

class C : public A, public B
{
    public:
        virtual void doA()
        virtual void doB()
}

struct impA
{
    (*doA)(void *self);
}

struct impB
{
    (*doB)(void *self);
}

struct impC
{
    (*doA)(void *self);
    (*doB)(void *self);
}

void * AfromC(void *v) {
    C*c = reinterpret_cast<C*>(v); // Known to be C* type
    return static_cast<void*>(static_cast<A*>(c)); // method 1
    return reinterpret_cast<void*>(static_cast<A*>(c)); // method 2

    //method 3 & 4
    C** c = static_cast<C**>(v); // Known to be C* type
    return static_cast<void*>(&static_cast<A*>(*c)); // method 3
    return static_cast<void*>(static_cast<A**>(c)); // method 4
}

/////////// main code

class A
{
    public:
        void doA() { imp.doA(self); }
    private:
        impA imp;
        void *self;
}

class B
{
    public:
        void doB() { imp.doB(self); }
    private:
        impB imp;
        void *self;
}

考虑 AfromC,我有 4 种可能的方法来获取可以安全地通过 C ABI 的指针,我想知道对这些不同方法的考虑,我的首选是方法 1。

我不确定所有这些方法是否合法或安全。

注意:对象将始终由它们所在的二进制文件中的函数访问 created/destroyed 它们 return/accept 它们处理的其他对象自身或 C 样式数据类型(直到 POD 的结构)

虽然我在网上发现了此类事情的提及,但它们都是关于人们因转换为 void 而遇到问题的,即 A* a= static_cast<A*>(static_cast<void*>(c)) // c -> C* 这是意料之中的,因为这并没有纠正vtable 和解决方案是使用抽象基类型(这对我不起作用,因为我需要通过 C ABI),但是我也听说虚拟指针比普通指针大,因此我考虑的原因方法 3 和 4,因为这将是指向较大指针的普通指针,因此即使对于具有较大指针的类型也是安全的。

所以我的主要问题是方法 1 是否可以正常工作?另外,我能否按照 template <typename T, typename U> void * void_cast(U v) { static_cast<void *>(static_cast<T>(v)); } 的方式安全地定义一个模板函数来简化插件代码。最后,如果方法 1 是正确的,为什么?可以使用任何方法吗?

return static_cast<void*>(static_cast<A*>(v)); // method 1
return reinterpret_cast<void*>(static_cast<A*>(v)); // method 2

如果void* v指向类型C的实例,那么static_cast<A*>(v)是错误的。

//method 3 & 4
C** c = static_cast<C**>(v); // Known to be C* type
return static_cast<void*>(&static_cast<A*>(*c)); // method 3
return static_cast<void*>(static_cast<A**>(c)); // method 4

如果void* v指向类型C的实例,那么static_cast<C**>(v)是错误的。


void* 转换为指向对象的正确类型时要非常小心。如果可以,我宁愿使用 static_cast,而不是 reinterpret_cast。我也更喜欢隐式转换以访问基本子对象和转换为 void*。减少的样板对眼睛来说不那么紧张。

void* AfromC(void* v) {
    C* c = static_cast<C*>(v); // Known to be C* type
    A* a = c;                  // point to base sub object
    return a;                  // implicit conversion to void*
}

规则是您可以在指向对象的指针和指向其基址的指针 class 以及从指向对象的指针和 void * 之间来回转换。但是不能保证所有这些指针都保持相同的值(甚至不能保持相同的表示)!

与 C 派生自 A 的示例不同:

C* c = new C;
A* a = static_cast<A*>(c);   // legal
C* c1 = static_cast<C*>(a);  // ok c1 == c guaranteed

void *vc = static_cast<void *>(c); // legal
C* c2 = static_cast<C*>(vc); // ok, c2 == c guaranteed

void *va = static_cast<void *>(a); // legal, but va == vc is not guaranteed

a2 = static_cast<A*>(vc);    // legal, but a2 == a not guaranteed 
                             //  and dereferencing a2 is Undefined Behaviour

这意味着如果v被构建为void *v = static_cast<void *>(c);然后传递给您的 AfromC 方法 static_cast<A*>(v) 可能未指向有效对象。方法 (1) 和 (2) 都是 no-op 因为您从 void* 转换为指向 obj 的指针并返回,这是获取原始值所必需的。

对于方法 (4),但是您将指向 void 的指针转换为指向指针的指针,从指针到指针再到指向指针的指针,然后再返回到 void。如 3.9.2 复合类型 [basic.compound] 声明:

3 ...Pointers to layout-compatible types shall have the same value representation and alignment requirements...

因为所有指针都是布局兼容类型,第二个操作不应该改变值,我们回到方法(1)和(2)的no-op )

方法 (3) 甚至不应该编译,因为你取了一个 static_cast 的地址,那不是左值。

TL/DR:方法(1)、(2)、(4)是no-op,也就是说你return输入值不变,方法(3)是非法的,因为& 运算符需要一个左值。

将指向 C 对象的 void* 转换为可以安全转换为 A* 的东西的唯一可行方法是:

void * AfromC(void *v) {
    C* c = static_cast<C*>(v);   // v shall be static_cast<void>(ptr_to_C)
    A* a = static_cast<A*>(c);
    return static_cast<void *>(a); // or return a; with the implicit void * convertion
}

或作为单行表达式

void * AfromC(void *v) {
    return static_cast<A*>(static_cast<C*>(v));
}