Ещё несколько примеров байндингов типов из C++
и daScript
.
Standart-layout структуры#
Пустой базовый класс C++:class MyClass
{
public:
int hidden;
};
void printMyClass(const MyClass& a) {
std::cout << a.hidden << std::endl;
}
(переменная hidden — просто для проверок, что класс инициализирован)
Привязка его к daScript:
struct MyClassAnnotation final : ManagedStructureAnnotation<MyClass> {
MyClassAnnotation(ModuleLibrary& ml) : ManagedStructureAnnotation("MyClass", ml) {
}
void init() {
}
};
MAKE_TYPE_FACTORY(MyClass, MyClass);
..и добавление в модуль
class Module_Tutorial : public Module {
public:
Module_Tutorial() : Module("tutorial") { // module name, when used from das file
ModuleLibrary lib(this);
lib.addBuiltInModule();
addAnnotation(make_smart<MyClassAnnotation>(lib));
addCtorAndUsing<MyClass>(*this, lib, "MyClass", "MyClass");
addExtern<DAS_BIND_FUN(printMyClass)>(*this, lib, "printMyClass", SideEffects::worstDefault, "printMyClass");
}
};
REGISTER_MODULE(Module_Tutorial);
...
//где-то в регистрации модулей
NEED_MODULE(Module_Tutorial);
определение ManagedStructureAnnotation
выглядит как:
template <typename OT,
bool canNew = is_default_constructible<OT>::value,
bool canDelete = canNew && is_destructible<OT>::value
> struct ManagedStructureAnnotation ;
Это структура разбирает данные о типе OT
с помощью std::type_traits
и устанавливает его свойства, на основе которых daScript знает, что можно/нельзя делать с типом.
virtual bool hasNonTrivialCtor() const override {
return !is_trivially_constructible<OT>::value;
}
virtual bool hasNonTrivialDtor() const override {
return !is_trivially_destructible<OT>::value;
}
virtual bool hasNonTrivialCopy() const override {
return !is_trivially_copyable<OT>::value
|| !is_trivially_copy_constructible<OT>::value;
}
virtual bool isPod() const override {
return is_standard_layout<OT>::value && is_trivial<OT>::value;
}
virtual bool canMove() const {
return !hasNonTrivialCopy();
}
virtual bool canCopy() const {
return !hasNonTrivialCopy();
}
Пока всё тривиально, MyClass
— standart layout структура, не требующая дополнительной инициализации.//объявления на стеке
var c1 = [[MyClass]]
var c2 = MyClass()
var c3 : MyClass
printMyClass(c1) //0
//или на хипе
var c4 = new MyClass()
printMyClass(*c4) //0
//или создание временного объекта с помощью идиомы using:
using() <| $ ( var c5: MyClass# )
printMyClass(c5) //0
Non standart layout классы#
Попробуем добавить в класс что-нибудь, что потребует его инициализации:
class MyClass
{
public:
int hidden = 42; //инициализация члена
std::string str; //non-stardary layout член
MyClass(const MyClass& other) : str(other.str) {} //copy-ctor
virtual void virtualFunction() const {} //virtual function
//
MyClass(const char* data): str(data) {}
MyClass() = default
};
//...
//привязки
addCtorAndUsing<MyClass, const char*>(*this, lib, "MyClass", "MyClass")->args({ "str" });
addCtorAndUsing<MyClass, const MyClass&>(*this, lib, "MyClass", "MyClass")->args({ "other" });
using _method_100 = das::das_call_member< void(MyClass::*)() const, &MyClass::virtualFunction >;
makeExtern<DAS_CALL_METHOD(_method_100), SimNode_ExtFuncCall>(lib, "virtualFunction", "das::das_call_member< void(MyClass::*)(), &MyClass::virtualFunction >::invoke")
->addToModule(*this, SideEffects::worstDefault);
Байндинг класса при перекомпиляции “увидит”, что теперь класс нетривиальный, и правильно переопределит его свойства. Теперь класс не может быть локальным, не может быть скопирован или перемещён:bool canCopy() const override { return false; }
bool canMove() const override { return false; }
bool isLocal() const override { return false; }
Соотвественно, тот же код на daScript выдаст ошибки компиляции, при попытке создать класс без инициализации://можно создать класс на хипе:
var a <- new MyClass("hello heap")
printMyClass( *a ) //42
//можно создать временный объект с помощью using:
using("hello temp") <| $(var c5: MyClass explicit)
printMyClass(c5) //42
//можно создать временный объект на стеке и передать его в качестве параметра
//(конструктор класса ничем не отличается от обычной функции)
printMyClass(MyClass("hello temp"))
//нельзя создать объект, требующий перемещения (массив из одного элемента)
printMyClass([[MyClass()]])
//нельзя переместить объект
var c1 <- MyClass()
//нельзя скопировать объект
var c1 = MyClass()
daScript
достаточно консервативно определяет, что объект нельзя копировать/перемещать, вообще говоря, если у класса есть конструктор копирования, то его можно разрешить копировать, если просто явно переопределить функцию canCopy в аннотации-обёртке класса
bool isCopyConstructable = std::is_copy_constructible<MyClass>::value;
bool canCopy() const override { return isCopyConstructable; }
bool canClone() const override { return isCopyConstructable; }
bool canMove() const override { return isCopyConstructable; }
//но нельзя создавать локальный переменные, так как они позволяет не инициализировать класс
bool isLocal() const override { return false; }
Если можно построить объект из другого объекта, то можно и copy/clone/move?
//теперь можно copy/clone/move
var a <- new MyClass("hello heap")
var b = new MyClass()
*b = *a
*b := *a
*b <- *a
Также в unsafe блоке теперь можно делать небезопасные, но интересные штуки:struct Params
a: MyClass
unsafe
//создаём локальную переменную на стеке
var m = MyClass("hello_local")
//создаём контейнер неинициализированных объектов
// которые можно построить позже в этой памяти
// (аналог placement new в c++)
var n = [[MyClass(m)]]
//создаём структуру из неинициализированных объектов
//( аналог stackframe)
var s : Params
printMyClass(s.a)
//s.a |> virtualFunction() //пока нельзя обращаться к объекту
s.a <- MyClass("hello_local")
s.a |> virtualFunction() //vtable инициализирована
printMyClass(s.a) //42
Аргументы и результаты#
Если попытаться привязать такую функцию:void functionWithClassArgument(MyClass a) { }
компилятор начнёт ругаться на то, что не определён шаблон cast_arg<MyClass>::to
. daScript-функции представляют свои аргументы и результаты в виде 128-битного типа vec4f
, так что для кастомных типов необходимо описать способ преобразования с помощью частичной специализации этого шаблона.
template <> struct cast_arg<MyClass> {
static __forceinline const MyClass& to(Context& ctx, SimNode* node) {
vec4f res = node->eval(ctx);
return *cast<MyClass*>::to(res);
}
};
Другие примеры возможных способов определения преобразования:
//если тип standart-layout и меньше 128 байт -- можно просто скопировать память
template <> struct cast_arg<const ImVec2 &> {
static __forceinline ImVec2 to ( Context & ctx, SimNode * node ) {
vec4f res = node->eval(ctx);
ImVec2 v2;
memcpy(&v2,&res,sizeof(ImVec2));
return v2;
}
};
//для типов-хэндлеров можно указать способ приведения хэндлера к какому-нибудь базовому типу
// (каст указателей можно рассматривать как частный случай хэндлеров, уже определенных явно)
template <> struct cast_arg<ax::NodeEditor::NodeId> {
static __forceinline ax::NodeEditor::NodeId to ( Context & ctx, SimNode * node ) {
vec4f res = node->eval(ctx);
return ax::NodeEditor::NodeId(cast<int32_t>::to(res));
}
};
template <> struct cast_res<ax::NodeEditor::NodeId> {
static __forceinline vec4f from ( ax::NodeEditor::NodeId node, Context * ) {
return cast<int32_t>::from(int32_t(node.Get()));
}
};
//для типов-прокси можно определить способ построения прокси из базового типа/извлечения базового типа
template <> struct cast_arg<const sf::String &> {
static __forceinline sf::String to ( Context & ctx, SimNode * node ) {
char * pstr = node->evalPtr(ctx);
return sf::String(pstr ? pstr : "");
}
};
template <> struct cast_res<sf::String> {
static __forceinline vec4f from ( const sf::String & str, Context * context ) {
auto text = context->stringHeap->allocateString(str);
return cast<char *>::from(text);
}
};
Из пары примеров выше видно, что для привязки функций, возвращающих тип в качестве результата, необходимо определить специализацию шаблона cast_res
с функцией from
. Это верно для standard layout структур, но для сложного класса (с созданием временного объекта на хипе, по аналогии с sf::String) daScript бросает assert:
addExtern(getMyClass_ExtFuncCall)::failed
this function should be bound with addExtern<DAS_BIND_FUNC(getMyClass_ExtFuncCall), SimNode_ExtFuncCallAndCopyOrMove>
likely cast<> is implemented for the return type, and it should not
говорит этот ассерт о том, что вместо того, чтобы создавать временный объект, что тормознуто, лучше использовать специальную ноду языка, которая возвращает уже созданный объект. Такое себе принуждение к оптимизации.
class MyClass {
MyClass getMyClass() const{ return *this; }
};
using _method_2 = das::das_call_member< MyClass(MyClass::*)() const, &MyClass::getMyClass >;
//makeExtern<DAS_CALL_METHOD(_method_2), SimNode_ExtFuncCall >(lib, "getMyClass_ExtFuncCall", "das::das_call_member< MyClass(MyClass::*)() const, &MyClass::getMyClass >::invoke")
// ->addToModule(*this, SideEffects::worstDefault); //work with pod type, but not if type has something not trivial
makeExtern<DAS_CALL_METHOD(_method_2), SimNode_ExtFuncCallAndCopyOrMove >(lib, "getMyClass_ExtFuncCallAndCopyOrMove", "das::das_call_member< MyClass(MyClass::*)() const, &MyClass::getMyClass >::invoke")
->addToModule(*this, SideEffects::worstDefault);
Теперь в daScript можно использовать эти функции:
functionWithClassArgument(MyClass("hello arg"))
MyClass("hello res")|> getMyClass_ExtFuncCallAndCopyOrMove()
Reflection over C++#
Вся эта шаблонная магия привязок генерится не руками, а генератором привязок dasClangBind. Распознавание инфы о типах сделано на уровне самого кода daScript, а не генератора, чтобы сам код генератора и сгенерированный код был более простым и однообразным. Но все примеры обёрток, сделанные dasClingBind
, сделаны для библиотек с C-интерфейсом, которые почти не требуют ручного вмешательства. Но как только дело доходит до реального C++ кода, вылезает всё и сразу. Описанные в статье приёмы позволяют побороть большую часть сложности, и нагенерировать что-нибудь серьёзное, типа привязок классов Unreal Engine
(с небольшими доработками напильником).
Примеры привязок либ с c-интерфейсом через dasClangBind
:
Другие подходы:
Automatic Language Bindings — размышления о способах генерации привязок к языкам от автора sokol gfx (тоже c-style, с помощью clang json)
Using C Libraries in Zig — прозрачный импорт C из zig. с Си (не С++) вообще все достаточно просто
Binding Nim to C++ std::list — читерский подход в nim, без интерпретации и с транспиляцией в C++ можно просто встраивать и использовать куски плюсового кода.
Circle — “бэтменский” альтернативный компилятор с встроенными compile-time фичами, включая рефлексию. Автор публикует прогресс в твиттере
cppyy: Automatic Python-C++ bindings — хардкор с использованием интерактивного компилятора C++ cling, прозрачный парсинг, компиляция и генерация привязок на лету