通过 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<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是正确的,为什么?任何一种方法都可以使用吗?
【问题讨论】:
【参考方案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<void *>(c)
;然后传递给您的AfromC
方法static_cast<A*>(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) 是非法的,因为 &
运算符需要一个左值。
将指向 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_casta2
和 c2
与 a
和 c
相同,因此它们应该是相同的 UB - 是或否,而不是中途。
@glenflet:标准中不能保证这一点。在使用虚表实现虚函数的实现中,对象的地址通常是虚表的地址。在这种情况下,static_cast 会进行偏移校正,但va
和vc
的实际值在这种情况下是不同的。只需尝试使用调试器以及具有虚函数的基类和子类
@dascandy: va
是通过从指向派生的指针的静态转换构建的。在常见的实现中,vc
和 c
将具有相同的表示和值,但如果 A 和 C 都具有虚函数,va
和 vc
将具有不同的值。作为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<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*
【讨论】:
以上是关于通过 void 指针通过 C ABI 传递 C++ 对象(可能具有多重虚拟继承)的主要内容,如果未能解决你的问题,请参考以下文章