包装 C++ 类 API 以供 C 消费

问题描述 投票:0回答:7

我有一组相关的 C++ 类,必须将它们包装并从 DLL 导出,以便 C/FFI 库可以轻松使用它。我正在寻找一些“最佳实践”来做到这一点。例如,如何创建和释放对象、如何处理基类、替代解决方案等...

到目前为止,我的一些基本准则是将方法转换为简单函数,并带有表示“this”指针的额外 void* 参数,包括任何析构函数。构造函数可以保留其原始参数列表,但必须返回表示对象的指针。所有内存都应该通过同一组进程范围的分配和空闲例程进行处理,并且在某种意义上应该是可热插拔的,无论是通过宏还是其他方式。

c++ c api class word-wrap
7个回答
37
投票

对于每个公共方法,您需要一个 C 函数。
您还需要一个不透明的指针来表示 C 代码中的类。
尽管您可以构建一个包含 void* 和其他信息的结构(例如,如果您想支持数组?),但仅使用 void* 会更简单。

Fred.h
--------------------------------

#ifdef  __cplusplus
class Fred
{
    public:
    Fred(int x,int y);
    int doStuff(int p);
};
#endif

//
// C Interface.
typedef void*   CFred;

//
// Need an explicit constructor and destructor.
extern "C" CFred  newCFred(int x,int y);
extern "C" void   delCFred(CFred);

//
// Each public method. Takes an opaque reference to the object
// that was returned from the above constructor plus the methods parameters.
extern "C" int    doStuffCFred(CFred,int p);

实施很简单。
将不透明指针转换为 Fred,然后调用该方法。

CFred.cpp
--------------------------------

// Functions implemented in a cpp file.
// But note that they were declared above as extern "C" this gives them
// C linkage and thus are available from a C lib.
CFred newCFred(int x,int y)
{
    return reinterpret_cast<void*>(new Fred(x,y));
}

void delCFred(CFred fred)
{
    delete reinterpret_cast<Fred*>(fred);
}

int doStuffCFred(CFred fred,int p)
{
    return reinterpret_cast<Fred*>(fred)->doStuff(p);
}

20
投票

虽然 Loki Astari 的回答非常好,但他的示例代码将包装代码放在 C++ 类中。我更喜欢将包装代码放在单独的文件中。另外,我认为用类名作为包装 C 函数的前缀是更好的风格。

以下博客文章展示了如何做到这一点: http://blog.eikke.com/index.php/ikke/2005/11/03/using_c_classes_in_c.html

我复制了重要部分,因为该博客已被放弃并可能最终消失(归功于 Ikke 的博客):


首先我们需要一个 C++ 类,使用一个头文件 (Test.hh)

class Test {
    public:
        void testfunc();
        Test(int i);

    private:
        int testint;
};

和一个实现文件(Test.cc)

#include <iostream>
#include "Test.hh"

using namespace std;

Test::Test(int i) {
    this->testint = i;
}

void Test::testfunc() {
    cout << "test " << this->testint << endl;
}

这只是基本的 C++ 代码。

然后我们需要一些粘合代码。这段代码介于 C 和 C++ 之间。同样,我们得到了一个头文件(TestWrapper.h,只是 .h,因为它不包含任何 C++ 代码)

typedef void CTest;

#ifdef __cplusplus
extern "C" {
#endif

CTest * test_new(int i);
void test_testfunc(const CTest *t);
void test_delete(CTest *t);
#ifdef __cplusplus
}
#endif

以及函数实现(TestWrapper.cc、.cc,因为它包含 C++ 代码):

#include "TestWrapper.h"
#include "Test.hh"

extern "C" {

    CTest * test_new(int i) {
        Test *t = new Test(i);

        return (CTest *)t;
    }

    void test_testfunc(const CTest *test) {
        Test *t = (Test *)test;
        t->testfunc();
    }

    void test_delete(CTest *test) {
        Test *t = (Test *)test;

        delete t;
    }
}

6
投票

我的一些经验意见:

  • 函数应该返回代表错误的代码。有一个以字符串形式返回错误描述的函数很有用。所有其他返回值都应该是输出参数。

例如:

C_ERROR BuildWidget(HUI ui, HWIDGET* pWidget);
  • 将签名放入句柄指针指向的结构/类中,以检查句柄的有效性。

例如你的函数应该是这样的:

C_ERROR BuildWidget(HUI ui, HWIDGET* pWidget){
    Ui* ui = (Ui*)ui;
    if(ui.Signature != 1234)
    return BAD_HUI;
}
  • 应使用从 DLL 导出的函数创建和释放对象,因为 DLL 和消费应用程序中的内存分配方法可能不同。

例如:

C_ERROR CreateUi(HUI* ui);
C_ERROR CloseUi(HUI hui); // usually error codes don't matter here, so may use void
  • 如果您要为某些缓冲区或其他可能需要在库外部保留的数据分配内存,请提供此缓冲区/数据的大小。这样用户就可以将其保存到磁盘、数据库或任何他们想要的地方,而无需侵入你的内部来找出实际大小。否则,您最终需要提供自己的文件 I/O api,用户仅使用它来将数据转换为已知大小的字节数组。

例如:

C_ERROR CreateBitmap(HUI* ui, SIZE size, char** pBmpBuffer, int* pSize);
  • 如果您的对象在 C++ 库之外有一些典型的表示形式,请提供一种转换为该表示形式的方法(例如,如果您有某个类
    Image
    并通过
    HIMG
    句柄提供对它的访问,请提供将其转换为和的函数来自例如 Windows HBITMAP)。这将简化与现有 API 的集成。

例如

C_ERROR BitmapToHBITMAP(HUI* ui, char* bmpBuffer, int size, HBITMAP* phBmp);

5
投票

首先,您可能不需要将所有方法转换为 C 函数。如果您可以简化 API 并隐藏一些 C++ 接口,那就更好了,因为当您更改后面的 C++ 逻辑时,您可以最大限度地减少更改 C API 的机会。

因此,请考虑通过该 API 提供更高级别的抽象。使用您描述的 void* 解决方案。在我看来,这是最合适的(或 typedef void* 作为 HANDLE :) )。


2
投票

使用向量(和 string::c_str)与非 C++ API 交换数据。 (指南 #78,来自 C++ 编码标准,H. Sutter/A. Alexandrescu)。

PS “构造函数可以保留其原始参数列表”并不正确。这仅适用于与 C 兼容的参数类型。

PS2 当然,听Cătălin 并让你的界面尽可能小和简单。


1
投票

0
投票

我自己还没有尝试过,但是CPPBind 现在已经存在并且有一个 C 后端。

© www.soinside.com 2019 - 2024. All rights reserved.