具有堆栈分配的 C++ class 的 C 包装器

C wrapper for C++ class with stack allocation

假设我们有一个带有 class 的 C++ 库,如下所示:

class TheClass {
public:
  TheClass() { ... }
  void magic() { ... }
private:
  int x;
}

此 class 的典型用法包括堆栈分配:

TheClass object;
object.magic();

我们需要为此创建一个 C 包装器 class。最常见的方法如下所示:

struct TheClassH;
extern "C" struct TheClassH* create_the_class() {
  return reinterpret_cast<struct TheClassH*>(new TheClass());
}
extern "C" void the_class_magic(struct TheClassH* self) {
  reinterpret_cast<TheClass*>(self)->magic();
}

但是,它需要堆分配,这对于这么小的 class 显然是不希望的。

我正在寻找一种允许从 C 代码中分配此 class 堆栈的方法。这是我能想到的:

struct TheClassW {
  char space[SIZEOF_THECLASS];
}
void create_the_class(struct TheClassW* self) {
  TheClass* cpp_self = reinterpret_cast<TheClass*>(self);
  new(cpp_self) TheClass();
}
void the_class_magic(struct TheClassW* self) {
  TheClass* cpp_self = reinterpret_cast<TheClass*>(self);
  cpp_self->magic();
}

很难将 class 的真实内容放入结构的字段中。我们不能只包含 C++ 头文件,因为 C 无法理解它,因此需要我们编写兼容的 C 头文件。这并不总是可能的。我认为 C 库并不真正需要关心结构的内容。

此包装器的用法如下所示:

TheClassW object;
create_the_class(&object);
the_class_magic(&object);

问题:

存在对齐危险。但也许不在您的平台上。修复此问题可能需要特定于平台的代码,或未标准化的 C/C++ 互操作。

设计明智,有两种类型。在 C 中,它是 struct TheClass;。在 C++ 中,struct TheClass 有一个主体。

制作struct TheClassBuff{char buff[SIZEOF_THECLASS];};

TheClass* create_the_class(struct TheClassBuff* self) {
  return new(self) TheClass();
}

void the_class_magic(struct TheClass* self) {
  self->magic();
}

void the_class_destroy(struct TheClass* self) {
  self->~TheClass();
}

C 应该制作 buff,然后从中创建句柄并使用它进行交互。现在通常不需要重新解释指向 theclassbuff 的指针,但我认为从技术上讲这是未定义的行为。

这是另一种方法,它可能会或可能不会被接受,具体取决于应用程序的具体情况。在这里,我们基本上从 C 代码中隐藏了 TheClass 实例的存在,并将 TheClass 的每个使用场景封装在一个包装函数中。如果此类场景的数量太多,这将变得难以管理,但否则可能是一种选择。

C 包装器:

extern "C" void do_magic()
{
  TheClass object;
  object.magic();
}

从 C 调用包装器。

2016 年 2 月 17 日更新:

既然你想要一个有状态 TheClass 对象的解决方案,你可以遵循你原来方法的基本思想,这在另一个答案中得到了进一步改进。这是该方法的另一个旋转,其中检查 C 代码提供的内存占位符的大小,以确保它足够大以容纳 TheClass 的实例。

我要说的是,拥有堆栈分配的 TheClass 实例的价值在这里值得怀疑,这是一个取决于应用程序细节的判断调用,例如表现。您仍然必须手动调用取消分配函数,该函数又会调用析构函数,因为 TheClass 可能分配了必须释放的资源。

但是,如果堆栈分配的 TheClass 很重要,这里是另一个草图。

要包装的 C++ 代码以及包装器:

#include <new>
#include <cstring>
#include <cstdio>

using namespace std;

class TheClass {
public:
  TheClass(int i) : x(i) { }
  // cout doesn't work, had to use puts()
  ~TheClass() { puts("Deleting TheClass!"); }
  int magic( const char * s, int i ) { return 123 * x + strlen(s) + i; }
private:
  int x;
};

