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, для перехода от с-апи к более удобной работы с библиотекой

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