Spiiin's blog

daScript: C++ auto-bindings, msgpack

Ещё один “подход” к языку daScript от Gaijin. В последней заметке про использование байндингов к OpenGL в daScript, я упоминал про наличие скрипта dasClangBind, позволяюшего генерировать байндинги к библиотекам на C и C++. Так как из документации к скрипту только совет автора Use it, abuse it, то неплохо попробовать его в деле, чтобы разобраться, что он умеет/не умеет.

С помощью этого генератора байндингов сделаны обёртки над: dasRequests, dasPhys2d, а также добавленные в основной репозиторий dasGLFW, dasBGFX, dasImgui, dasOpenGL, dasClangBind (привязки для генератора байндингов тоже сгенерированы им самим).

dasClangBind не собирается с дефолтными настройками cmake, поэтому сначала необходимо включить его сборку. В файле CMakeLists.txt видим настройку пути с libclang:

SET(PATH_TO_LIBCLANG ${PROJECT_SOURCE_DIR}/../libclang)

Можно скачать скомпилированные библиотеки LLVM (yay, даже для windows!), и указать путь к скачанной библиотеке с cmake-файле. Дальше перегенерируем решение с помощью команды generate_msvc_XXX.bat и компилируем проект libDasModuleClangBind.

Получаем библиотеку libDasModuleClangBind.lib, которую можно подключить для статической линковки из проекта, который будет использовать этот модуль (командой TARGET_LINK_LIBRARIES). В сгенерированном решении убеждаемся в том, что линкер подключает библиотеки libDasModuleClangBind.lib и libclang.lib:
lib

Далее в коде подключаем заголовочный файл и макрос добавления модуля в daScript:

#include "../modules/dasClangBind/src/dasClangBind.h"
...
int main( int, char * [] ) {
NEED_ALL_DEFAULT_MODULES;
NEED_MODULE(Module_dasClangBind); //<---
}

Теперь в das-скрипте можно импортировать модуль cbind, который предоставляет функции-обёртки над библиотекой clang, а главное — cbind_boost, классы, с помощью которых можно настроить поведение генераторы, без низкоуровневого обращения к c-апи clang-а:

require cbind
require cbind_boost

libclang

Для начала лучше бегло ознакомиться с тем, что умеет libclang:
Using libclang to Parse C++
Пример разбора C++ кода с помощью libclang на Python
Choosing the Right Interface for Your Application

DasGenBind

У генератор привязок dasClangBind, есть 2 режима: генерация обёрток над функциями в виде daScript (с помощью ffi-интерфейса dasbind) — DasGenBind, и более мощная генерация “обвязочного” c++-кода библиотеки — CppGenBind. С помощью DasGenBind сгенерированы байндинги к OpenGL, так как сама библиотека language-agnostic, и её обвязки тривиальны — используются только функции и примитивные типы.

CppGenBind

Более интересно посмотреть на байндинги к glfw, по которым можно приблизительно понять, что генерируется автоматически, а что необходимо добавлять в исключения и дописывать руками. dasClangBind в ходе своей работы пишет, какие объявляния функций он пропускает (код - поиск по ключевому слову skip):

  • шаблоны
  • функции с аргументами-указателями на функции (к примеру, колбеки)
  • чисто виртуальные функции
  • глобальные операторы
    Также генерируются, но вызывают последующие ошибки компиляции, функции (в лучших традициях текстов ошибок шаблонов C++), получающие аргументы POD-типов по значению.

Для таких функций предполагается добавление их в список пропускаемых при автоматической генерации и последующее написания обработчика вручную:

