пятница, 2 августа 2013 г.

Использование в коде C библиотек, написанных на C++

На C++ можно писать библиотеки, которые можно было бы использовать в др. языках программирования, в т.ч. не являющихся объектно ориентированными. В частности, меня интересовал вопрос о написании на C++ таких библиотек, которыми можно было бы воспользоваться в C. Я в курсе о том, что следует использовать extern "C", однако этим дело не заканчивается, поскольку по ходу реализации такой библиотеки выявляется большое количество подводных камней, которые следует как-то обходить.


В качестве эксперимента, сначала я написал на C++ простенький проект библиотеки (DLL), в составе которой разместил пользовательское пространство имён, а в нём - пару классов, вложенный enum и т.п. Затем, опять же на C++, я написал простое приложение (EXE), использующее эту библиотеку. Пояснения по обоим проектам и их исходный код - здесь.

Следующим шагом было внесение в код моей библиотеки таких изменений, чтобы ею можно было воспользоваться в C89. Порывшись в google я находил множество однотипных простых примеров, однако они не давали ответа на все вопросы, которые у меня возникали в процессе решения данной задачи. В виду этого я создал тему на stackoverflow.com и, к счастью, достаточно быстро получил подробный ответ, в котором рассматривались многие из интересующих меня вопросов. Я весьма благодарен пользователю celtschk, потратившему своё время на столь подробный ответ. Поскольку ответ был на английском, то я решил перевести его на русский и разместить у себя в блоге, указав при этом ссылку на оригинал.

Итак, мой вопрос:
Что мне нужно сделать, чтобы в приложениях, написанных на C, можно было использовать мои DLL, написанные на C++? Дело в том, что в составе библиотеки могут использоваться средства, отсутствующие в C: пространства имён, классы, вложенные перечисления, шаблоны и т.п.

Ответ:
Решением этого вопроса является написание C интерфейса для вашей DLL в C++ (либо в виде части этой DLL, либо как отдельную DLL), и затем использовать этот интерфейс из вашего C кода.

Убедитесь, что ваш C интерфейс состоит только из фукций, каждая из которых является extern "C". Далее перечислены проблемы и способы их решения:

Ваш класс размещён не в глобальном пространстве имён

Обычно, вы можете экспортировать C++ классы в C путём обыкновенного объявления непрозрачного struct ClassName*. Как бы то ни было, это не возможно для классов, размещённых в пространствах имён, отличных от глобального, т.к. C не знаком с концепцией пространств имён. Существует несколько путей для решения этого:

Простой путь: создать C интерфейс, принимающий и возвращающий указатели типа void*, маскирующих определения фактических типов.

Преимущества: простая реализация. Всё что вам нужно - приведение типа для каждой точки входа.

Недостатки: C интерфейс не обеспечит безопасность типов. В такой интерфейс легко передать неправильный указатель и всё перепутать.

Напрашивающийся путь: В глобальной области видимости вашей C++ библиотеке определить базовые классы для каждого C++ класса, доступ к которому вы желаете предоставить и уже от них унаследовать фактические классы. Обратите внимание на то, что базовые классы глобального пространства имён могут быть пустыми, потому что вы будете выполнять static_cast для каждого фактического типа, прежде чем начнёте использовать его.

Преимущества: лёгок в реализации, обеспечивает безопасность типов.

Недостатки: придётся выполнить много работы, если у вас много классов. Кроме того, этот вариант может не подойти в том случае, если по каким-либо причинам вы не можете внести изменения в классы, функционал которых хотели бы использовать в C. Кроме того, такие базовые классы вы не сможете выделить так, чтобы их могли бы использовать только C клиенты. Каждый C++ клиент получит доступ к таким глобальным классам, даже если в библиотеку не включён C интерфейс.

Использование целочисленных дискрепторов (хэндлов): использование целочисленного дискрептора (хэндла), который фактически содержит указатель, приведённый к int. Если ваш компилятор поддерживает это, то используйте intptr_t из stdint.h, потому что это это гарантирует, что intptr_t будет достаточно большим, независимо от используемой вами платформы. В противном случае, самый лучший вариант - использовать long, дабы обезопасить себя.

Преимущества: способ лёгок в реализации, немного более типобезопасен, чем void* и более близок к интерфейсному стилю для любого C программиста, который когда-либо имел дело с низкоуровневыми функциями, подобными обработке файлов в ОС.

Недостатки: не слишком типобезопасен. Вы по прежнему можете передать в вашу функцию некорректный тип хэндла.

