8

环境是 Windows,MS Visual Studio 2012。如何在 C(不是 C++)应用程序中使用用 C++ 编写的 DLL 库?在我的 DLL 中,我使用了命名空间、类等。例如,我编写了两个简单的项目

  • DLL 项目
  • EXE 项目(使用 DLL)

两者都是用 C++ 编写的。谁能给我看一个用 C 语言编写的使用 C++ 库的类似应用程序吗?

谢谢你。

4

2 回答 2

19

对此的解决方案是在 C++ 中为您的 DLL 编写一个 C 接口(作为 DLL 的一部分,或作为单独的 DLL),然后在您的 C 代码中使用该 C 接口。

确保您的 C 接口包含所有extern "C".

编辑:正如 SigTerm 在评论中指出的那样,制作函数并不是一个好主意stdcall(默认情况下它们不是,但有些人从 WinAPI 函数中复制了它),因为这会在尝试使用其他语言的函数时造成麻烦。

以下是挑战,以及如何解决它们:

您的类在命名空间中

通常,您可以通过声明一个 opaque 来导出 C 中的 C++ 类struct ClassName*。然而,这对于命名空间中的类是不可能的,因为 C 不知道命名空间的概念。有几种方法可以解决它:

  • 简单的方法:只需让您的 C 接口分发并接受 void 指针,可能会被 typedefs 伪装成实际类型。

    优点:实现起来很简单。您只需要在每个入口点进行类型转换。

    缺点: C 接口不是类型安全的。很容易将错误的指针传递给您的界面并搞砸事情。

  • 侵入方式:在您的 C++ 库中,为您要公开的每个 C++ 类定义全局范围基类,并从中派生实际类。请注意,全局范围基类可能是空的,因为在使用它之前无论如何您都将static_cast使用实际类型。

    优点:易于实现,类型安全。

    缺点:如果你有很多课程,工作量会很大。此外,如果您无法更改您希望以这种方式公开的所有类,则可能是不可能的。此外,作为基类,您不能将它们分开,以便只有 C 客户端需要它们。每个 C++ 客户端都将获得全局类,即使它不包含 C 接口。

  • 使用句柄:使用整数句柄,它实际上包含转换为 int 的指针。如果您的编译器支持它,请使用intptr_tfrom,stdint.h因为无论您使用什么平台,它都保证足够大。long否则,为了安全起见,最好使用 a 。

    优点:易于实现,类型安全性略高于void*,对于任何曾经处理过 OS 文件处理等低级函数的 C 程序员来说,这是一种熟悉的界面风格。

    缺点:不是很类型安全;您仍然可以将错误类型的句柄传递给您的函数。

  • 使用“封装”句柄:对于要公开的每种类型,在全局范围内定义一个 C 结构,其中仅包含一个void*或整数句柄作为成员。

    优点:相对容易实现(虽然不像void*原始句柄那样容易),类型安全(只要 C 代码不与 struct 成员混淆)。

    缺点:可能使 C 编译器更难优化。但这在很大程度上取决于 C 编译器。

我的建议是使用封装的句柄,除非事实证明存在性能问题,在这种情况下我会使用整数句柄。

类接口接受并返回std::string

C 代码无法与std::string. 因此,必须在 C 接口中使用char*/ 。char const*

将字符串传递到您的库

将字符串传递给库的情况很简单:您只需传入 achar const*并让构造函数std::string完成其余的工作。

从库中返回字符串

这是复杂的情况。您不能只返回结果,因为当临时对象被破坏c_str时,它会给您留下一个悬空指针。std::string所以你基本上有以下选择:

  • 存储一个静态std::string变量(这样它就不会在返回时被释放),将函数调用的结果复制到其中,然后返回c_str)。

    优点:实现简单,保证给你完整的字符串。

    缺点:不是线程安全的,而且用户必须记住立即将内容复制到某个地方,否则下一次调用该函数将使数据无效,甚至更糟糕的是,指针。

  • 在包装函数中分配一个适当大小的字符数组,将字符串的内容复制到该数组中,并返回一个指向它的指针。

    优点:您可以保证返回完整的字符串(假设内存分配没有失败)。

    缺点:这是一个众所周知的问题,即在另一个 DLL 中释放内存而不是分配它的那个不能正常工作。因此,您必须提供一个释放接口,并且您的 C 接口的用户必须记住始终使用它,而不是简单地释放内存。

  • 让您的用户传递一个字符数组和一个长度,并使用strncpy将字符串数据复制到字符数组。

    优点:不必对字符串做内存管理;这是用户的责任。此外,如果用户希望将字符串写入特定位置,他可以简单地将地址传递给包装器,而不是制作另一个副本。

    缺点:如果用户提供的数组不够长,你会得到一个截断的字符串。

我的建议:考虑到跨 DLL 内存分配的问题,我会说使用第三种方式并接受截断字符串的风险(但请确保您有办法向用户报告错误)。

enum您的一个班级中有一名成员

这很简单:在您的 C 接口中提供相同的(名称除外)全局范围枚举定义。由于相同enum的定义为标识符提供了相同的值,因此只需将值从您的类内枚举转换为全局枚举并返回即可。唯一的问题是,每当您更新类内枚举时,您必须记住也要更新类外枚举。

你有 std::vector 类型的公共成员

好吧,首先,无论如何,这都是一个糟糕的接口决定。处理它的方法与您在 C++ 中实际处理这些向量的方法基本相同:为它们提供迭代器接口。

