用C语言编写的DLL与用C++编写的相同DLL之间的区别

12

我今天和一位同事讨论。他声称用C语言编写DLL可以让任何其他语言编写的应用程序使用该DLL。但是,如果该DLL是用C++编写的,则可使用该DLL的应用程序数量有限(可能受到语言约束的影响)。

  1. 他说得对吗?
  2. 如果您要编写一个应该由各种语言编写的所有类型的应用程序使用的DLL(但在同一平台上;让我们暂时忘记可移植性),您是否会使用C/C++,为什么?

我希望这个问题不是类似于大猩猩 vs. 鲨鱼的问题。如果是,请关闭它。


1. 他说的是正确的吗? 答:是的。通过使用C语言编写DLL,可以让任何其他语言编写的应用程序使用该DLL。而如果使用C++编写DLL,则可能会受到语言限制的影响,使可使用该DLL的应用程序数量有限。
2. 如果您要编写一个应该由各种语言编写的所有类型的应用程序使用的DLL(但在同一平台上;让我们暂时忘记可移植性),您是否会使用C/C++,为什么? 答:是的,我会使用C/C++来编写这样的DLL。因为C/C++是广泛使用的编程语言,在许多不同类型的应用程序中都可以使用。此外,C/C++在处理低级别的计算机任务方面非常出色,比其他高级语言更快且更稳定。

1
我的OneJoker库是用C编写的,并且可以从Linux和Windows上的C、C++、Java和Python进行调用。这相对而言比较简单,因为Java的JNI和Python的ctypes模块都是设计用于与C一起工作,而不是C ++。 C ++会引入很多依赖项,混淆外部函数名称等等。这只是我的经验。 - Lee Daniel Crocker
1
很确定如果需要导出C接口,你可以使用extern "C"。关于第二个问题,你需要查看COM对象(任何东西都可以使用它们,这就是重点)。 - Meirion Hughes
3个回答

16

大多数编程语言都提供了一种(简单)的方式来调用DLL中的C函数。但这不适用于C++,因为C++ ABI(C++函数的二进制接口)是特定供应商的。

除此之外,几乎不可能与使用模板或STL等高级C ++结构的C ++ DLL进行接口。

然而,你的DLL的内容可以用C++编写,你只需要确保你的接口符合C标准。为此,请勿在接口中使用C++结构,并在你的声明周围加上:

#ifdef __cpluscplus
extern "C" {
#endif

/* You declarations here */

#ifdef __cpluscplus
}
#endif

通过这种方式,你可以使用C接口来封装你的C++库。

编辑:正如Mats Petersson所写,不要忘记确保在你的包装器中处理每个可能的C++异常。


这个 C 包装器是否被认为是一般混乱的?因为除非底层的 C++ 代码非常复杂,使用 STL 和模板等,否则我觉得最好将代码重构为 C。我是对的吗? - Anish Ramaswamy
2
不,我不这么认为。你的底层代码可能使用了高级C++概念,实现了算法,并利用了高级C++库,但其接口可以非常简单,例如简单的启动/停止函数和以缓冲区作为参数并返回一些缓冲区的函数,数据封装在JSON、XML或protobuf数据块中。 - Matthieu Rouget
2
我认为这正是封装的作用。用简单的接口隐藏高级处理...但这取决于你的库。如果你的DLL需要提供实用函数,并且有一个非常丰富的API要导出到外部世界,你可能需要重新考虑以跨语言方式使用此DLL或尝试像SWIG(http://www.swig.org/)这样的库。 - Matthieu Rouget

7

1) 如果DLL提供的接口确实是C++接口,那么是的,它会使得其他语言与DLL进行接口交互变得困难(甚至不可能)。

C++的接口比C更加复杂,因为类的结构更加复杂(需要传递this指针,虚函数指针/VTABLE布局),并且异常处理也更加复杂(被调用的代码必须以某种方式处理异常的情况,并且为了做到这一点,代码需要能够“展开”抛出异常的代码的调用堆栈并销毁沿途创建的任何对象,直到找到一个catch - 如果在DLL中的调用堆栈中不存在,你将会遇到问题 - 这种展开不是C++标准的一部分,因为标准不希望限制处理器需要/应该实现C++的哪些体系结构和功能)。在DLL内部捕获异常可以解决这些问题。

换句话说,如果调用代码的语言不是C++ [可能是来自同一供应商],那么就需要对C++与调用它的任何语言进行处理。这可能会变得非常复杂。

任何C++对象都需要在调用代码的相关语言中进行翻译。对于与C通用的基本类型,这通常不是问题,但是类、结构等需要与本地语言中的兼容对象匹配。

另一方面:C函数非常容易进行接口交互:将参数放在堆栈上,调用函数,并在返回时清理参数。没有奇怪的事情发生,没有隐藏的函数参数,也没有必要展开堆栈。唯一的轻微复杂性是返回一个struct(大于某个大小)的函数 - 但这在C中是一个相当特殊的情况。 C的“对象”更简单,并且大多数语言都具有与基本C语言类型相对应的类型(但是C样式的struct仍然可能会导致一些有趣的问题,而union如果被“巧妙地”使用,则可能是一个真正的挑战)。

2) 选择语言是一个复杂的业务,它在很大程度上取决于DLL应该如何使用或提供什么样的接口,以及它本身应该连接到哪些接口 - 如果您的DLL与另一个C++ DLL(或其他C++代码)进行接口交互,则可能希望使用C++。但是,有一种方法可以生成具有C接口的C++ DLL,即使用extern "C"来定义接口函数(并确保没有将任何内容抛出到“C”之外,因为这肯定会引起问题)。

结论:
显然,将接口限制为“仅使用C++”会进一步复杂化问题,因为任何使用该库的人(例如使用C、Python或Lisp等可能可以轻松调用C函数的语言)都必须在C语言包装器中封装C++代码。是的,这可以做到,并且在某些非常好的库可用于C++时,有人想要连接到具有C样式接口的语言时,经常使用这种方法。这与“在DLL中提供C到C++接口”的解决方案基本相同,只是它不是由DLL的生产者提供的。

哇,我从没想过异常会带来这么多问题!如果异常在 DLL 函数内部被捕获呢?那么调用者就不需要做任何事情了,对吧? - Anish Ramaswamy
在我看来,这个答案有点误导人。堆栈展开是被调用函数的责任,而不是调用者的责任,因为调用者永远不会知道函数内部发生了什么。在跨语言传递对象时,无论如何都行不通,因为你必须在语言之间翻译对象。C处理较简单的对象,因此开销较小,但仍然必须完成。在C++中有一个“隐藏”的参数是调用约定的一部分。你必须知道这一点,就像你必须知道C中的名称前面加上“_”一样。 - Devolus
@Devolus:好的,我会稍微改一下措辞。 - Mats Petersson

0
每种编程语言都有其特点,例如调用约定、堆栈设置等等。无论何时您尝试处理跨语言边界的函数调用,都必须处理这些问题。编译器通常支持各种调用约定,因此您必须确保定义和编译是正确的,然后才能使用任何语言与其他模块通信。
由此得出的结论,某种语言比另一种更容易或更困难,是不正确的,因为您总是需要在某个时间点处理这些问题。
我会选择最适合我的任务的语言,而不是相反。当我编写GUI应用程序时,我通常使用Java,因为它让我专注于解决方案,而不是追踪内存。 :) 当我需要性能时,我使用C、C++甚至汇编语言,因此选择语言取决于我想要做什么或者我的环境如何。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接