//переопределение метода AnyGenBind
 def override skip_anyFunction ( var c : CXCursor; isMethod : bool )
        if AnyGenBind`skip_anyFunction(self, c, isMethod)
            return true
        //свои проверки, нужно или нет генерировать обёртку для функции

Для дописывания несгенерированных автоматически функций предусмотрен файл MODULENAME.main.cpp. На выходе генератор байндингов выдаёт пачку и кусок cmake-файла в stdout, с помощью которого можно собрать их в модуль. На практике почему-то у меня не создавались файлы MODULE.func.reg.inc и MODULE.func.decl.,inc, их для теста заполнил руками.

CmakeList.txt

В качестве шаблона cmake-файла можно взять готовый из других модулей, основная логика:

  • собрать модуль из сгенерированных файлов (это за нас выводит сам dasClangBind)
  • подключить lib-файл самой C++ библиотеки, для которой делается обвязка
  • определить переменные сборки daScript, позволяющие отключить модуль по желанию пользователя

Генерация байндингов для библиотеке MessagePack

Исходя из ограничений генератора, для учебного примера проще всего выбрать для примера максимально простую библиотеку, имеющую C, а не С++-интерфейс. Например - MessagePack.

Стартовый код генератора:

require cbind/cbind_boost
require daslib/safe_addr
require daslib/strings
require daslib/defer
require daslib/fio


class MsgpackGen : CppGenBind
    override func_to_stdout = false
    unique_functions : table<string; bool>

    def MsgpackGen
        bind_root = "{get_das_root()}/modules/dasMsgpack/src"
        bind_module = "msgpack"
        bind_das_module = "msgpack"
        let pfn = "msgpack.h"
        let pfp = "{get_das_root()}/modules/dasMsgpack/msgpack-c/include/"

        let args <- [{string
            "-xc++-header";
            "-std=c++1z";
            "-I{get_full_file_name(pfp)}"
        }]

        func_per_chunk = 20
        init_args(pfn,pfp,args)
        setDefaultFiles()
        //init_skip_func()
        openAllFiles()


    def override namespace_name(name:string; dash:string="::") : string
        return AnyGenBind`namespace_name(self, name, dash)

    def override skip_struct(name : string)
        return false
        //return AnyGenBind`skip_struct(name)

    def override skip_anyFunction(var c : CXCursor; isMethod:bool) : bool
        let funcname = string(clang_getCursorSpelling(c))
        //не генерировать обвязку для повторно встречаемой функции (не совсем понимаю, почему встречаются повторы)
        if unique_functions |> find(funcname) != null
            return true
        else
            unique_functions[funcname] = true
            return AnyGenBind`skip_anyFunction(self, c, isMethod)


    //генерируем код для файлов, подходящих по шаблону msgpack/*.h
    def override skip_file(fname:string) : bool
        if fname |> find("msgpack/") != -1
            return false
        return ! fname |> ends_with(PARSE_FILE_NAME)


    def override generateModuleHPrefix
        module_h_file |> fwrite("#include \"need_msgpack.h\"\n")



[export]
def main
    var cgb = new MsgpackGen()
    defer <|
        unsafe
            delete cgb
    cgb->generate()
    cgb->genCMakeDecl("DAS_MSGPACK_BIND")

Такой скрипт генерирует привязки к библиотеке, однако при её компиляции возникают несколько ошибок вида:

использование неопределенного типа "das::cast<TT>" libDasModuleMsgpack \daScript\include\daScript\simulate\simulate.h

Необходимо добавить эти функции в список исключаемых из генерации:

//передача в качестве аргумента POD-объекта по значению
msgpack_object_print
msgpack_object_print_buffer
msgpack_object_equal

//какие-то непонятки с передачей некоторых из базовых типов из C++ в daScript?
msgpack_pack_char
msgpack_pack_long
msgpack_pack_unsigned_long

Убрав их из генерации, получаем компилирующийся модуль daScript dasMsgpack.

Тестовый скрипт

Попытаемся портировать тестовый пример библиотеки с MessagePack с языка C на daScript:

require msgpack

[export]
def main
    var sbuf : msgpack_sbuffer
    var pk : msgpack_packer
    var mempool: msgpack_zone
    var deserialized: msgpack_object

    unsafe
        var ptrBuf = addr(sbuf)
        var ptrPk = addr(pk)
        var ptrMempool = addr(mempool)
        var ptrDeserialized = addr(deserialized)

        /* msgpack::sbuffer is a simple buffer implementation. */
        msgpack_sbuffer_init(ptrBuf)
        /* serialize values into the buffer using msgpack_sbuffer_write callback function. */
        msgpack_packer_init(ptrPk, ptrBuf)

        msgpack_pack_array(ptrPk, 4ul)
        msgpack_pack_int(ptrPk, 1)
        msgpack_pack_true(ptrPk)
        msgpack_pack_float(ptrPk, 3.0)
        msgpack_pack_str(ptrPk, 7ul)
        msgpack_pack_str_body(ptrPk, "example", 7ul);

        //print("{sbuf.data}, {sbuf.size}")

        msgpack_zone_init(ptrMempool, 2048ul)

        var data = 0ul;
        var ptrData = addr(data)
        msgpack_unpack(sbuf.data, sbuf.size, ptrData, ptrMempool, ptrDeserialized);
        msgpack_object_print(ptrDeserialized);

        msgpack_zone_destroy(ptrMempool);
        msgpack_sbuffer_destroy(ptrBuf)

Некоторые функции из примеры не попали в автоматическую обвязку, поэтому необходимо дописать обвязку для них вручную в файле msgpack.main.cpp:

//передаём 3-й параметр по умолчанию
void das_msgpack_packer_init(msgpack_packer* pk, void* data) {
msgpack_packer_init(pk, data, msgpack_sbuffer_write);
}

//передаём параметр по указателю, не по значению
void das_msgpack_object_print(msgpack_object* o) {
msgpack_object_print(stdout, *o);
}

//меняем тип второго указателя на const char* вместо неизвестного генератору void *
void das_msgpack_pack_str_body(msgpack_packer* pk, const char* b, size_t l) {
msgpack_pack_str_body(pk, b, l);
}

void Module_msgpack::initMain() {
//добавляем функции в модуль
addExtern<DAS_BIND_FUN(das_msgpack_packer_init)>(*this,lib,"msgpack_packer_init",
SideEffects::worstDefault,"das_msgpack_packer_init");
addExtern<DAS_BIND_FUN(das_msgpack_object_print)>(*this, lib, "msgpack_object_print",
SideEffects::worstDefault, "das_msgpack_object_print");
addExtern<DAS_BIND_FUN(das_msgpack_pack_str_body)>(*this, lib, "msgpack_pack_str_body",
SideEffects::worstDefault, "das_msgpack_pack_str_body");
}

Теперь скрипт работает и выдаёт корректный результат десериализации объекта:

[1, true, 3.000000, "example"]

Дальнейшая работа над модулем может предполагать:

  • возможность устанавливать daScript колбеки (пример из dasGlfw)
  • daScript-обвязка над POD-структурой msgpack_object и корректная передача исключенных типов(?)
  • написание “daScript-ивизирующей” обёртки msgpack_boost, для перехода от с-апи к более удобной работы с библиотекой

Репозиторий с примерами кода