Использование "инкапсулированных" дискрепторов: для каждого типа, доступ к которому вы желаете предоставить из C, в глобальном пространстве имён можно определить структуры, содержащие только их члены или void*, или целочисленные дискрепторы (хэндлы).

Преимущества: относительно легко реализуемо (хотя не настолько легко, как для void* или для необработанного хэндла), типобезопасен (пока C код не смешивают с  элементом структуры).

Недостатки: возможно, что такой способ будет более тяжёлым для C компилятора, когда дело дойдёт до оптимизации кода... Однако, это сильно зависит от самого C компилятора.

Мой совет: идти по пути "инкапсулированных" дискрепторов, если у этого способа нет проблем с производительностью, в противном же случае - прибегнуть к варианту с использованием целочисленных дискрепторов (хэндлов).

Интерфейс класса принимает или возвращает std::string

В C коде нет способа работать с std::string и вы, несомненно, не желаете выполнять работу по созданию оболочки, которую смогли бы использовать в C (пользователи вашего C интерфейса так же не хотели бы этого от вас). Поэтому, в вашем C интерфейсе обязательно использовать char* или char const*.

Передача строки в вашу библиотеку

Передать строку в вашу библиотеку легко: вы просто передаёте char const* и позволяете конструктору std::string создавать результат.

Возвращение строки из вашей библиотеки

Это трудный случай. Вы не можете просто взять и вернуть результат, воспользовавшись для этого экземплярным методом c_str() класса std::string потому, что это оставит вас с "висячим" указателем, когда временный std::string будет разрушен. Т.о. вы, в основном, имеете следующие варианты:

- сохранять результат в static std::string (т.о. эта строка не будет уничтожена по выходу из функции), копирование результата вызова вашей функции в статическую переменную, и возврашение c_str()).

Преимущества: простота в реализации, гарантированное получение полной строки.

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

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

Преимущества: вы гарантированно возвращаете полную строку (предполагается, что операция выделения памяти прошла успешно).

Недостатки: хорошо знакомая проблема: освобождение памяти в другой DLL, а не в той, которая выделила эту память, происходит некорректно. Поэтому вы должны будете обеспечить интерфейс освобождения этой памяти и пользователь вашего C интерфейса должен будет не забывать использовать его, вместо того, чтобы просто освобождать память.

- позволить пользователю передать массив символов и его длину, с тем, чтобы воспользовавшись функцией strncpy вы смогли бы скопировать вашу строку в этот массив.

Преимущества: вы не управляете памятью, использующейся для хранения строки - это перекладывается на плечи пользователя. Кроме того, если пользователь захочет, строка может быть записана в указанное им место. Он может просто передать адрес в обёртку, вместо того, чтобы делать другую копию.

Недостатки: если массив, предоставленный пользователем, не будет достаточно длинным, то вы получите усечённую строку.

Мой совет:  учитывая проблему совместного распределения памяти различными DLL, я бы предложил использовать третий вариант и согласился бы на риск получения усечённой строки (однако убедитесь, что вы предусмотрели способ информирования пользователя о подобных ошибках).

Вы определили перечисление (enum) в рамках своего класса

Это просто: предоставьте идентичное (за исключением имени) определение enum в вашем C интерфейсе. Поскольку идентичные определения enum дают идентичные значения идентификаторов, просто выполняйте приведение значений из вашего enum, встроенного в класс, к глобальному перечислению и обратно - это должно работать великолепно. Проблема заключается только в том, что всякий раз, когда вы вносите изменения в enum, определённый в вашем классе, вам следует не забывать обновлять и enum, размещённый в глобальном пространстве имён.

Вы объявили public члены типа std::vector

Начнём с того, что такой подход в любом случае является плохим решением. Способ обработки такой ситуации является, в основном, тем же, поскольку вы фактически должны обработать эти векторы в C++, то предоставьте для них интерфейс итератора (iterator).

Сейчас итераторы C++ не работают как следует в C, однако в C и так нет никакого резона придерживаться итераторов в стиле C++.

Учитывайте, что члены интерфейса являются типом std::vector. Другой вариант состоял бы в том, чтобы передавать указатель на первый элемент вектора. Однако это лишится законной силы в следующий раз, когла ваш вектор будет изменён. Т.о. этот вариант для очень ограниченных случаев.

У вас имеются функции печати, которые принимают std::ostream в качестве одного из аргументов и ссылаются на объект

