
В первой части рассказываются совсем базовые вещи про настройку инструментария и общие концепции.
Вторая часть про, так сказать, первый подход к снаряду, задумки, наметки, планы.
В этой статье будет чуть больше хардкора про интероп Си и 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/
Добавить комментарий