使用C封装C++类并进行堆栈分配的包装器

28

假设我们有一个 C++ 库,其中包含如下类:

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

该类的典型用法包括堆栈分配:

TheClass object;
object.magic();

我们需要为这个类创建一个C语言的包装器。最常用的方法看起来像这样:

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();
}

然而,这需要进行堆分配,对于如此小的类显然是不必要的。

我正在寻找一种方法,以便在C代码中允许对该类进行栈分配。以下是我想到的方法:

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();
}

将类的实际内容放入结构体字段中很困难。我们不能只包含C++头文件,因为C语言无法理解它,所以需要编写兼容的C头文件。而这并不总是可行的。我认为C库实际上不需要关心结构体的内容。

使用这个包装器的方法如下:

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

问题:

  • 这种方法是否存在任何危险或缺点?
  • 是否有替代方案?
  • 是否有使用这种方法的现有包装器?

magic() 函数中的类对象是否会进行任何内存分配?或者你只关心类本身的大小分配? - RyanP
如果类在内部使用堆分配,那很好(反正也没办法改)。我只需要类本身被分配在栈上。 - Pavel Strakhov
2
你可能想在包装器源文件中加入 static_assert(sizeof(TheClassW) == sizeof(TheClass)) - user253751
2
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - dxiv
3
您可以在 C 包装器中使用 alloca() 在堆栈上分配空间,然后在 C++ 端使用 placement new() 来构造对象。此举可以实现对象的构造并避免堆内存的使用。 - tofro
显示剩余3条评论
7个回答

14

您可以使用放置newalloca的组合在堆栈上创建对象。对于Windows,可以使用_malloca。这里的重要性在于,alloca和malloca会相应地对齐内存,并且包装sizeof运算符可以使您的类的大小在不同系统上移植。但请注意,在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));;

在我看来,alloca 是最合理的解决方案。只需将其包装在宏中,以便 create_class 同时执行分配和构造。 - sbabbi
@sbabbi实际上我考虑过将它放在一个宏中,但最终决定不这样做,因为我认为人们应该自己决定是否要将其包装在宏中。但我可以很容易地添加它。 - Alexander Oh

6

存在对齐危险。但或许不会出现在你的平台上。修复此问题可能需要平台特定代码或未标准化的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语言应该创建缓冲区,然后从中创建句柄并使用它进行交互。通常情况下,不需要这样做,因为重新解释指向类缓冲区的指针就可以工作,但我认为这在技术上是未定义行为。


如果我使用两种类型(struct TheClass和TheClassBuff),那会带来什么好处?哪些平台可能会导致对齐问题,如何防止它们发生? - Pavel Strakhov
@pavel 我不知道哪些平台可能存在对齐问题?如果你的数据是一个过度对齐的字符,使用一个1个字符的数组并在其中构造它不太可能起作用。至于这两种类型:除了通过new的返回值访问缓冲区中创建的对象之外,任何方式都是未定义行为,并且即使地址相同,也可能触发严格别名优化。因此,最大的优点是避免UBL,一旦出现UB,代码必须在每次编译器升级和每个平台上进行验证,这很糟糕。 - Yakk - Adam Nevraumont
1
嗯。使用Std align,再加上使缓冲区大小为sizeof + alignof -1,将是可移植的。请注意,这是您可能需要该指针的另一个原因。 - Yakk - Adam Nevraumont
为什么缓冲区应该是“sizeof + alignof -1”,而不是“sizeof + alignof”? - Pavel Strakhov
2
@PavelStrakhov 因为如果你需要4字节对齐,并且大小为4个字节,那么大小为(4 + 4-1)=7的缓冲区就足够了。所需的偏移量可以是0、1、2或3。大小为4的偏移量意味着0也可以。最大偏移量3(等于alignof-1)加上大小,即为所需缓冲区的大小,以保证std::align可以从非对齐源缓冲区生成具有该对齐方式的数据。顺便说一下,这意味着具有1对齐的数据不需要额外的空间,正如人们所期望的那样。 - Yakk - Adam Nevraumont
在x64/x86芯片中,这可能会导致问题,请尝试将您的C++结构体作为指针,并将您的C结构体设置为struct test{ char unused; TheClassBuff buff; },并使用任何pragma packing。这可能会导致buff对齐不正确,从而引起性能问题(在其他具有硬对齐要求的系统上,可能会触发陷阱)。 - Yakk - Adam Nevraumont

6

这里有另一种方法,可能是可以接受的,也可能不行,具体取决于应用程序。在这里,我们基本上隐藏了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;
}

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

几个额外的注释:

  • C代码中的THE_CLASS_SIZE只是用于分配C++的TheClass实例所需内存缓冲区的大小;只要缓冲区的大小足以容纳C++的TheClass, 我们就可以了。

  • 因为C中的TheClass只是内存占位符,我们可以使用void *作为包装函数中的参数类型,可能使用typedef进行定义。在包装代码中使用reinterpret_cast将其重新解释为C++的TheClass,这将使代码更清晰:指向C的TheClass的指针本质上已经被重新解释为C++的TheClass

  • 没有任何限制阻止C代码将不实际指向C++的TheClass实例的指针传递给包装函数。解决这个问题的一种方法是,在C++代码中存储指向正确初始化的C++ TheClass实例的指针,并返回可用于查找这些实例的句柄。
  • 要在C++包装器中使用cout,我们需要在构建可执行文件时链接C++标准库。例如,如果将C代码编译为main.o,将C++代码编译为lib.o,则在Linux或Mac上我们可以使用gcc -o junk main.o lib.o -lstdc++

这只适用于无状态对象。例如,它不允许在同一个对象上执行一系列 do_magic(); do_more_magic(); collect_applause(); 操作。 - dxiv
这个序列是一个使用场景,必须用单独的包装函数来封装它。这就是缺点:我们拥有的场景越多,这种方法就越不吸引人。 - Anatoli P
1
我的目标是创建通用使用的包装器,因此这种方法无法应用。 - Pavel Strakhov

3

值得将每一份知识都放在一个单独的位置,所以我建议为C语言编写一个“部分可读”的类代码。可以使用简单的宏定义集启用它以用短而标准的字完成。此外,也可以使用一个宏在栈分配对象生命周期开始和结束时调用构造函数和析构函数。

比如说,我们首先将以下通用文件包含到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);}

现在我们可以声明我们的类(头文件在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++中的类实现:

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。但是,如果您不想使用它,则可以使用一些固定大小的模板代替。

1
在类似的情况下,我所做的事情类似于: (我省略了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:放置新对象。因此,在create_class_instance()中传递附加参数,该参数指向已分配的缓冲区,足以存储类实例,但在堆栈上。

1
这是如何安全可移植地完成它的方法。
// 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);

0
这是我解决问题的方法(基本思路是让解释器以不同的方式解释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);
}

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