extern "C" TheClass * create_the_class( TheClass * self, size_t len )
{
  // Ensure the memory buffer is large enough.
  if (len < sizeof(TheClass)) return NULL;
  return new(self) TheClass( 3 );
}

extern "C" int do_magic( TheClass * self, int l )
{
  return self->magic( "abc", l );
}

extern "C" void delete_the_class( TheClass * self )
{
  self->~TheClass();  // 'delete self;' won't work here
}

C代码:

#include <stdio.h>
#define THE_CLASS_SIZE 10

/*
   TheClass here is a different type than TheClass in the C++ code,
   so it can be called anything else.
*/
typedef struct TheClass { char buf[THE_CLASS_SIZE]; } TheClass;

int do_magic(TheClass *, int);
TheClass * create_the_class(TheClass *, size_t);
void delete_the_class(TheClass * );

int main()
{
  TheClass mem; /* Just a placeholder in memory for the C++ TheClass. */
  TheClass * c = create_the_class( &mem, sizeof(TheClass) );
  if (!c) /* Need to make sure the placeholder is large enough. */
  {
    puts("Failed to create TheClass, exiting.");
    return 1;
  }
  printf("The magic result is %d\n", do_magic( c, 232 ));
  delete_the_class( c );

  return 0;
}

这只是一个用于说明目的的人为示例。希望对您有所帮助。这种方法可能存在细微的问题,因此在您的特定平台上进行测试非常重要。

一些补充说明:

  • THE_CLASS_SIZE在C代码中就是一个内存缓冲区的大小,其中 将分配 C++ 的 TheClass 实例;我们很好,只要 缓冲区的大小足以容纳 C++ 的 TheClass

  • 因为TheClass在C中只是一个内存占位符,我们不妨 我们可以使用 void *,可能是 typedef' 作为参数类型 包装函数而不是 TheClass。我们会 reinterpret_cast 它在包装器代码中,这实际上会使代码更清晰:
    无论如何,指向 C 的 TheClass 的指针实际上被重新解释为 C++ 的 TheClass

  • 没有什么可以阻止 C 代码将 TheClass* 传递给 实际上并不指向 C++ 的包装函数 TheClass 实例。解决这个问题的一种方法是正确存储指针 在某种数据结构中初始化 C++ TheClass 个实例 在 C++ 代码中和 return 到可用于的 C 代码句柄 查找这些实例。
  • 要在 C++ 包装器中使用 couts,我们需要 link 构建可执行文件时的 C++ 标准库。例如,如果 C代码被编译成main.o,C++被编译成lib.o,然后 Linux 或 Mac 我们会 gcc -o junk main.o lib.o -lstdc++.

您可以使用 placement new in combination of alloca to create an object on the stack. For Windows there is _malloca。这里的重要性在于 alloca 和 malloca 相应地为您对齐内存并包装 sizeof 运算符可移植地公开您的 class 的大小。请注意,在 C 代码中,当您的变量超出范围时,不会发生任何事情。尤其不是破坏你的对象。

main.c

#include "the_class.h"
#include <alloca.h>

int main() {
    void *me = alloca(sizeof_the_class());

    create_the_class(me, 20);

    if (me == NULL) {
        return -1;
    }

    // be aware return early is dangerous do
    the_class_magic(me);
    int error = 0;
    if (error) {
        goto fail;
    }

    fail:
    destroy_the_class(me);
}

the_class.h

#ifndef THE_CLASS_H
#define THE_CLASS_H

#include <stddef.h>
#include <stdint.h>

#ifdef __cplusplus
    class TheClass {
    public:
        TheClass(int me) : me_(me) {}
        void magic();
        int me_;
    };

extern "C" {
#endif

size_t sizeof_the_class();
void *create_the_class(void* self, int arg);
void the_class_magic(void* self);
void destroy_the_class(void* self);

#ifdef __cplusplus
}
#endif //__cplusplus


#endif // THE_CLASS_H

the_class.cc

#include "the_class.h"

#include <iostream>
#include <new>

void TheClass::magic() {
    std::cout << me_ << std::endl;
}

