【问题标题】:C wrapper for C++ class with stack allocation具有堆栈分配的 C++ 类的 C 包装器
【发布时间】:2016-05-28 12:26:51
【问题描述】:

假设我们有一个 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() 中进行任何内存分配?还是班级本身的大小是您唯一担心的分配?
  • 如果类在内部使用堆分配,那完全没问题(反正什么也做不了)。我只需要对类本身进行堆栈分配。
  • 您可能希望将static_assert(sizeof(TheClassW) == sizeof(TheClass)) 放在包装源文件中。
  • 通常你还需要一个destruct_the_object(struct TheClassW* self) { reinterpret_cast&lt;TheClass*&gt;(self)-&gt;~TheClass(); }。加上一些处理异常的政策,例如见Code reuse in exception handling
  • 您可以使用 alloca() 在 C 包装器中的堆栈上分配空间,然后将 new() 用于 C++ 端的类以在那里构造对象。

标签: c++ c api shared-libraries


【解决方案1】:

您可以使用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));;

【讨论】:

  • IMO alloca 是最合理的解决方案。只需将其包装在一个宏中,以便create_class 进行分配和构造。
  • @sbabbi 实际上我曾考虑将其放入宏中但最终决定反对,因为我认为人们应该自行决定是否应将其包装在宏中。但我可以很容易地添加它。
【解决方案2】:

存在对齐危险。但也许不在你的平台上。解决此问题可能需要特定于平台的代码,或未标准化的 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,然后从它创建一个句柄并使用它进行交互。现在通常这不是必需的,因为重新解释指向 classbuff 的指针会起作用,但我认为从技术上讲这是未定义的行为。

【讨论】:

  • 如果我使用两种类型(struct TheClass 和 TheClassBuff),这有什么好处?哪些平台会导致对齐问题,可以采取哪些措施来防止这些问题?
  • @pavel 我不知道哪些平台会出现对齐问题?如果您的数据是大量过度对齐的字符,则使用 1 个字符的数组并在其中构造它不太可能起作用。至于这两种类型:通过 new 的返回值以外的任何方式访问在缓冲区中创建的对象是 UB,并且即使地址结果相同,也可能会触发严格的别名优化。所以最大的优势是一旦你做了 UB 就可以避免 UBL,代码必须在每次编译器升级和构建的每个平台上都经过验证,这很糟糕。
  • 嗯。标准对齐,加上使缓冲区为 sizeof + alignof -1,将是可移植的。请注意,这是您可能需要该指针的另一个原因。
  • 为什么缓冲区应该是“sizeof + alignof -1”而不是“sizeof + alignof”?
  • @PavelStrakhov 因为如果您需要 4 字节对齐并且您的大小为 4 字节,则大小为 (4+4-1)=7 的缓冲区就足够了。所需的偏移量可以是 0、1、2 或 3。大小为 4 的偏移量意味着 0 也可以。最大偏移量 3(等于 alignof -1)加上大小是保证std::align 可以从未对齐的源缓冲区生成该对齐数据所需的缓冲区大小。顺便说一句,这意味着对齐为 1 的数据不需要额外的空间,正如人们所期望的那样。
【解决方案3】:

这是另一种方法,可能会也可能不会接受,具体取决于应用程序的具体情况。这里我们基本上从 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'd,作为参数类型 包装函数而不是TheClass。我们会reinterpret_cast 它在包装代码中,这实际上会使代码更清晰:
    无论如何,指向 C 的 TheClass 的指针基本上都被重新解释为 C++ 的 TheClass

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

【讨论】:

  • 这只适用于无状态对象。例如,它不允许在 same 对象上使用 do_magic(); do_more_magic(); collect_applause(); 序列。
  • 这个序列是一个使用场景,必须用这种方法封装在一个单独的包装函数中。这就是缺点:我们拥有的场景越多,这种方法的吸引力就越小。
  • 我的目标是创建通用包装器,所以这种方法不能应用。
【解决方案4】:

将每条知识放在一个地方是值得的,所以我建议为 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。但是,如果您不想使用它,可以使用一些固定大小的模板来代替。

【讨论】:

    【解决方案5】:

    以下是安全便携的方法。

    // 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);
    

    【讨论】:

      【解决方案6】:

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

      【讨论】:

        【解决方案7】:

        这就是我解决问题的方法(基本思想是让 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 方法更具可读性。测试:
        ma​​in.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);
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2017-12-23
          • 2010-10-20
          • 1970-01-01
          • 2018-06-29
          • 2011-06-11
          • 2011-12-01
          • 2017-01-04
          相关资源
          最近更新 更多