带有堆栈分配的C ++类的C包装器

时间:2016-02-17 00:46:22

标签: c++ c api shared-libraries

假设我们有一个类似这样的类的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();
}

很难将类的真实内容放在struct的字段中。我们不能只包含C ++标头,因为C不理解它,所以它需要我们编写兼容的C标头。这并不总是可行的。我认为C库并不需要关心结构的内容。

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

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

问题:

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

7 个答案:

答案 0 :(得分:11)

您可以结合placement new使用alloca在堆栈上创建对象。对于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));;

答案 1 :(得分: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应该生成buff,然后从中创建一个句柄并使用它进行交互。现在通常不需要重新解释指向theclassbuff的指针,但我认为这在技术上是未定义的行为。

答案 2 :(得分:6)

这是另一种方法,根据具体应用的具体情况,可能接受也可能不接受。这里我们基本上从C代码中隐藏了TheClass实例的存在,并在包装​​函数中封装了TheClass的每个使用场景。如果此类场景的数量太大,这将变得无法管理,但在其他方面可能是一种选择。

C包装器:

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

从C中简单地调用包装器。

2016年2月17日更新:

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

我想说具有堆栈分配的TheClass实例的值在这里是有问题的,并且它是一个判断调用,具体取决于应用程序细节,例如性能。您仍然必须调用de-allocation函数,该函数又手动调用析构函数,因为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&#39; d作为参数类型 包装器函数而不是TheClass。我们会reinterpret_cast 它在包装器代码中,实际上会使代码更清晰:
    指向C&#39; TheClass的指针基本上被重新解释为C ++的TheClass

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

答案 3 :(得分:3)

值得将每一段知识保存在一个地方,所以我建议制作一个类代码&#34;部分可读&#34;对于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。但是,如果您不想使用它,可以使用许多固定大小的模板。

答案 4 :(得分: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);

答案 5 :(得分: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的选项:placement new。所以你要在create_class_instance()中传递额外的参数,该参数指向一个足以存储类实例的分配缓冲区,但是在堆栈上。

答案 6 :(得分: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);
}