Подтвердить что ты не робот

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-библиотекам действительно не нужно заботиться о содержании структур.

Использование этой оболочки будет выглядеть следующим образом:

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

Вопросы:

  • Есть ли у этого подхода какие-либо опасности или недостатки?
  • Есть ли альтернативный подход?
  • Существуют ли существующие оболочки, которые используют этот подход?
4b9b3361

Ответ 1

Вы можете использовать размещение нового в комбинации 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));;

Ответ 2

Есть опасность выравнивания. Но, возможно, не на вашей платформе. Для этого может потребоваться специальный код платформы или C/С++, который не стандартизован.

Дизайн мудрый, имеет два типа. В C это struct TheClass;. В С++ 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 будет работать, но я думаю, что это undefined поведение технически.

Ответ 3

Вот еще один подход, который может быть или не быть приемлемым, в зависимости от специфики приложения. Здесь мы в основном скрываем существование экземпляра TheClass из кода C и инкапсулируем каждый сценарий использования TheClass в функцию обертки. Это станет неуправляемым, если число таких сценариев слишком велико, но в противном случае это может быть вариант.

Обертка C:

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

Обертка тривиально вызывается из C.

Обновление 2/17/2016:

Поскольку вам нужно решение с объектом TheClass с состоянием, вы можете следовать основной идее вашего первоначального подхода, который был дополнительно улучшен в другом ответе. Вот еще один поворот в этом подходе, где размер заполнитель памяти, предоставленный кодом C, проверяется, чтобы убедиться, что он достаточно велик, чтобы содержать экземпляр TheClass.

Я бы сказал, что значение наличия экземпляра TheClass, распределенного по стеку, является сомнительным здесь, и это вызов решения в зависимости от специфики приложения, например. представление. Вам все еще нужно вызвать функцию де-распределения, которая, в свою очередь, вызывает деструктор вручную, поскольку возможно, что TheClass выделяет ресурсы, которые необходимо освободить.

Однако, если иметь выделенную стекю TheClass, это еще один эскиз.

Код С++, который будет обернут вместе с оберткой:

#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 - это просто размер буфера памяти, в котором должен быть выделен экземпляр С++ TheClass; мы в порядке, пока размер буфера достаточен для хранения С++ TheClass

  • Так как TheClass в C является просто заполнителем памяти, мы можем так же используйте void *, возможно typedef 'd, как тип параметра в вместо TheClass. Мы бы reinterpret_cast это в коде оболочки, что на самом деле сделает код более понятным:
    указатели на C TheClass, по существу, интерпретируются как С++ TheClass.

  • Ничто не мешает коду C передать TheClass* в которые на самом деле не указывают на С++ TheClass пример. Один из способов решения этой проблемы - правильно хранить указатели инициализированные экземпляры С++ TheClass в некотором виде структуры данных в коде С++ и вернуться к кодам кода C, которые могут быть использованы для просмотрите эти экземпляры.
  • Чтобы использовать cout в оболочке С++, нам нужно связать стандартную библиотеку С++ при создании исполняемого файла. Например, если код C скомпилирован в main.o и С++ в lib.o, затем на Linux или Mac, мы сделали бы gcc -o junk main.o lib.o -lstdc++.

Ответ 4

Стоит сохранить каждую часть знаний в одном месте, поэтому я бы предложил сделать код класса "частично читаемым" для 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 и С++)

/* 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

Вот реализация класса в С++:

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

Обратите внимание, что используется вариационный шаблон, и для этого требуется С++ 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: размещение new. Поэтому вы должны передать дополнительный параметр в create_class_instance(), который указывает на выделенный буфер, достаточный для хранения экземпляра класса, но в стеке.

Ответ 7

Вот как я бы решил проблему (основная идея - позволить интерпретировать 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 были общедоступны в С++. Я догнал вариант массива 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);
}