python

Введение в Beautiful Capi, инструмент создания С++ оберток для С++ библиотек

  • вторник, 24 октября 2017 г. в 03:13:02
https://habrahabr.ru/post/340770/
  • Компиляторы
  • Python
  • C++
  • C
  • API


Beautiful Capi — это инструмент облегчающий создание динамических библиотек на языке С++ с внешним интерфейсом на языке Си. Данный инструмент генерирует также С++ обертки для этого Си интерфейса. Beautiful Capi написан на языке Python 3.


Основная головная боль разработчиков библиотек на С++ в отсутствии единого стандарта ABI. Различные компиляторы имеют разный ABI, соглашения о именовании, схемах перехвата исключений и т.д. Поэтому программистам на С++ приходится каждый раз брать исходники библиотеки и собирать ее при помощи нужного компилятора.


Это хорошо, если библиотека популярная, и для нее добрый дядя уже выложил бинарные файлы для большинства компиляторов С++. Опять таки, для большинства компиляторов. Компиляторов С++ достаточно много, и, если учитывать разные версии одного и того же компилятора, имеющими несовместимый ABI, то вероятность того, что уже собранная библиотека вам не подойдет — достаточно высока. Плюс, добавим к этому различные настройки компиляторов, влияющие на двоичную совместимость.


Инструмент Beautiful Capi предлагает свой рецепт для этой проблемы. Динамическая библиотека должна снаружи иметь только Си интерфейс. В интерфейсе должны использоваться только те типы, размер которых строго фиксирован, например, int32_t, uint8_t и т.д. Также не забываем и про конвенцию вызова функций. Си интерфейс может использоваться и напрямую, если какой-то программист на Си решит использовать вашу библиотеку. Для использования библиотекой программистами на С++ существуют С++ обертки, которые Beautiful Capi генерирует автоматически. В планах генерация оберток и для других языков, таких как C# (.NET), Java и Python.


Инструмент Beautiful Capi не занимается парсингом исходных кодов на С++, как это делает аналогичная система Swig. Вы сами должны предоставить инструменту описание публичного API библиотеки. Публичное API библиотеки задается в формате XML и представляет из себя набор описаний С++ классов, методов, простых функций, энумераторов, сгруппированных по пространствам имен. Планируется добавить иные, чем XML, форматы описания публичного API.


Другой серьезной проблемой является менеджеры хипов (heap memory manager). Реализация такого менеджера, используемого в С++ библиотеке, может отличаться от реализации в клиентском приложении. Как результат, попытка освободить блок памяти, выделенный в С++ библиотеке, в клиентском приложении приведет к краху этого приложения. Инструмент гарантирует создание и удаление каждого экземпляра класса при помощи менеджера хипа С++ библиотеки.


Проект Beautiful Capi имеет открытый исходный код и лицензирован под лицензией GNU GPL. Однако, это не мешает использовать его в проприетарных проектах, потому что Beautiful Capi это, по-сути, внешний инструмент, какой как git или иной другой.


Привет, мир!


Рассмотрим пример первой компиляторо-независимой библиотеки на С++. В пространстве имен HelloWorld имеется класс PrinterImpl. Класс имеет единственный метод Show(), выполняющий заветные действия.


Собственно, сам класс PrinterImpl:


namespace HelloWorld
{
    class PrinterImpl
    {
    public:
        void Show() const
        {
            std::cout << "Hello Beautiful World!" << std::endl;
        }
    };
}

Для инструмента Beautiful Capi необходимо создать следующий XML файл:


<?xml version="1.0" encoding="utf-8" ?>
<hello_world:api xmlns:hello_world="http://gkmsoft.ru/beautifulcapi" project_name="HelloWorld">
  <namespace name="HelloWorld">
    <class name="Printer"
           implementation_class_name="HelloWorld::PrinterImpl"
           implementation_class_header="PrinterImpl.h"
           lifecycle="copy_semantic">
      <constructor name="Default"/>
      <method name="Show" const="true"/>
    </class>
  </namespace>
</hello_world:api>

Надеюсь, тут достаточно все очевидно. Несмотря на то, что наш реализационный класс называется PrinterImpl, мы решили, что его публичное имя будет называться просто Printer. Атрибуты implementation_class_name и implementation_class_header задают имя реализационного класса и имя заголовочного файла, в котором доступно его описание. Атрибут lifecycle задает сценарий жизненного цикла объектов. Скажу заранее, что в данный момент поддерживается три вида семантики жизненного цикла: copy_semantic, reference_counted и raw_pointer_semantic. Семантика копирования означает, что реализационный класс будет копироваться каждый раз, когда копируется соответствующий класс С++ обертки.


Покажем, какие Си функции будут сгенерированы. Сгенерированный при помощи инструмента Beautiful Capi файл AutoGenWrap.cpp необходимо включить в состав библиотеки:


