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

Pav*_*hov 26 c c++ api shared-libraries

假设我们有一个像这样的类的C++库:

class TheClass {
public:
  TheClass() { ... }
  void magic() { ... }
private:
  int x;
}
Run Code Online (Sandbox Code Playgroud)

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

TheClass object;
object.magic();
Run Code Online (Sandbox Code Playgroud)

我们需要为这个类创建一个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();
}
Run Code Online (Sandbox Code Playgroud)

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

我正在寻找一种允许从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();
}
Run Code Online (Sandbox Code Playgroud)

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

这个包装器的用法如下所示:

TheClassW object;
create_the_class(&object);
the_class_magic(&object);
Run Code Online (Sandbox Code Playgroud)

问题:

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

Ale*_*lex 12

您可以在alloca的组合中使用placement new来在堆栈上创建对象.对于Windows,有_malloca.这里的重要性是alloca和malloca相应地对齐内存,并且包装操作符可以轻松地暴露类的大小.请注意,在C代码中,当变量超出范围时,没有任何反应.特别是不要毁坏你的物体.sizeof

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);
}
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑:

您可以创建包装器宏以避免创建和初始化分离.你不能使用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);
Run Code Online (Sandbox Code Playgroud)

这将gcc扩展为:

void *me = __builtin_alloca (sizeof_the_class()); if (me == __null) goto fail; create_the_class(me, (20));;
Run Code Online (Sandbox Code Playgroud)


Yak*_*ont 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();
}
Run Code Online (Sandbox Code Playgroud)

C应该生成buff,然后从中创建一个句柄并使用它进行交互.现在通常这不是必需的,因为重新解释指向theclassbuff的指针将起作用,但我认为这是技术上未定义的行为.

  • @PavelStrakhov因为如果你需要4字节对齐并且你的大小是4字节,那么大小(4 + 4-1)= 7的缓冲就足够了.所需的偏移量可以是0,1,2或3.大小为4的偏移量意味着0也可以.最大偏移量3(等于-1的对齐)加上大小,是保证`std :: align`可以从未对齐的源缓冲区产生该对齐数据所需的缓冲区大小.顺便说一句,这意味着对齐为1的数据不需要额外的空间,正如人们所预料的那样. (2认同)

Ana*_*i P 6

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

C包装器:

extern "C" void do_magic()
{
  TheClass object;
  object.magic();
}
Run Code Online (Sandbox Code Playgroud)

从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
}
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

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

一些额外的说明:

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

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

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