Конечно, std::ostream не доступен в C коде. В C доступ к файлам осуществляется посредством использования FILE*. Я думаю, что лучший вариант в данном случае будет использование класса iostream, который обёрнут вокруг FILE* и позволяет вам наследоваться от него. Я не знаю, имеется ли такая возможность у MSVC++, но если нет, то Boost имеет AFAIK как поток.

Ваша обёртка печати принимала бы FILE* и хэндл объекта(или void* или какой-нибудь др. вариант, выбранный вами для представления ваших объектов в C), создавала бы временный объект ostream на основе FILE*, извлекала бы указатель на объект из хэндла и затем вызывала бы print на указанном объекте.

Дополнительное рассмотрение: исключения (exceptions)

Не забывайте отлавливать исключения и превращать их в обрабатываемые ошибки C. Обычно лучшим вариантом является возвращение кода ошибки.

Пример заголовочного файла для C интерфейса моей DLL библиотеки, написанной на C++

Ниже приведён возможный интерфейс заголовочного файла для нашей библиотеки, используемой в качестве примера:

/* somelib_c_interface.h */
#ifndef SOMELIB_C_INTERFACE_H
#define SOMELIB_C_INTERFACE_H

#ifdef __cplusplus
extern "C" {
#endif

#include <stdint.h>
#include <stdlib.h>

/* typedef for C convenience */
typedef struct
{
  intptr_t handle;
} some_namespace_info;

typedef struct
{
  intptr_t handle;
} some_namespace_employee;

/* error codes for C interface */
typedef int errcode;
#define E_SOMELIB_OK 0              /* no error occurred */
#define E_SOMELIB_TRUNCATE -1       /* a string was created */
#define E_SOMELIB_OUT_OF_MEMORY -2  /* some operation was aborted due to lack of memory */
#define E_SOMELIB_UNKNOWN -100      /* an unknown error occurred */

/* construct an info object (new)
errcode some_namespace_info_new(some_namespace_info* info, char const* name, char const* number);

/* get rid of an info object (delete) */
errcode some_namespace_info_delete(some_namespace_info info);

/* Some_namespace::Info member functions */
errcode some_namespace_info_get_name(some_namespace_info info, char* buffer, size_t length);
errcode some_namespace_info_set_name(some_namespace_info info, char const* name);

errcode some_namespace_info_get_number(some_namespace_info info, char* buffer, size_t length);
errcode some_namespace_info_set_number(some_namespace_info info, char const* name);

/* the employee class */

/* replicated enum from employee */
enum some_namespace_employee_sex { male, female };

errcode some_namespace_employee_new(some_namespace_employee* employee,
                                    char const* name, char const* surname, int age,
                                    some_namespace_employee_sex sex);

errcode some_namespace_employee_delete(some_namespace_employee employee);

errcode some_namespace_employee_get_name(some_namespace_employee employee, char* buffer, size_t length);
errcode some_namespace_employee_set_name(some_namespace_employee employee, char const* name);

errcode some_namespace_employee_get_surname(some_namespace_employee employee, char* buffer, size_t length);
errcode some_namespace_employee_set_surname(some_namespace_employee employee, char const* name);

/* since ages cannot be negative, we can use an int return here
   and define negative results as error codes, positive results as valid ages
   (for consistency reason, it would probably be a better idea to not do this here,
   but I just want to show another option which sometimes is useful */
int some_namespace_employee_get_age(some_namespace_employee employee);

errcode some_namespace_employee_set_age(some_namespace_employee employee, int age);

errcode some_namespace_employee_get_set(some_namespace_employee employee,
                                        enum some_namespace_employee_sex* sex);
errcode some_namespace_employee_set_sex(some_namespace_employee employee,
                                        enum some_namespace_employee_sex sex);

typedef struct
{
  intptr_t handle;
} info_iter;

info_iter_delete(info_iter iterator);

some_namespace_info info_iter_get(info_iter iterator);
void info_iter_next(info_iter iterator);
bool info_iter_finished(info_iter iterator);

info_iter some_namespace_employee_phones(some_namespace_employee employee);
info_iter some_namespace_employee_emails(some_namespace_employee employee);
info_iter some_namespace_employee_sites(some_namespace_employee employee);

errcode some_namespace_print(FILE* dest, some_namespace_employee employee);

#ifdef __cplusplus
}
#endif

#endif // defined(SOMELIB_C_INTERFACE_H)

Вот как-то так...

Комментариев нет: