Расширение 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, в целом, готов. Осталась добавить некоторый, не самый критичный, функционал и отполировать.

Сам проект и, я надеюсь, хорошую документацию к нему, можно посмотреть на гитхабе — https://github.com/rjhdby/kotlin-native-php-extension

Что могу сказать про 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 Если найдется добрая душа, которая вычитает мой корявый английский и поправит документацию — благодарности моей не будет предела.


ссылка на оригинал статьи https://habr.com/post/423145/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *