В первой части рассказываются совсем базовые вещи про настройку инструментария и общие концепции.
Вторая часть про, так сказать, первый подход к снаряду, задумки, наметки, планы.
В этой статье будет чуть больше хардкора про интероп Си и K/N, много макросов, боли, безысходности и «лучей добра». Конечно же будет глава с рассказом о достижениях (сам себя не похвалишь… и в качестве бонуса рассказ о эпичном факапе.
Disclaimer: всё нижеследующее рассматривается в контексте написание библиотеки для PHP.
Про то, как использовать K/N функций в Си описано в первой части цикла. Соответственно тут я расскажу, как использовать функции Си в K/N.
Официальная документация довольно скупа и лаконична, однако, для простых проектов, ее вполне достаточно.
Если вкратце, то надо создать специальный файл с расширением .def и указать в нем необходимые заголовочные файлы.
Потом скормить его программе под названием cinterop.
На выходе вы получите библиотеку libphp.klib, содержащую llvm bitcode и различную мета-информацию.
Дальше можно смело пользоваться описанными в заголовочном файле функциями и макросами (
Но есть нюанс. И не один.
Почему? А потому, что в php.h присутствуют следующие строки:
Тут надо заметить, что компиляцией библиотеки занимается все же llvm, а у него есть ключ -I, а у cinterop есть ключ -copt. Ну вы поняли. В итоге, для компиляции php.h достаточно вот такой команды.
Все, что вам нужно знать про
А потом вспоминаем, что расширение PHP — это макрос на макросе и макросом погоняет и стараемся не расплакаться.
Но не все так плохо. Для обхода подобной ситуации разработчики K/N предусмотрели моток синей изоленты для примотки к def-файлу custom declarations. Выглядит оно таким образом (для примера возьмем макрос
Теперь в коде K/N можно будет использовать функцию
Это «луч добра» в сторону исходников PHP.
Для извлечения настроек предусмотрено 4 макроса:
Где
А теперь давайте, на примере
Уже заметили его «фатальный недостаток»?
Если нет, то подскажу — это функция
Когда вы используете его через прокси-функцию из .def-файла — карета превращается в тыкву, а sizeof(name) возвращает размер указателя. Шах и мат Kotlin Native.
Вариантов обхода, собственно, всего два.
Первый вариант всем лучше второго, кроме одного момента — никто не даст гарантии, что декларация макроса не поменяется. Поэтому, для своего проекта, я, с чувством глубокого неудовлетворения, выбрал второй вариант.
В один прекрасный момент, после приматывания синей изолентой к def-файлу 20-ти очередных прокси-функций, я получил замечательную ошибку.
Комментим половину, пересобираем, если повторилось комментим половину оставшегося, собираем… А учитывая, что процесс компиляции хидеров достаточно долог… (да, так показалось быстрее, чем лазить по десятку исходных файлов и скурпулезно, с лупой, выверять).
Второй «луч добра» уходит в сторону JetBrains.
Получаю в рантайме segmentation fault. Ну ок, бывает. Лезу в отладчик. Эммм… ШТА?
Тут необходимо рассказать, как работает та фиговина, которую я делаю.
Вы пишите DSL, описывающий будущее расширение PHP, пишите код на K/N с реализацией функций, классов и методов, потом запускаете
Сборку можно поделить на 4 этапа:
Задача — добавить возможность создавать инстансы PHP-класса в коде K/N. Например, чтобы у класса можно было определить метод
В Си эта задача решается на раз-два.
Казалось бы все просто — бери да переноси в K/N, но вот
А вот
Следите за руками. Нужно скомпилировать библиотеку из кода на K/N, в которой будет функция, которой необходимо иметь доступ к
В конечном итоге, реализация этого функционала привела к добавлению двух новых артефактов: .h и .kt на этапе кодогенерации, усложнению этапа cinterop и эпичному факапу, про который расскажу в самом конце.
Сказ про то, почему:
лучше, чем:
Да тут даже объяснять особо не нужно. Вот во что превращается
И вот как на такое реагирует `gcc`
Занавес! Следите за именами.
По большому счету, поставленных перед собой целей я достиг. В тему погрузился, «фреймворк» для написания PHP-расширений на Kotlin Native, в целом, готов. Осталась добавить некоторый, не самый критичный, функционал и отполировать.
Сам проект и, я надеюсь, хорошую документацию к нему, можно посмотреть на гитхабе.
Что могу сказать про K/N? Только хорошее. Писать на нем одно удовольствие, а мелкие косяки и шероховатости вполне можно списать на то, что он еще даже не выбрался из колыбели :)
А вот теперь абсолютно серьезно и с глубоким уважением хочу поблагодарить ребят из JetBrains и резидентов slack-канала Kotlin Native. Вы супер!
И отдельное спасибо Николаю Иготти.
Контекст описан в четвертой главе.
Собственно когда все было дописано до состояния, в котором компилировалось без ошибок, возникла проблема — во время тестирования, PHP открылся мне с совершенно незнакомой ранее стороны.
«Фигасе!» — подумал я, полез в исходники PHP и нашел вот такой кусок.
Добавление отладки:
привело к:
«Фигасе!» — снова подумал я.
На тот момент, когда я догадался взглянуть на реально использующийся на linux-хосте php.h(7.1.8), а не на тот, который утянул с гитхаба из master-бранча(7.3.х), прошли сутки. Прям стыдно.
Но, как оказалось, дело было не в бобине.
Корректный код проверки на рекурсию, на всех подконтрольных мне этапах жизни объекта, рапортовал, что все ок и должно работать. А это значит, что стоит внимательно присмотреться к тем местам которые я не контролирую. Таковое нашлось ровно одно — в котором мой объект возвращается функции
Раскроем до конца макрос
Вот тут то мне стало стыдно во второй раз. Я, совершенно на голубом глазу, пихал
Спасибо за внимание, всем Kotlin! :)
PS. Если найдется добрая душа, которая вычитает мой корявый английский и поправит документацию — благодарности моей не будет предела.
Вторая часть про, так сказать, первый подход к снаряду, задумки, наметки, планы.
В этой статье будет чуть больше хардкора про интероп Си и 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 — интероп.
В один прекрасный момент, после приматывания синей изолентой к 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 этапа:
- Создание прослойки между Си и K/N (тот самый
cinterop
) - Генерация Си-кода расширения
- Компиляция библиотеки с логикой
- Компиляция целевой библиотеки
Задача — добавить возможность создавать инстансы 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. Если найдется добрая душа, которая вычитает мой корявый английский и поправит документацию — благодарности моей не будет предела.