void* hello_world_printer_default()
{
    return new HelloWorld::PrinterImpl();
}

void hello_world_printer_show_const(void* object_pointer)
{
    const HelloWorld::PrinterImpl* self = static_cast<HelloWorld::PrinterImpl*>(object_pointer);
    self->Show();
}

void* hello_world_printer_copy(void* object_pointer)
{
    return new HelloWorld::PrinterImpl(*static_cast<HelloWorld::PrinterImpl*>(object_pointer));
}

void hello_world_printer_delete(void* object_pointer)
{
    delete static_cast<HelloWorld::PrinterImpl*>(object_pointer);
}

Для целей наглядности опущены все конвенции вызова функций и ключевые слова extern "C". Как видим, неявный аргумент this является первым аргументом типа void*.


Функция hello_world_printer_default не имеет никаких аргументов, она создает в хипе (heap) реализационный объект и возвращает указатель на него как указатель на void. Функция hello_world_printer_show_const внутри себя просто вызывает метод Show(). Функция hello_world_printer_copy копирует реализационный объект и возвращает указатель на его копию. Функция hello_world_printer_delete удаляет реализационный объект.


Сгенерированная С++ обертка, файл Printer.h:


namespace HelloWorld
{
    class Printer
    {
    public:
        Printer()
        {
            SetObject(hello_world_printer_default());
        }
        void Show() const
        {
            hello_world_printer_show_const(GetRawPointer());
        }
        Printer(const Printer& other)
        {
            if (other.GetRawPointer())
            {
                SetObject(hello_world_printer_copy(other.GetRawPointer()));
            }
            else
            {
                SetObject(0);
            }
        }
        ~Printer()
        {
            if (GetRawPointer())
            {
                hello_world_printer_delete(GetRawPointer());
                SetObject(0);
            }
        }
        void* GetRawPointer() const
        {
            return mObject;
        }
    protected:
        void SetObject(void* object_pointer)
        {
            mObject = object_pointer;
        }
        void* mObject;
    };
}

Код на клиентской стороне:


#include "HelloWorld.h"

int main()
{
    HelloWorld::Printer printer;
    printer.Show();

    return EXIT_SUCCESS;
}

Результат работы программы:


Hello Beautiful World!

Истинная кросс-компиляторность


Стоит отметить одну особенность создания динамических библиотек при помощи компилятора Microsoft Visual C++ (и не только) на операционной системе Microsoft Windows. По умолчанию для любой динамической библиотеке some_name.dll создается статическая библиотека some_name.lib, которая уже линкуется к клиентам библиотеки. Но проблема в том, что существуют два несовместимых между собой формата .lib файлов, один от Microsoft, а другой от ныне почившей фирмы Borland. И если мы захотим использовать нашу динамическую библиотеку в клиенте, использующим, например, компилятор MinGW GCC, необходимо будет использовать стороннюю утилиту конвертирования .lib файлов, либо отказаться от использования .lib файлов. Благо, инструмент Beautiful Capi позволяет использовать динамический загрузчик, что позволяет полностью отказаться от статических библиотек:


#include <iostream>
#include <cstdlib>

#define HELLOWORLD_CAPI_USE_DYNAMIC_LOADER
#define HELLOWORLD_CAPI_DEFINE_FUNCTION_POINTERS
#include "HelloWorld.h"

int main()
{
    try
    {
#ifdef _WIN32
        HelloWorld::Initialization module_init("hello_world.dll");
#elif __APPLE__
        HelloWorld::Initialization module_init("libhello_world.dylib");
#else
        HelloWorld::Initialization module_init("libhello_world.so");
#endif

        HelloWorld::Printer printer;
        printer.Show();
    }
    catch (const std::exception& exception)
    {
        std::cout << "Exception: " << exception.what() << std::endl;
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

В случае, если динамическая библиотека hello_world.dll недоступна, то выбрасывается исключение типа std::runtime_error с описанием ошибки.


Мною был успешно протестирован данный пример при помощи компилятора Cygwin Clang, двоичный файл динамической библиотеки был создан с использованием компилятора Microsoft Visual C++ 2015.


Заключение


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


Также не был дан сравнительный анализ решения на базе Beautiful Capi и других инструментов, способных выполнить поставленную задачу, а именно, системы генерации врапперов Swig, технологии Microsoft COM и ее кроссплатформенных аналогов.


Однако эта статья дает общее представление об инструменте Beautiful Capi и решаемых им задач. Количество и качество отзывов побудят автора написать продолжение, либо дополнить данную статью.


Ссылки


  1. Инструмент Beautiful Capi
  2. Двоичный интерфейс приложений, ABI
  3. Свободный инструмент для связывания программ и библиотек Swig