extern "C" {
    size_t sizeof_the_class() {
        return sizeof(TheClass);
    }

    void* create_the_class(void* self, int arg) {
        TheClass* ptr = new(self) TheClass(arg);
        return ptr;
    }

    void the_class_magic(void* self) {
        TheClass *tc = reinterpret_cast<TheClass *>(self);
        tc->magic();
    }

    void destroy_the_class(void* self) {
        TheClass *tc = reinterpret_cast<TheClass *>(self);
        tc->~TheClass();
    }
}

编辑:

您可以创建一个包装宏来避免创建和初始化的分离。您不能使用 do { } while(0) 样式的宏,因为它会限制变量的范围。还有其他解决方法,但这在很大程度上取决于您如何处理代码库中的错误。概念证明如下:

#define CREATE_THE_CLASS(NAME, VAL, ERR) \
  void *NAME = alloca(sizeof_the_class()); \
  if (NAME == NULL) goto ERR; \

// example usage:
CREATE_THE_CLASS(me, 20, fail);

这在 gcc 中扩展为:

void *me = __builtin_alloca (sizeof_the_class()); if (me == __null) goto fail; create_the_class(me, (20));;

以下是安全、便携的方法。

// C++ code
extern "C" {
 typedef void callback(void* obj, void* cdata);

 void withObject(callback* cb, void* data) {
  TheClass theObject;
  cb(&theObject, data);
 }
}

// C code:

struct work { ... };
void myCb (void* object, void* data) {
   struct work* work = data;
   // do whatever 
}

// elsewhere
  struct work work;
  // initialize work
  withObject(myCb, &work);

将每条知识都放在一个地方是值得的,所以我建议为 C 编写一个 class 代码 "partially readable"。可以使用相当简单的一组宏定义来启用它要用简短和标准的词来完成。此外,宏可用于在堆栈分配对象生命周期的开始和结束时调用构造函数和析构函数。

比如说,我们首先将以下通用文件包含到 C 和 C++ 代码中:

#include <stddef.h>
#include <alloca.h>

#define METHOD_EXPORT(c,n) (*c##_##n)
#define CTOR_EXPORT(c) void (c##_construct)(c* thisPtr)
#define DTOR_EXPORT(c) void (c##_destruct)(c* thisPtr)

#ifdef __cplusplus
#define CL_STRUCT_EXPORT(c)
#define CL_METHOD_EXPORT(c,n) n
#define CL_CTOR_EXPORT(c) c()
#define CL_DTOR_EXPORT(c) ~c()
#define OPT_THIS
#else
#define CL_METHOD_EXPORT METHOD_EXPORT
#define CL_CTOR_EXPORT CTOR_EXPORT
#define CL_DTOR_EXPORT DTOR_EXPORT
#define OPT_THIS void* thisPtr,
#define CL_STRUCT_EXPORT(c) typedef struct c c;\
     size_t c##_sizeof();
#endif

/* To be put into a C++ implementation coce */
#define EXPORT_SIZEOF_IMPL(c) extern "C" size_t c##_sizeof() {return sizeof(c);}
#define CTOR_ALIAS_IMPL(c) extern "C" CTOR_EXPORT(c) {new(thisPtr) c();}
#define DTOR_ALIAS_IMPL(c) extern "C" DTOR_EXPORT(c) {thisPtr->~c();}
#define METHOD_ALIAS_IMPL(c,n,res_type,args) \
    res_type METHOD_EXPORT(c,n) args = \
        call_method(&c::n)

#ifdef __cplusplus
template<class T, class M, M m, typename R, typename... A> R call_method(
    T* currPtr, A... args)
{
    return (currPtr->*m)(args...);
}
#endif

#define OBJECT_SCOPE(t, v, body) {t* v = alloca(t##_sizeof()); t##_construct(v); body; t##_destruct(v);}

现在我们可以声明我们的 class(头文件在 C 和 C++ 中也很有用)

