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);
Вопросы:
- Есть ли у этого подхода какие-либо опасности или недостатки?
- Есть ли альтернативный подход?
- Существуют ли существующие оболочки, которые используют этот подход?
Ответы
Ответ 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);
}