C ++存在许多可移植性问题,这仅是由于缺乏二进制级别的标准化。
我不认为这是如此简单。提供的答案已经为缺乏对标准化的关注提供了很好的理由,但是C ++可能太丰富了的语言,因此不适合与C作为ABI标准真正竞争。
我们可以进行名称重整,这是由于函数重载,vtable不兼容性,跨越模块边界抛出异常的不兼容性等导致的。所有这些都是一个真正的难题,我希望它们至少可以使vtable布局标准化。
但是,ABI标准不仅仅在于使在一个编译器中生成的C ++ dylib能够被另一个由不同编译器构建的二进制文件使用。使用ABI跨语言。如果它们至少能涵盖第一部分,那就太好了,但是我看不到C ++在通用的ABI级别上能够真正与C竞争,因此对于制作兼容性最强的dylib至关重要。
想象一下这样导出的一对简单函数:
void f(Foo foo);
void f(Bar bar, int val);
...然后想像Foo
和Bar
为班级带参数构造,拷贝构造函数,移动构造函数和非平凡的析构函数。
然后以Python / Lua / C#/ Java / Haskell / etc的场景为例。开发人员尝试导入此模块并以其语言使用它。
首先,我们需要一个名称处理标准,以了解如何利用函数重载来导出符号。这是一个简单的部分。但是,它实际上不应该被称为“ mangling”。由于dylib的用户必须按名称查找符号,因此此处的重载应导致名称看起来不完整。也许符号名称可能是类似的名称"f_Foo"
"f_Bar_int"
。我们必须确保它们不会与开发人员实际定义的名称冲突,也许会保留一些符号/字符/约定以供ABI使用。
但是现在情况更加艰难。例如,Python开发人员如何调用移动构造函数,复制构造函数和析构函数?也许我们可以将其作为dylib的一部分导出。但是如果Foo
和Bar
导出到不同的模块怎么办?我们是否应该复制此dylib中关联的符号和实现?我建议我们这样做,因为它可能很快变得非常烦人,否则开始不得不在多个dylib接口中纠缠在一起,只是为了在此处创建对象,在此处传递,在此处复制,然后在此处销毁。尽管相同的基本关注点在C中可能有所应用(只是更手动/显式地),但C倾向于仅出于人们对其编程的方式而避免这种情况。
这只是尴尬的一小部分。当f
上面的函数之一BazException
向JavaScript中抛出一个(也是一个带有构造函数和析构函数并派生std :: exception的C ++类)时,会发生什么?
充其量,我认为我们只能希望将一个ABI标准化,该ABI可以从一个C ++编译器生成的二进制文件转换为另一个C ++编译器生成的二进制文件。当然,那很好,但是我只是想指出这一点。通常,伴随着这样的担忧来分配一个可以跨编译器使用的通用库,通常也希望使其真正具有通用性和兼容性。
建议的解决方案
在努力寻找多年使用COM风格接口的API / ABI C ++接口方法后,我建议的解决方案是成为“ C / C ++”(双关语)开发人员。
使用C创建那些通用ABI,并使用C ++来实现。我们仍然可以做诸如导出函数之类的事情,这些函数使用显式函数将指针返回到不透明的C ++类,以在堆上创建和销毁此类对象。即使我们完全使用C ++来实现,也要尝试从ABI角度爱上C语言的美感。可以使用函数指针表对抽象接口进行建模。将这些内容包装到C API中很繁琐,但是随之而来的发行版本的好处和兼容性将使其变得非常有价值。
然后,如果我们不喜欢直接使用此接口(至少出于RAII原因,我们可能不应该使用该接口),则可以将所有需要的东西包装在随SDK一起提供的静态链接C ++库中。C ++客户端可以使用它。
Python客户端不希望直接使用C或C ++接口,因为无法制作这些pythonique。他们希望将其包装到自己的pythonique接口中,因此,实际上,我们只是导出最低限度的C API / ABI以使其尽可能简单,这实际上是一件好事。
我认为,更多的C ++行业将从中受益,而不是顽固地交付COM样式的接口等等。这也将使我们的生活更加轻松,因为使用这些dylib的用户不必大惊小怪地使用ABI。C使它变得简单,并且从ABI的角度来看,它的简单性使我们能够创建自然而简单地针对所有FFI运作的API / ABI。