现在 C++ 迭代器在 C 中不能很好地工作,但是,在 C 中也没有理由坚持 C++ 迭代器风格。

鉴于接口成员是std::vector,另一种选择是给出指向向量第一个元素的指针。但是,下次调整矢量大小时,这些将失效,因此仅适用于非常有限的情况。

你有一个print函数,它接受std::ostream一个对象的一个​​和一个引用。

当然std::ostream不适用于 C 代码。在 C 中,文件是使用FILE*. 我认为这里最好的选择是使用一个 iostream 类,它围绕 aFILE*并允许您从一个构造。我不知道这是否由 MSVC++ 提供,但如果没有,Boost 有 AFAIK 这样的流。

然后,您的打印包装器将采用一个FILE*和一个对象句柄(或 void 指针,或您为在 C 中表示对象而选择的任何选项),从 FILE 指针构造一个临时 ostream 对象,从句柄中提取指向该对象的指针,然后然后调用print指向的对象。

其他注意事项:例外情况

不要忘记捕获异常并将其转化为 C 可以处理的错误。最好的选择通常是返回错误代码。

C 接口的示例标头

以下是链接示例库的可能 C 接口标头

/* somelib_c_interface.h */
#ifndef SOMELIB_C_INTERFACE_H
#define SOMELIB_C_INTERFACE_H

#ifdef __cplusplus
extern "C" {
#endif

#include <stdint.h>
#include <stdlib.h>

/* typedef for C convenience */
typedef struct
{
  intptr_t handle;
} some_namespace_info;

typedef struct
{
  intptr_t handle;
} some_namespace_employee;

/* error codes for C interface */
typedef int errcode;
#define E_SOMELIB_OK 0              /* no error occurred */
#define E_SOMELIB_TRUNCATE -1       /* a string was created */
#define E_SOMELIB_OUT_OF_MEMORY -2  /* some operation was aborted due to lack of memory */
#define E_SOMELIB_UNKNOWN -100      /* an unknown error occurred */

/* construct an info object (new)
errcode some_namespace_info_new(some_namespace_info* info, char const* name, char const* number);

/* get rid of an info object (delete) */
errcode some_namespace_info_delete(some_namespace_info info);

/* Some_namespace::Info member functions */
errcode some_namespace_info_get_name(some_namespace_info info, char* buffer, size_t length);
errcode some_namespace_info_set_name(some_namespace_info info, char const* name);

errcode some_namespace_info_get_number(some_namespace_info info, char* buffer, size_t length);
errcode some_namespace_info_set_number(some_namespace_info info, char const* name);

/* the employee class */

/* replicated enum from employee */
enum some_namespace_employee_sex { male, female };

errcode some_namespace_employee_new(some_namespace_employee* employee,
                                    char const* name, char const* surname, int age,
                                    some_namespace_employee_sex sex);

errcode some_namespace_employee_delete(some_namespace_employee employee);

errcode some_namespace_employee_get_name(some_namespace_employee employee, char* buffer, size_t length);
errcode some_namespace_employee_set_name(some_namespace_employee employee, char const* name);

errcode some_namespace_employee_get_surname(some_namespace_employee employee, char* buffer, size_t length);
errcode some_namespace_employee_set_surname(some_namespace_employee employee, char const* name);

/* since ages cannot be negative, we can use an int return here
   and define negative results as error codes, positive results as valid ages
   (for consistency reason, it would probably be a better idea to not do this here,
   but I just want to show another option which sometimes is useful */
int some_namespace_employee_get_age(some_namespace_employee employee);

errcode some_namespace_employee_set_age(some_namespace_employee employee, int age);

errcode some_namespace_employee_get_set(some_namespace_employee employee,
                                        enum some_namespace_employee_sex* sex);
errcode some_namespace_employee_set_sex(some_namespace_employee employee,
                                        enum some_namespace_employee_sex sex);

typedef struct
{
  intptr_t handle;
} info_iter;

info_iter_delete(info_iter iterator);

some_namespace_info info_iter_get(info_iter iterator);
void info_iter_next(info_iter iterator);
bool info_iter_finished(info_iter iterator);

info_iter some_namespace_employee_phones(some_namespace_employee employee);
info_iter some_namespace_employee_emails(some_namespace_employee employee);
info_iter some_namespace_employee_sites(some_namespace_employee employee);

errcode some_namespace_print(FILE* dest, some_namespace_employee employee);

#ifdef __cplusplus
}
#endif

#endif // defined(SOMELIB_C_INTERFACE_H)
于 2013-06-30T17:54:48.563 回答
3

您将必须创建可以将不透明指针传递给您的类实例的 C 包装函数。
这似乎是一个合理的例子: http ://www.daniweb.com/software-development/cpp/threads/355990/strategy-for-sumption-c-dlls-in-c

引用链接:

typedef Foo* FOO_HDL;
extern "C" { 
__declspec(dllexport) FOO_HDL FooCreate() {
  return new Foo();
};
__declspec(dllexport) void FooDestroy(FOO_HDL obj) {
  delete obj;
};
__declspec(dllexport) double FooGetValue(FOO_HDL obj) {
  return obj->GetValue();
};
__declspec(dllexport) void FooSetValue(FOO_HDL obj, double NewValue) {
  obj->SetValue(NewValue);
};
};
于 2013-06-30T17:54:37.700 回答