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

Posted

技术标签:

【中文标题】通过 void 指针通过 C ABI 传递 C++ 对象(可能具有多重虚拟继承)【英文标题】:Pass C++ object (with possible multiple virtual inheritance) through a C ABI via void pointer 【发布时间】:2017-01-25 09:54:41 【问题描述】:

我对类型转换的安全性有些担心我正在设计一个抽象接口,导出面向对象的 C ABI 的插件将支持女巫,即指向对象的指针和 func(void *this, ...) 形式的 C 风格函数,而不是与 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。

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

注意:对象将始终由创建/销毁它们的二进制文件中的函数访问,它们返回/接受由它们自己处理的其他对象或 C 样式数据类型(直到 POD 的结构)

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

所以我的主要问题是方法 1 会不会出现问题?我可以按照template &lt;typename T, typename U&gt; void * void_cast(U v) static_cast&lt;void *&gt;(static_cast&lt;T&gt;(v)); 的方式安全地定义一个模板函数来简化插件代码。最后,如果方法1是正确的,为什么?任何一种方法都可以使用吗?

【问题讨论】:

【参考方案1】:

规则是,您可以从指向对象的指针到指向其基类的指针,以及从指向对象的指针到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&lt;void *&gt;(c);然后传递给您的AfromC 方法static_cast&lt;A*&gt;(v) 可能未指向有效对象。并且方法 (1) 和 (2) 都是无操作的,因为您将 void* 转换为指向 obj 的指针并返回,这是获取原始值所必需的。

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

3 ...指向布局兼容类型的指针应具有相同的值表示和 对齐要求...

由于所有指针都是布局兼容类型,第二个操作不应该改变值,我们又回到了方法(1)和(2)的空操作

方法 (3) 甚至不应该编译,因为您获取的是 static_cast 的地址,而那不是左值。

TL/DR:方法 (1)、(2) 和 (4) 是无操作的,这意味着您返回输入值不变并且方法 (3) 是非法的,因为 &amp; 运算符需要一个左值。

将指向 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));

【讨论】:

如果我将 va 转换为 a as A* = static_cast(va),您能否澄清“但不能保证 va == vc”,然后我可以将其转换为类型的对象吗C 即 static_cast(a) 或 reinterpret_cast(a),因为我知道 va 已按照您的答案从 C 类型的对象转换,或者在转换后不再安全void 类型? “取消引用 a2 是未定义的行为”想解释一下您为什么这么认为?据我所知,在一般视图中,a2c2ac 相同,因此它们应该是相同的 UB - 是或否,而不是中途。 @glenflet:标准中不能保证这一点。在使用虚表实现虚函数的实现中,对象的地址通常是虚表的地址。在这种情况下,static_cast 会进行偏移校正,但vavc 的实际值在这种情况下是不同的。只需尝试使用调试器以及具有虚函数的基类和子类 @dascandy: va 是通过从指向派生的指针的静态转换构建的。在常见的实现中,vcc 将具有相同的表示和值,但如果 A 和 C 都具有虚函数,vavc 将具有不同的值。作为va2,如果从void * 构建,它将具有与vc 相同的值,因此它无法指向有效对象。这就是为什么取消引用它是 UB。 @SergeBallesta 如果您从 va 构建 a2 并从 a 构建,那么它是从 T* 到 void* 到 T*。看不到 UB - 并且 a2 根本不需要等于 vc。哦等等,你正在从vc 投射到a2...不,那是UB。【参考方案2】:
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&lt;A*&gt;(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&lt;C**&gt;(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*

【讨论】:

以上是关于通过 void 指针通过 C ABI 传递 C++ 对象(可能具有多重虚拟继承)的主要内容,如果未能解决你的问题,请参考以下文章

将c ++引用的地址传递给指针

C++ 通过引用传递向量字符指针

C ++:将成员函数作为普通函数指针传递的闭包

为 C++ 库创建一个 c 包装器

通过 C++ 中的指针将子对象值传递给被调用者类

如何在 C++ 中通过指针传递字符串?