Обтекание API класса C++ для потребления C

У меня есть набор связанных классов C++, которые должны быть упакованы и экспортированы из DLL таким образом, чтобы ее можно было легко использовать библиотеками C / FFI. Я ищу "лучшие практики" для этого. Например, как создавать и освобождать объекты, как обрабатывать базовые классы, альтернативные решения и т. Д.

Некоторые основные рекомендации, которые у меня есть, - это преобразовать методы в простые функции с дополнительным аргументом void *, представляющим указатель 'this', включая любые деструкторы. Конструкторы могут сохранять свой исходный список аргументов, но должны возвращать указатель, представляющий объект. Вся память должна обрабатываться с помощью одного и того же набора процессов и свободных подпрограмм, и в некотором смысле должна иметь возможность горячей замены, либо с помощью макросов, либо иным образом.

6 ответов

Решение

Для каждого публичного метода вам нужна функция C.
Вам также нужен непрозрачный указатель для представления вашего класса в коде C.
Проще просто использовать void*, хотя вы можете построить структуру, которая содержит void* и другую информацию (например, если вы хотите поддерживать массивы?).

Fred.h
--------------------------------

#ifdef  __cplusplus
class Fred
{
    public:
    Fred(int x,int y);
    int doStuff(int p);
};
#endif

//
// C Interface.
typedef void*   CFred;

//
// Need an explicit constructor and destructor.
extern "C" CFred  newCFred(int x,int y);
extern "C" void   delCFred(CFred);

//
// Each public method. Takes an opaque reference to the object
// that was returned from the above constructor plus the methods parameters.
extern "C" int    doStuffCFred(CFred,int p);

Реализация тривиальна.
Преобразуйте непрозрачный указатель в Fred и затем вызовите метод.

CFred.cpp
--------------------------------

// Functions implemented in a cpp file.
// But note that they were declared above as extern "C" this gives them
// C linkage and thus are available from a C lib.
CFred newCFred(int x,int y)
{
    return reinterpret_cast<void*>(new Fred(x,y));
}

void delCFred(CFred fred)
{
    delete reinterpret_cast<Fred*>(fred);
}

int doStuffCFred(CFred fred,int p)
{
    return reinterpret_cast<Fred*>(fred)->doStuff(p);
}

Хотя ответ Локи Астари очень хороший, его пример кода помещает код переноса в класс C++. Я предпочитаю иметь код упаковки в отдельном файле. Также я думаю, что лучше использовать префикс обёрточных функций C с именем класса.

Следующие сообщения в блоге показывают, как это сделать: http://blog.eikke.com/index.php/ikke/2005/11/03/using_c_classes_in_c.html

Я скопировал основную часть, потому что блог заброшен и может окончательно исчезнуть (ссылка на блог Ikke):


Сначала нам нужен класс C++, использующий один заголовочный файл (Test.hh)

class Test {
    public:
        void testfunc();
        Test(int i);

    private:
        int testint;
};

и один файл реализации (Test.cc)

#include <iostream>
#include "Test.hh"

using namespace std;

Test::Test(int i) {
    this->testint = i;
}

void Test::testfunc() {
    cout << "test " << this->testint << endl;
}

Это просто базовый код C++.

Тогда нам нужно немного кода клея. Этот код является чем-то средним между C и C++. Опять же, мы получили один заголовочный файл (TestWrapper.h, просто.h, так как он не содержит кода C++)

typedef void CTest;

#ifdef __cplusplus
extern "C" {
#endif

CTest * test_new(int i);
void test_testfunc(const CTest *t);
void test_delete(CTest *t);
#ifdef __cplusplus
}
#endif

и реализации функций (TestWrapper.cc, .cc, поскольку он содержит код C++):

#include "TestWrapper.h"
#include "Test.hh"

extern "C" {

    CTest * test_new(int i) {
        Test *t = new Test(i);

        return (CTest *)t;
    }

    void test_testfunc(const CTest *test) {
        Test *t = (Test *)test;
        t->testfunc();
    }

    void test_delete(CTest *test) {
        Test *t = (Test *)test;

        delete t;
    }
}

Некоторые мнения из моего опыта:

  • функции должны возвращать коды для представления ошибок. Полезно иметь функцию, возвращающую описание ошибки в виде строки. Все остальные возвращаемые значения должны быть вне параметров.

Например:

C_ERROR BuildWidget(HUI ui, HWIDGET* pWidget);
  • поместите подписи в структуры / классы, на которые указывают ваши маркеры, для проверки маркеров на валидность.

Например, ваша функция должна выглядеть так:

C_ERROR BuildWidget(HUI ui, HWIDGET* pWidget){
    Ui* ui = (Ui*)ui;
    if(ui.Signature != 1234)
    return BAD_HUI;
}
  • объекты должны создаваться и освобождаться с использованием функций, экспортируемых из DLL, так как метод выделения памяти в DLL и используемом приложении могут различаться.

Например:

C_ERROR CreateUi(HUI* ui);
C_ERROR CloseUi(HUI hui); // usually error codes don't matter here, so may use void
  • если вы выделяете память для некоторого буфера или других данных, которые могут потребоваться для сохранения за пределами вашей библиотеки, укажите размер этого буфера / данных. Таким образом, пользователи могут сохранить его на диск, в БД или где угодно, не взламывая ваши внутренние данные, чтобы узнать их реальный размер. В противном случае вам, в конечном итоге, потребуется предоставить собственный API-интерфейс ввода / вывода файлов, который пользователи будут использовать только для преобразования ваших данных в байтовый массив известного размера.

Например:

C_ERROR CreateBitmap(HUI* ui, SIZE size, char** pBmpBuffer, int* pSize);
  • если ваши объекты имеют какое-то типичное представление за пределами вашей библиотеки C++, укажите способ преобразования в это представление (например, если у вас есть какой-то класс Image и предоставить доступ к нему через HIMG обрабатывать, предоставлять функции для преобразования в и из окон, например, Windows HBITMAP). Это упростит интеграцию с существующим API.

Например

C_ERROR BitmapToHBITMAP(HUI* ui, char* bmpBuffer, int size, HBITMAP* phBmp);

Во-первых, вам может не понадобиться конвертировать все ваши методы в функции Си. Если вы можете упростить API и скрыть некоторые интерфейсы C++, это лучше, поскольку вы сводите к минимуму возможность изменить C API при изменении логики C++.

Так что подумайте о абстракции более высокого уровня, которая будет предоставлена ​​через этот API. Используйте то решение void*, которое вы описали. Это выглядит мне наиболее подходящим (или typedef void* как HANDLE:)).

Это может представлять интерес: "Смешивание C и C++" в C++ FAQ Lite. В частности [32.8] Как я могу передать объект класса C++ в / из функции C?

Используйте vector (и string::c_str) для обмена данными с не-C++ API. (Руководство № 78 из стандартов кодирования C++, H. Sutter / A. Alexandrescu).

PS Это не так, что "конструкторы могут сохранить свой первоначальный список аргументов". Это верно только для типов аргументов, которые являются C-совместимыми.

PS2 Конечно, слушайте Cătălin и держите ваш интерфейс как можно меньше и проще.

Другие вопросы по тегам