/* A class declaration example */
#ifdef __cplusplus
class myClass {
private:
    int y;
    public:
#endif
    /* Also visible in C */
    CL_STRUCT_EXPORT(myClass)
    void CL_METHOD_EXPORT(myClass,magic) (OPT_THIS int c);
    CL_CTOR_EXPORT(myClass);
    CL_DTOR_EXPORT(myClass);
    /* End of also visible in C */
#ifdef __cplusplus

};
#endif

这是 C++ 中的 class 实现:

myClass::myClass() {std::cout << "myClass constructed" << std::endl;}
CTOR_ALIAS_IMPL(myClass);
myClass::~myClass() {std::cout << "myClass destructed" << std::endl;}
DTOR_ALIAS_IMPL(myClass);
void myClass::magic(int n) {std::cout << "myClass::magic called with " << n << std::endl;}

typedef void (myClass::* myClass_magic_t) (int);
void (*myClass_magic) (myClass* ptr, int i) = 
    call_method<myClass,myClass_magic_t,&myClass::magic,void,int>;

这是一个使用 C 代码的示例

main () {
    OBJECT_SCOPE(myClass, v, {
        myClass_magic(v,178);
        })
}

它很短而且很管用! (这是输出)

myClass constructed
myClass::magic called with 178
myClass destructed

请注意,使用了可变参数模板,这需要 c++11。但是,如果您不想使用它,也可以使用一些固定大小的模板。

我在类似情况下所做的是这样的: (我省略static_cast, extern "C")

class.h:

class TheClass {
public:
  TheClass() { ... }
  void magic() { ... }
private:
  int x;
}

class.cpp

<actual implementation>

class_c_wrapper.h

void* create_class_instance(){
    TheClass instance = new TheClass();
}

void delete_class_instance(void* instance){
    delete (TheClass*)instance;
}

void magic(void* instance){
    ((TheClass*)instance).magic();
}

现在,您声明需要堆栈分配。为此,我可以建议 new 很少使用的选项:placement new。因此,您将在 create_class_instance() 中传递附加参数,该参数指向分配的缓冲区,足以存储 class 实例,但在堆栈上。

这就是我解决问题的方法(基本思想是让 C 和 C++ 解释相同的内存 名称不同):
TheClass.h:

#ifndef THECLASS_H_
#define THECLASS_H_

#include <stddef.h>

#define SIZEOF_THE_CLASS 4

#ifdef __cplusplus
class TheClass
{
public:
    TheClass();
    ~TheClass();
    void magic();

private:
    friend void createTheClass(TheClass* self);
    void* operator new(size_t, TheClass*) throw ();
    int x;
};

#else

typedef struct TheClass {char _[SIZEOF_THE_CLASS];} TheClass;

void create_the_class(struct TheClass* self);
void the_class_magic(struct TheClass* self);
void destroy_the_class(struct TheClass* self);

#endif

#endif /* THECLASS_H_ */

TheClass.cpp:

TheClass::TheClass()
    : x(0)
{
}

void* TheClass::operator new(size_t, TheClass* self) throw ()
{
    return self;
}

TheClass::~TheClass()
{
}

void TheClass::magic()
{
}

template < bool > struct CompileTimeCheck;
template < > struct CompileTimeCheck < true >
{
    typedef bool Result;
};
typedef CompileTimeCheck< SIZEOF_THE_CLASS == sizeof(TheClass) >::Result SizeCheck;
// or use static_assert, if available!

inline void createTheClass(TheClass* self)
{
    new (self) TheClass();
}

extern "C"
{

void create_the_class(TheClass* self)
{
    createTheClass(self);
}

void the_class_magic(TheClass* self)
{
    self->magic();
}

void destroy_the_class(TheClass* self)
{
    self->~TheClass();
}

}

createTheClass 函数仅用于友谊 - 我想避免 C 包装函数在 C++ 中公开可见。我了解了 TO 的数组变体,因为我认为这比 alloca 方法可读性更好。测试:
main.c:

#include "TheClass.h"

int main(int argc, char*argv[])
{
    struct TheClass c;
    create_the_class(&c);
    the_class_magic(&c);
    destroy_the_class(&c);
}