Pull to refresh
607.66
Альфа-Банк
Лучший мобильный банк по версии Markswebb

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

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

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

В этой статье будет чуть больше хардкора про интероп Си и 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. Если найдется добрая душа, которая вычитает мой корявый английский и поправит документацию — благодарности моей не будет предела.
Tags:
Hubs:
Total votes 15: ↑15 and ↓0+15
Comments0

Articles

Information

Website
digital.alfabank.ru
Registered
Founded
1990
Employees
over 10,000 employees
Location
Россия