Решил я запилить open source средство для автоматического враппинга С++ кода.
Компиляторов С\С++ много, у каждого свой ABI, соглашения о наименовании методов (name mangling) разные.
Даже размер std::string разный в Debug и Release.
Думаю, что получиться что-то вроде Swig в первом приближении, но не он.
Упор будет делаться на то, что генерируемые С-обвёртки будут читаемы для пользователей, а также то, что ими могут пользоваться С-программисты.
Ну и на стороне клиенты будет еще (опционально) генерироваться С++ header only style библиотека для возврата этого дела опять в С++.
Ну, и вообще, наблюдал несколько разных библиотек, имеющих С API, написанное в духе С++,
то есть имеется метод имя_класса_create() имя_класса_destoy(), имя_класса_имя_метода(),
this обычно пробрасывается как первый аргумент void* или HANDLE. Да тот же WinAPI
Другие примеры ZeroMQ, и C++ биндинг к ней.
То есть средство должно быть как можно легковесней, не быть очередным COM.
У Swig есть С-генератор, но он вроде заброшен.
Плюс Swig парсит C++\C заголовки, я же планирую облегчить себе жизнь,
вводя описание API как внешние XML файлы в формализированном виде.
Кто что думает?
P.S.: На следующей недели обещаю принести ссылку на код.
Дело нужное. Не забудь только оператор присваивания запретить.
И самое главное — любое летящее из внутреннего С++-а исключение должно обязательно ловиться С-шной обёрткой и снова бросаться в неизменном виде в С++-ной. И, кстати, что ты будешь делать в случае, если над С++-ом есть только С (без С++ над С) и этот внутренний С++ выбросит, скажем, boost::thread_interrupted. С-шный код как-то должен понять, что пора закругляться, но как?
Если не поможет, будем действовать током... 600 Вольт (C)
Здравствуйте, -MyXa-, Вы писали:
MX>Дело нужное. Не забудь только оператор присваивания запретить.
Детали реализации будем всем комьюнити оттачивать.
MX>И самое главное — любое летящее из внутреннего С++-а исключение должно обязательно ловиться С-шной обёрткой и снова бросаться в неизменном виде в С++-ной.
Об этом я думал.
MX> И, кстати, что ты будешь делать в случае, если над С++-ом есть только С (без С++ над С) и этот внутренний С++ выбросит, скажем, boost::thread_interrupted. С-шный код как-то должен понять, что пора закругляться, но как?
Над этим еще не размышлял.
GC>То есть средство должно быть как можно легковесней, не быть очередным COM.
C++ это обычно еще и контейнеры std/ссылки на другие С++ объекты в параметрах + уже упомянутые исключения. Без всего этого С++ не торт, а со всем этим — придется реализовывать некий маршалинг, т.е. очередной ком.
GC>Ну, и вообще, наблюдал несколько разных библиотек, имеющих С API, написанное в духе С++, то есть имеется метод имя_класса_create() имя_класса_destoy(), имя_класса_имя_метода(),
Я както реализовывал одну библиотеку и решил соригинальничать. Сделал create, который возвращает указатель на структуру, содержащую указатели на thunk'и, вызывающие методы с this конкретно этого С++ объекта. В итоге в С код выглядел совсем как С++-ный
Some *thing = CreateSome();
thing->MakeUserHappy();
thing->Release();
CreateSome в свою очередь выделял кусок executable памяти, в которую генерил на асме стабчик для вызова конкретного метода конкретного плюсового объекта 'под капотом' Some.
Как много веселых ребят, и все делают велосипед...
Здравствуйте, ononim, Вы писали:
GC>>То есть средство должно быть как можно легковесней, не быть очередным COM. O>C++ это обычно еще и контейнеры std/ссылки на другие С++ объекты в параметрах + уже упомянутые исключения. Без всего этого С++ не торт, а со всем этим — придется реализовывать некий маршалинг, т.е. очередной ком.
Согласен.
Вообще надеюсь, что тут можно будет отделаться интерфейсом для STL, написанным вручную и реализацией для него.
Будет идти в составе пакета.
GC>>Ну, и вообще, наблюдал несколько разных библиотек, имеющих С API, написанное в духе С++, то есть имеется метод имя_класса_create() имя_класса_destoy(), имя_класса_имя_метода(), O>Я както реализовывал одну библиотеку и решил соригинальничать. Сделал create, который возвращает указатель на структуру, содержащую указатели на thunk'и, вызывающие методы с this конкретно этого С++ объекта. В итоге в С код выглядел совсем как С++-ный O>
O>CreateSome в свою очередь выделял кусок executable памяти, в которую генерил на асме стабчик для вызова конкретного метода конкретного плюсового объекта 'под капотом' Some.
Прикольно. Но this ведь нужно было передавать, или как-то автоматом подхватывался?
P.S.: Сегодня вечером скину ссылку на GitHub проект, все еще в процессе, немного забросил его, но думаю возобновить.
Здравствуйте, GhostCoders, Вы писали:
GC>Добрый день!
GC>Решил я запилить open source средство для автоматического враппинга С++ кода. GC>Компиляторов С\С++ много, у каждого свой ABI, соглашения о наименовании методов (name mangling) разные.
штука нужная, я буквально неделю назад в полуавтоматическом режиме делал C-wrapper для библиотеки на C++.
как дела обстоят с колбэками? скажем, есть что-то вроде такого:
Здравствуйте, LuciferSaratov, Вы писали:
LS>штука нужная, я буквально неделю назад в полуавтоматическом режиме делал C-wrapper для библиотеки на C++. LS>как дела обстоят с колбэками? скажем, есть что-то вроде такого:
LS>
Пока, к сожалению, колбэки никак не поддерживаются.
Добавил в файлик TODO запись про поддержку колбэков.
Ну, а вообще идеи такие.
IEventListener — это класс, и он с точки зрения клиента будет представлен в виде С функции:
extern"C"void ieventlistener_oneventhappened(void* self, int errorCode);
Пока так, размер буковок пока не сохраняется, над этим надо подумать, но пока это не важно.
Если клиент будет вызывать реализацию IEventListener, которая реализована в библиотеке, то он будет пользоваться данной С-функцией.
Но, колбэк на то и колбэк, чтобы дать возможность библиотеке вызывать нашу реализацию IEventListener (реализованную на стороне клиента).
Пользователь вручную создает или автоматически генерирует такую С-функцию:
Возможно библиотека будет иметь насколько реализаций интерфейса IEventListener.
Но BCAPI должна каким-то образом узнать, что для интерфейса IEventListener возможна реализация на стороне клиента (могут быть колбэки).
Думаю, что для этого должен быть какой-то флаг в XML файле описания интерфейсов.
Если данный флаг включен, то BCAPI генерирует еще одну реализацию IEventListener:
class CustomCallbackForIEventListener : public IEventListener
{
void* m_self;
ONEVENTHAPPENDED_FP m_function;
public:
virtual void SetSelfPointer(void* pointer)
{
m_self = pointer;
}
void SetOnEventHappenedPointer(ONEVENTHAPPENDED_FP pointer_to_function) // передается указатель не функцию типа ieventlistener_oneventhappened
{
m_function = pointer_to_function;
}
virtual void OnEventHappened( int errorCode )
{
m_function(m_self, errorCode); // вызываем функцию по указателю на нее, передаем ранее запомненный this на пользовательский объект
}
};
Со стороны клиента нужно создать объект CustomCallbackForIEventListener (который будет обвёрнут обычным для BCAPI способом),
задать ему указатель на нужный объект колбака через SetSelf, задать указатель на функцию, и использовать объект CustomCallbackForIEventListener там, где планируется использовать IEventListener:
Как-то так.
Замечания:
1) Фактически два раза вызов виртуальной функции при вызове колбэка OnEventHappened() — один раз виртуальная функция CustomCallbackForIEventListener::OnEventHappened(),
и затем вызов функции через указатель. Да еще третий раз в самой функции my_event_listener_on_event_happened() вызывается виртуальный метод.
Но на самом деле тут реализацию MyEventListener можно делать обычным классом (не полиморфным), а методы заинлайнить.
2) Интерфейс CustomCallbackForIEventListener нужно делать видимым для пользователя, чтобы он мог воспользоваться функциями SetSelfPointer и SetOnEventHappenedPointer
3) Если число методов большое (не только один метод OnEventHappened) — то это неудобно.
Нужно сделать так, чтобы генерировались классы, уже делающие всю работу на стороне клиента
4) Еще есть мысль, чтобы CustomCallbackForIEventListener тоже был неполиморфный, возможно с инлайн методами.
Сейчас в проекте Чаппи (beautuful capi, или просто capi, или просто чаппи) уже кое-что есть и что-то даже работает.
Проект использует Python 3 для собственно генерации.
Есть несколько примеров, написаны на С++ 2003, с использованием CMake (проверял на 3-м CMake, на 2.8 могут быть проблемы).
CMake примеры используют add_custom_command для запуска генерирующего Python скрипта:
Для запуска всех примеров нужно сделать примерно следующее (находясь в корне репозитрия):
cd examples
mkdir build
cd build
cmake -G "Visual Studio 14 2015 Win64" ..
в папочке build появится солюшн для студии, открываем его и собираем.
Например, hello_word — пример библиотечки, hellow_world_client — пример программы, использующей эту библиотеку.
/examples/hello_world/library/hello_world.xml содержит описание API нашей библиотеки.
Формальная схема формата описания API — source/capi.xsd
Кратце так:
1) API состоит из пространств имен. Пространства имен могут быть вложенными неограниченное число раз.
2) На самом верхнем уровне могут быть несколько пространств имен.
3) В каждом пространстве имен могут находиться описания _классов_. Именно, классов, а не интерфейсов. Об этом чуть позже.
4) В каждом классе может быть несколько методов. У метода есть возвращаемое значение и набор аргументов.
5) Для каждого класса может быть несколько конструкторов.
6) В каждом програнстве имен могут быть еще standalone функции, как обычные так и фабрики, т.е. функции которые возвращают новые объекты классов.
По-умолчанию принята такая схема генерации хидеров:
1) Для каждого пространства имен создается папка
2) Структура папок вложенная, отражающая структуру вложенности пространст имен.
Например, у нас есть пространство имен Example::Geometry::Brep, то создается папка Example/Geometry/Brep
3) Каждый класс представлен своим хидером, например, класс Example::Geometry::Brep::Body будет находиться в
Example/Geometry/Brep/Body.h
4) Для каждого пространства имен создается "куммулятивный" хидер, который включает хидеры всех его классов и нижестоящих пространств имен.
Например, Example/Geometry/Brep.h будет включать в себе через #include файл Example/Geometry/Brep/Body.h,
Example/Geometry.h будет включать Example/Geometry/Brep.h,
а Example.h будет включать Example/Geometry.h.
Пользователю достаточно будет включить файл Example.h и все содержимое просранства имен Example будет включено.
Я также ввел различные флаги для управления алгоритмом создания таких папок.
Есть возможность генерации всех классов, пространств имен в один-единственный хидер (для простоты).
Кроме xml описания самого API есть xml с настройками генерации, формальная схема которого — source/capi-params.xsd
В папочке examples есть кое-какие примеры.
В папке test находятся тесты, сейчас там тестируются всевозможные режимы создание папок с хидерами (file_options).
Я не питонист, прощу оценить код, дать какие-нибудь полезные замечания, как по питон-коду, так и по С\С++ коду, так и вообще по проекту.
По-умолчанию, Чаппи, будет генерировать С-функции вида
extern"C" ...
Но для этого нужны .lib файлы — компаньоны .dll.
Проблема в том, что их форматы иногда не совместимы — COEF vs OMF.
Поэтому имеет смысл (опционально) сделать их динамически загружаемыми через указатели,
с использованием LoadLibrary\GetProcAddress\FreeLibrary для Windows, и dlopen\dlsym\dlclose для *nix
(будет располагаться в header-only библиотеке).
Но указатели нужно инициализировать явно, либо генерировать классы для этого, типа:
Сейчас пока реализована возможность с использованием implib библиотек (нужны файлы компаньоны к .dll — .lib, хотя для *nix это вообще не нужно, как понимаю).
Небольшая проблемка в том, что в оборачиваемой библиотеке существует несколько пространств имен верхнего уровня.
И нет единого хидера верхнего уровня для всей библиотеки (за исключением случая когда один файл только и генерируется).
И тут придеться инициализировать каждый неймспейс верхнего уровня отдельно, или... что-то придумать..
Какие будет мысли?
(хотя, конечно, реализация динамической загрузки С-функций, думаю, не должны быть в приоритете над остальными фичами, как так исключения, коллбэки и некоторые другие)
То есть доступ идет через точку.
И сам класс, по сути являющийся легким враппером, имеет семантику копирования.
Проблема возникает в случае если класс А использует класс Б и наоборот.
Обычно, в С++ используют forward declaration, реализацию выносят в отдельный .cpp файл (от .h заголовка),
и в заголовках используют указатель или ссылку.
Такие циклические связи между классами необходимо отразить и в моих врапперах.
Но! Forward declaration работает только для указателей или ссылок, а у меня значения!
Поэтому пришлось немного исхитриться.
Для простого аргумента используется:
То есть для возращаемых значений их можно присвоить враппер классу (благодаря оператору приведения),
или сделать вызов на лету, с использованием ->
Circular::ClassA a_object; ...
a_object.GetB()->Show(); // вызов на лету
Circular::ClassB b_object = a_object.GetB();
b_object.Show(); // а тут используем точку (".") как обычно
но получается, в одном месте используется стрелка, в другом точка. Нехорошо.
Сейчас только что добавил новый пример boost_shared_ptr, показывающий как оборачивать boost::shared_ptr смарт-поинтеры.
У самого boost::shared_ptr используется семантика копирования, а уж внутри себя он считает ссылки сам.
Плюс добавил субмодули для используемого boostа.
Еще добавился атрибут pointer_access, который означает, что доступ к обворачиваему классу идет через -> а не через .
Обычно это нужно для обвертки смарт-пойнтеров и похожих вещей.
Сегодня добавил фичу down_cast.
Это такой аналог dynamic_cast.
down_cast — это шаблонная ф-я, которая принимает два шаблонных параметра — целевой тип и входной тип.
Входной тип обычно выводится автоматически.
Добавил поддержку исключений.
exception_handling_mode может быть no_handling, которое означает что исключения никак специальным образом не обрабатываются (как раньше работало).
И новый режим by_first_argument, который означает, что каждая С-функция теперь в качестве аргумента принимает указатель на структуру
struct beautiful_capi_exception_info_t
{
int code;
void* object_pointer;
};
где — code исключения (0 — не было исключений), а object_pointer — указатель на этот объект.
Конечно, при использовании голого С в качестве клиента это будет не очень красиво и удобно.
Это должен быть стиль проверки кодов возврата и обратки ошибки в случае ненулевого кода.
Но С++ обвёртки скрывают это, можно перехватывать исключения через базовый тип, например так:
/
*
* Beautiful Capi generates beautiful C API wrappers for your C++ classes
* Copyright (C) 2015 Petr Petrovich Petrov
*
* This file is part of Beautiful Capi.
*
* Beautiful Capi is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Beautiful Capi is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Beautiful Capi. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <iostream>
#include <cstdlib>
#include"Example.h"int main()
{
try
{
Example::Printer printer;
Example::ScannerPtr scanner;
scanner->PowerOn();
std::cout << "Scanned text: " << scanner->ScanText() << std::endl;
scanner->PowerOff();
printer.PowerOn();
printer.Show("from main()");
std::cout << "Trying to show null pointer" << std::endl;
printer.Show(0); // Здесь выбрасывает исключение типа Exception::NullArgument, которое занаследовано от Exception::BadArgument, которое наследовано от Exception::Generic
printer.PowerOff();
}
/* Если закоментировать блок catch для NullArgument, то исключение можно словить как Exception:BadArgument, если и его закоментировать, то словится как Exception:Generic
/*catch (Exception::NullArgument& null_argument_exception)
{
std::cout << " *** Exception::NullArgument was thrown" << std::endl;
std::cout << " " << null_argument_exception.GetErrorText() << std::endl;
std::cout << " argument: " << null_argument_exception.GetArgumentName() << std::endl;
} */catch (Exception::BadArgument& null_argument_exception)
{
std::cout << " *** Exception::BadArgument was thrown" << std::endl;
std::cout << " " << null_argument_exception.GetErrorText() << std::endl;
}
catch (Exception::Generic& generic_exception)
{
std::cout << " *** Exception::Generic was thrown" << std::endl;
std::cout << " " << generic_exception.GetErrorText() << std::endl;
}
catch (const std::exception& exception)
{
std::cout << " *** std::exception was thrown" << std::endl;
std::cout << " " << exception.what() << std::endl;
}
catch (...)
{
std::cout << " *** Unknown exception was hrown" << std::endl;
}
return EXIT_SUCCESS;
}
Здравствуйте, -MyXa-, Вы писали:
MX>Не забудь только оператор присваивания запретить.
Пока никак на это не реагировал. Но планирую не запретить, а переопределить.
MX>И самое главное — любое летящее из внутреннего С++-а исключение должно обязательно ловиться С-шной обёрткой и снова бросаться в неизменном виде в С++-ной.
Совсем в неизменном виде — не получается.
Исключение ловится С-шной обёрткой, но С++-ной выбрасывается соответствующий класс-обёртка (который виден на стороне клиента и через который осуществляется доступ к библиотечному классу).
MX>И, кстати, что ты будешь делать в случае, если над С++-ом есть только С (без С++ над С) и этот внутренний С++ выбросит, скажем, boost::thread_interrupted. С-шный код как-то должен понять, что пора закругляться, но как?
Здравствуйте, -MyXa-, Вы писали:
MX>Дело нужное. Не забудь только оператор присваивания запретить.
Я лично вот сильно сомневаюсь.
Очень уже сильно разная парадигма программирования в голом С и в маршевом C++.
Библиотеки С можно использовать в С++, и можно писать для них врапера.
А вот библиотеки С++ в С использовать очень будет сложно, для этого надо их сначала
переписать в дуже "С с классами", а потом уже использовать.
Исключения, управление памятью, инициализация, -- всё это не даст нормально
писать на С над С++ными библиотеками, да и зачем ? Легче выучить С++ и писать на нём.
Предполагается, что printer_create() будет
0) выделять память
1) инициализировать в ней новый объект соотв. класса.
Как она будет выделять память, где создавать объект ? Очевидно, в хипе (в динамической памяти), больше негде.
А в С++ объекты могут создаваться в 3 классах памяти: auto, global, dynamic.
Почему мы должны себя обделять такими возможностями, программируя на С с использованием этой будущей библиотеки?
Нет, не должны.
Имя printer_create(); состоит из двух частей: имя класса, и имя функции в классе.
Полное имя класса в С++ может быть очень большим, очень длинным — namespaces, inner classes .
И его мало того надо упаковать в С-шный идентификатор, но ещё и добавить потом имя метода, которое тут вполне безобидно короткое,
а для реального метода тоже может быть большим. А orerloading ?
Ну и вообще, проблема использования С++ кода из С надуманая и неинтересная -- проще взять С++ и на нём писать.
С и С++ компиляторы теперь ходят парами.
А нужна будет обёртка -- легче написать руками заточив под нужную семантику.
Здравствуйте, MasterZiv, Вы писали:
MZ>Исключения, управление памятью, инициализация, -- всё это не даст нормально MZ>писать на С над С++ными библиотеками, да и зачем ? Легче выучить С++ и писать на нём.
Ну, например, С++ный код в dll-ки заворачивать. Или в COM (это чисто теоретически, т.к. не думаю, что автор это планирует, но так-то у COM такие же правила, как и у С — не кидать исключений, возвращать код ошибки, вместо std::string — BSTR, и т.д.). Можно, конечно, классы экспортировать из dll как есть, но, думаю, это не все оценят положительно (особенно, если с того конца Васик или Питон).
MZ>Как она будет выделять память, где создавать объект ? Очевидно, в хипе (в динамической памяти), больше негде.
Она может запрашивать память у приложения. Многие С-шные библиотеки обращаются к malloc/free не напрямую, а через прослойку, которую можно настроить или во время компиляции библиотеки, или прямо во время исполнения.
Если не поможет, будем действовать током... 600 Вольт (C)
На днях была добавлена поддержка колбэков.
И поддержка исключений в таких колбэках.
Все в новом примере examples/callback:
class CustomPrinterImplementation
{
std::string mLastPrintedText;
public:
CustomPrinterImplementation()
{
std::cout << "CustomPrinterImplementation ctor" << std::endl;
}
~CustomPrinterImplementation()
{
std::cout << "CustomPrinterImplementation ctor" << std::endl;
}
void Print(const char* text) // Note that this method is non-virtual
{
if (!text)
{
// This exception will be correctly passed through the library boundary
// and will be caught by the library codethrow Exception::NullArgument();
}
mLastPrintedText = std::string(text);
std::transform(mLastPrintedText.begin(), mLastPrintedText.end(), mLastPrintedText.begin(), toupper);
std::cout << mLastPrintedText << std::endl;
}
};
int main()
{
Example::Person famous_person;
famous_person.SetFirstName("Isaac");
famous_person.SetSecondName("Newton");
famous_person.SetAge(26);
famous_person.SetMale(true);
Example::PrinterPtr printing_device = Example::CreateDefaultPrinter();
famous_person.Dump(printing_device);
famous_person.Print(printing_device, "Hello World");
CustomPrinterImplementation my_printer_implementation;
printing_device = Example::create_callback_for_customprinter(my_printer_implementation);
famous_person.Dump(printing_device);
// CustomPrinterImplementation::Print() will throw exception (Exception::NullArgument)
// and this exception will be caught by the library code
famous_person.Print(printing_device, 0);
return EXIT_SUCCESS;
}
Здесь сначала создается некая персона и стандартная реализация принтера (из библиотеки).
Персона дампится через этот принтер.
Далее создается "кастомный" принтер (реализация на стороне клиента).
И персона дампится с использованием этого принтера.
Еще есть метод Print у персоны. Она этот вызов перенапрявляет принтеру.
Это нужно для того, чтобы кастомному принтеру пришел nullptr в качестве строки и тот выкинул исключение (на стороне клиента).
И это исключение на стороне клиента корректно обрабатывается на стороне библиотеки.
DLL boundary проходит корректно.
Еще список TODO большой, но двигаюсь к первому релизу.
Потом буду просить использовать в каких-нибудь проектах для тестирования\использования.
Проект развивается потихонечку.
Добавил поддержку генерации doxygen комментариев, также был произведен рефакторинг.
Добавил план развития проекта (на GitHub в разделе Issues).