Расширение PHP и Kotlin Native. Часть третья, наверное финальная

    В первой части рассказываются совсем базовые вещи про настройку инструментария и общие концепции.

    Вторая часть про, так сказать, первый подход к снаряду, задумки, наметки, планы.

    В этой статье будет чуть больше хардкора про интероп Си и K/N, много макросов, боли, безысходности и «лучей добра». Конечно же будет глава с рассказом о достижениях (сам себя не похвалишь… и в качестве бонуса рассказ о эпичном факапе.

    Disclaimer: всё нижеследующее рассматривается в контексте написание библиотеки для PHP.

    Глава первая. Интероп наивный


    Про то, как использовать K/N функций в Си описано в первой части цикла. Соответственно тут я расскажу, как использовать функции Си в K/N.

    Официальная документация довольно скупа и лаконична, однако, для простых проектов, ее вполне достаточно.

    Если вкратце, то надо создать специальный файл с расширением .def и указать в нем необходимые заголовочные файлы.

    headers = php.h
    

    Потом скормить его программе под названием cinterop.

    # cinterop -def php.def -o php
    

    На выходе вы получите библиотеку libphp.klib, содержащую llvm bitcode и различную мета-информацию.

    Дальше можно смело пользоваться описанными в заголовочном файле функциями и макросами (#define), не забыв подключить библиотеку на этапе компиляции.

    # kotlinc -opt -produce static ${SOURCES} -l libphp.klib -o myLib
    

    Но есть нюанс. И не один.

    В том виде, как описано выше, библиотека не соберётся


    Почему? А потому, что в php.h присутствуют следующие строки:

    #include "php_version.h"
    #include "zend.h"
    #include "zend_sort.h"
    #include "php_compat.h"
    #include "zend_API.h"
    

    Тут надо заметить, что компиляцией библиотеки занимается все же llvm, а у него есть ключ -I, а у cinterop есть ключ -copt. Ну вы поняли. В итоге, для компиляции php.h достаточно вот такой команды.

    # cinterop -def my.def -o myLib -I${PHP_LIB_ROOT} -copt -I${PHP_LIB_ROOT} \
    -copt -I${PHP_LIB_ROOT}/main \
    -copt -I${PHP_LIB_ROOT}/Zend \
    -copt -I${PHP_LIB_ROOT}/TSRM
    

    Макросы. Я вас люблю и ненавижу! Хотя нет, просто ненавижу.


    Все, что вам нужно знать про #define в части интеропа Си > K/N — это
    Every C macro that expands to a constant is represented as Kotlin property. Other macros are not supported.

    А потом вспоминаем, что расширение PHP — это макрос на макросе и макросом погоняет и стараемся не расплакаться.

    Но не все так плохо. Для обхода подобной ситуации разработчики K/N предусмотрели моток синей изоленты для примотки к def-файлу custom declarations. Выглядит оно таким образом (для примера возьмем макрос Z_TYPE_P)

    headers = php.h
    
    ---
    
    static inline zend_uchar __zp_get_arg_type(zval *z_value) {
        return Z_TYPE_P(z_value);
    }

    Теперь в коде K/N можно будет использовать функцию __zp_get_arg_type

    Глава вторая. PHP INI-settings или макрос с подвыподвертом.


    Это «луч добра» в сторону исходников PHP.

    Для извлечения настроек предусмотрено 4 макроса:

    INI_INT(val)
    INI_FLT(val)
    INI_STR(val)
    INI_BOOL(val)
    

    Где val — строка с именем настройки.

    А теперь давайте, на примере INI_STR, посмотрим, как же этот макрос определен.

    #define INI_STR(name) zend_ini_string_ex((name), sizeof(name)-1, 0, NULL)

    Уже заметили его «фатальный недостаток»?

    Если нет, то подскажу — это функция sizeof. Когда вы используете макрос напрямую, то все хорошо:

    php_printf("The value is : %s", INI_STR("my.ini"));

    Когда вы используете его через прокси-функцию из .def-файла — карета превращается в тыкву, а sizeof(name) возвращает размер указателя. Шах и мат Kotlin Native.

    Вариантов обхода, собственно, всего два.

    1. Использовать не макросы, а функции, к которым они привязаны.
    2. Хардкодить функции-обертки для каждой необходимой настройки.

    Первый вариант всем лучше второго, кроме одного момента — никто не даст гарантии, что декларация макроса не поменяется. Поэтому, для своего проекта, я, с чувством глубокого неудовлетворения, выбрал второй вариант.

    Глава третья. Дебаг? Какой дебаг?


    Акт 1 — интероп.


    В один прекрасный момент, после приматывания синей изолентой к def-файлу 20-ти очередных прокси-функций, я получил замечательную ошибку.

    Exception in thread "main" java.lang.Error: /tmp/tmp399964332777824085.c:103:38: error: too many arguments to function call, expected 2, have 3
            at org.jetbrains.kotlin.native.interop.indexer.UtilsKt.ensureNoCompileErrors(Utils.kt:137)
            at org.jetbrains.kotlin.native.interop.indexer.IndexerKt.indexDeclarations(Indexer.kt:902)
            at org.jetbrains.kotlin.native.interop.indexer.IndexerKt.buildNativeIndexImpl(Indexer.kt:892)
            at org.jetbrains.kotlin.native.interop.indexer.NativeIndexKt.buildNativeIndex(NativeIndex.kt:56)
            at org.jetbrains.kotlin.native.interop.gen.jvm.MainKt.processCLib(main.kt:283)
            at org.jetbrains.kotlin.native.interop.gen.jvm.MainKt.interop(main.kt:38)
            at org.jetbrains.kotlin.cli.utilities.InteropCompilerKt.invokeInterop(InteropCompiler.kt:100)
            at org.jetbrains.kotlin.cli.utilities.MainKt.main(main.kt:29)
    


    Комментим половину, пересобираем, если повторилось комментим половину оставшегося, собираем… А учитывая, что процесс компиляции хидеров достаточно долог… (да, так показалось быстрее, чем лазить по десятку исходных файлов и скурпулезно, с лупой, выверять).

    Второй «луч добра» уходит в сторону JetBrains.


    Акт 2 — рантайм.


    Получаю в рантайме segmentation fault. Ну ок, бывает. Лезу в отладчик. Эммм… ШТА?

    Program received signal SIGSEGV, Segmentation fault.
    kfun:kotlinx.cinterop.toKString@kotlinx.cinterop.CPointer<kotlinx.cinterop.ByteVarOf<kotlin.Byte>>.()kotlin.String ()
        at /opt/buildAgent/work/4d622a065c544371/Interop/Runtime/src/main/kotlin/kotlinx/cinterop/Utils.kt:402
    402     /opt/buildAgent/work/4d622a065c544371/Interop/Runtime/src/main/kotlin/kotlinx/cinterop/Utils.kt: No such file or directory.
    


    Глава четвертая. Я налил чай в твой чай, чтобы ты мог пить чай пока пьешь чай.


    Тут необходимо рассказать, как работает та фиговина, которую я делаю.

    Вы пишите DSL, описывающий будущее расширение PHP, пишите код на K/N с реализацией функций, классов и методов, потом запускаете make и, чудесным образом, получаете готовую библиотеку, которую можно подключать к PHP.

    Сборку можно поделить на 4 этапа:

    1. Создание прослойки между Си и K/N (тот самый cinterop)
    2. Генерация Си-кода расширения
    3. Компиляция библиотеки с логикой
    4. Компиляция целевой библиотеки

    Задача — добавить возможность создавать инстансы PHP-класса в коде K/N. Например, чтобы у класса можно было определить метод getInstance(). Причем сделать хочется так, чтобы это было удобно использовать.

    В Си эта задача решается на раз-два.

    zval *obj = malloc(sizeof(zval));
    object_init_ex(obj, myClass);

    Казалось бы все просто — бери да переноси в K/N, но вот myClass

    А вот myClass — это глобальная переменная типа zend_class_entry*, декларируемая в Си коде проекта и с неизвестным заранее именем.

    Следите за руками. Нужно скомпилировать библиотеку из кода на K/N, в которой будет функция, которой необходимо иметь доступ к myClass, которая определена в сгенерированном, но не скомпилированном Си-коде, из которого потом будет вызываться эта функция.

    В конечном итоге, реализация этого функционала привела к добавлению двух новых артефактов: .h и .kt на этапе кодогенерации, усложнению этапа cinterop и эпичному факапу, про который расскажу в самом конце.

    Глава пятая. Что в имени тебе моем?


    Сказ про то, почему:

    enum class ArgumentType {
        PHP_STRING,
        PHP_LONG,
        PHP_DOUBLE,
        PHP_NULL,
    	...
    }

    лучше, чем:

    enum class ArgumentType {
        STRING,
        LONG,
        DOUBLE,
        NULL,
    	...
    }

    Да тут даже объяснять особо не нужно. Вот во что превращается ArgumentType.NULL в заголовочном файле котлиновской библиотеки:

    struct {
    	extension_kt_kref_php_extension_dsl_ArgumentType (*get)(); /* enum entry for NULL. */
    } NULL;

    И вот как на такое реагирует `gcc`

    /root/simpleExtension/phpmodule/extension_kt_api.h:113:17: error: expected identifier or '(' before 'void'
                   } NULL;
                     ^
    

    Занавес! Следите за именами.

    Глава предпоследняя. Сам себя не похвалишь — никто не похвалит.


    По большому счету, поставленных перед собой целей я достиг. В тему погрузился, «фреймворк» для написания PHP-расширений на Kotlin Native, в целом, готов. Осталась добавить некоторый, не самый критичный, функционал и отполировать.

    Сам проект и, я надеюсь, хорошую документацию к нему, можно посмотреть на гитхабе.

    Что могу сказать про K/N? Только хорошее. Писать на нем одно удовольствие, а мелкие косяки и шероховатости вполне можно списать на то, что он еще даже не выбрался из колыбели :)

    Глава последняя. Лучи добра, без кавычек.


    А вот теперь абсолютно серьезно и с глубоким уважением хочу поблагодарить ребят из JetBrains и резидентов slack-канала Kotlin Native. Вы супер!

    И отдельное спасибо Николаю Иготти.



    Бонус. Эпичный факап.


    Контекст описан в четвертой главе.

    Собственно когда все было дописано до состояния, в котором компилировалось без ошибок, возникла проблема — во время тестирования, PHP открылся мне с совершенно незнакомой ранее стороны.

    # php -dextension=./phpmodule/modules/extension.so -r "var_dump(ExampleClass::getInstance());"
    *RECURSION*
    #
    

    «Фигасе!» — подумал я, полез в исходники PHP и нашел вот такой кусок.

    case IS_OBJECT:
            if (Z_IS_RECURSIVE_P(struc)) {
                PUTS("*RECURSION*\n");
                return;
            }

    Добавление отладки:

    printf("%u", Z_IS_RECURSIVE_P(struc))

    привело к:

    undefined symbol: Z_IS_RECURSIVE_P in Unknown on line 0
    

    «Фигасе!» — снова подумал я.

    На тот момент, когда я догадался взглянуть на реально использующийся на linux-хосте php.h(7.1.8), а не на тот, который утянул с гитхаба из master-бранча(7.3.х), прошли сутки. Прям стыдно.

    Но, как оказалось, дело было не в бобине.

    Корректный код проверки на рекурсию, на всех подконтрольных мне этапах жизни объекта, рапортовал, что все ок и должно работать. А это значит, что стоит внимательно присмотреться к тем местам которые я не контролирую. Таковое нашлось ровно одно — в котором мой объект возвращается функции var_dump

    RETURN_OBJ(
            example_symbols()->kotlin.root.php.extension.proxy.objectToZval(
                example_symbols()->kotlin.root.exampleclass.getInstance(/*не важно*/)
               )
       )

    Раскроем до конца макрос RETURN_OBJ. Уберите от мониторов нервных и беременных!

    1)
    RETURN_OBJ(r)
    2)
    { RETVAL_OBJ(r); return; }
    3)
    { ZVAL_OBJ(return_value, r); return; }
    4)
    { do {                      
        zval *__z = (return_value);                     
        Z_OBJ_P(__z) = (r);                     
        Z_TYPE_INFO_P(__z) = IS_OBJECT_EX;      
    } while (0); return; }
    5)
    { do {                      
        zval *__z = (return_value);                     
        Z_OBJ(*(__z)) = (r);                        
        Z_TYPE_INFO(*(__z)) = (IS_OBJECT | (IS_TYPE_REFCOUNTED << Z_TYPE_FLAGS_SHIFT));     
    } while (0); return; }
    6)
    { do {                      
        zval *__z = (return_value);
        (*(__z)).value.obj = (r);
        (*(__z)).u1.type_info = (8 | ((1<<0) << 8));
    } while (0); return; }

    Вот тут то мне стало стыдно во второй раз. Я, совершенно на голубом глазу, пихал zval* туда, где ждали zend_object* и потратил на поиск ошибки почти два дня.

    Спасибо за внимание, всем Kotlin! :)

    PS. Если найдется добрая душа, которая вычитает мой корявый английский и поправит документацию — благодарности моей не будет предела.

    Альфа-Банк

    188,25

    Компания

    Поделиться публикацией

    Похожие публикации

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое