...

вторник, 10 июля 2018 г.

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

Краткое содержание первой части:


  1. Установка и настройка инструментария.
  2. Написание функции helloWorld() на Kotlin Native и компиляция ее в shared library.
  3. Доступ к этой функции из C-кода расширения PHP.

В этой статье я буду рассказывать про создание инструментария для написания расширения PHP без необходимости трогать Си, исключительно на K/N.

Кому интересно — добро пожаловать под кат.
Кому читать не интересно, а просто хочется посмотреть — добро пожаловать на github
В самом начале хочу сказать большое спасибо Николаю Иготти за оперативные и качественные ответы на мои, порой глупые и наивные, вопросы в слак-канале Kotlin Native.

Сразу оговорюсь, что не претендую на создания полноценного фреймворка (может быть потом), потому ограничим функциональность таким образом:

  1. Создание функций, которые можно вызывать из PHP-кода.
  2. Определение констант.
  3. Оперируем только простыми типами PHP: string, boolean, int, floatnull). Никаких массивов, объектом, ресурсов, передач по ссылке и т.д. — ниже расскажу почему.

Специфика разработки расширений PHP состоит в том, что практически весь служебный код и общение с zend engine пишется на макросах. С одной стороны — это сильно облегчает написания расширений на Си, а с другой — сильно мешает делать то же самое на всех остальных языках программирования.

При таких вводных самым очевидным решением было использовать кодогеренарию. И, учитывая, что Kotlin предоставляет очень широкие возможности по созданию DSL, процесс описания структуры расширения можно сделать простым и наглядным.

Для того, чтобы собрать библиотеку расширения классическим образом (phpize, configure, make), необходимы как минимум два артефакта — код расширения на Си и файл config.m4.

Сценарий использования будет таким:

  1. С помощью DSL описываем расширение.
  2. Пишем реализацию функций на K/N.
  3. По описанию генерируем extension.c и config.m4. Код в extencion.c будет заниматься банальным проксированием вызова функций.
  4. По описанию же генерируем constants.kt, что позволит использовать заданные константы в наших функциях на K/N.
  5. Компилируем K/N код в статическую библиотеку.
  6. Собираем все это в одну кучу и компилируем в библиотеку расширения.

Поехали!


Для реализации задуманного нам понадобится получить примерно вот такую структуру:
Расширение(имя, версия)
    Константа1
    Константа2
    ...
    Функция1(имя, возвращаемый тип)
        аргумент1
        аргумент2
        ...
        опциональныйАргумент1
        ...

Думаю, что ни для кого, работавшего с Kotlin, не составит труда написать соответствующий DSL. Для остальных же есть большое количество специализированных статей, где эта тема раскрывается гораздо подробнее, нежели если я попытаюсь сделать это в рамках данной статьи.

Следующим шагом нам надо превратить этот DSL в необходимые артефакты. Для этого напишем генератор на том же K/N, скомпилируем из него и нашего DSL исполняемый файл и запустим — вуаля! Решение не самое изящное, но ничего более простого и надежного пока в голову не пришло.

Ну а дальше все просто — компилируем библиотеку с функциями и штатным образом собираем расширение, включив туда оную.

Для простоты использования, вся магия с компиляциями спрятана в shell-скрипт.

Что из этого получилось


Пример описания и сгенерированный код простого расширения, описанного на этом DSL (для лучшего понимания все аргументы заданы в именованном виде).

konfigure.kt — DSL расширения

import php.extension.dsl.*

val dsl = extension(name = "example", version = "0.1") {
    constant(name = "HELLO_EN", value = "Hello")
    constant(name = "HELLO_ES", value = "Hola")
    constant(name = "HELLO_RU", value = "Привет")

    function(name = "hello", returnType = ArgumentType.STRING) {
        arg(type = ArgumentType.STRING, name = "name")
        arg(type = ArgumentType.STRING, name = "lang", optional = true)
    }
}

fun main(args: Array<String>) = dsl.make()

example.kt — Реализация функций
fun hello(name: String, lang: String?) = "${if (lang ?: "" == "") HELLO_EN else lang} $name!!!\n"

Обратите внимание на странный алгоритм определения значения для `lang`. Это связано с багом в текущей версии K/N, не позволяющем передать в качестве аргумента из Си неинициализированную переменную типа `char *` — приходится передавать пустую строку.

config.m4 — сгенерированный файл
PHP_ARG_ENABLE(example, whether to enable example support,[ --enable-example   Enable hello support])

if test "$PHP_EXAMPLE" != "no"; then
    PHP_ADD_INCLUDE(.)
    PHP_ADD_LIBRARY_WITH_PATH(example_kt, ., EXAMPLE_SHARED_LIBADD)
    PHP_SUBST(EXAMPLE_SHARED_LIBADD)
    PHP_NEW_EXTENSION(example, example.c, $ext_shared)
fi

example_generated_constants.kt — сгенерированный файл с константами Kotlin
const val HELLO_EN = "Hello"
const val HELLO_ES = "Hola"
const val HELLO_RU = "Привет"

example.c — сгенерированный файл с кодом на Си
#include "php.h"
#include "example_kt_api.h"

PHP_FUNCTION(hello);
static zend_function_entry example_functions[] = {
    PHP_FE(hello, NULL)
    {NULL,NULL,NULL}
};

PHP_MINIT_FUNCTION(example);

zend_module_entry example_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
        STANDARD_MODULE_HEADER,
#endif
        "example",
        example_functions,
        PHP_MINIT(example),
        NULL,
        NULL,
        NULL,
        NULL,
#if ZEND_MODULE_API_NO >= 20010901
        "0.1",
#endif
        STANDARD_MODULE_PROPERTIES
};
ZEND_GET_MODULE(example)

PHP_MINIT_FUNCTION(example)
{
    REGISTER_STRING_CONSTANT("HELLO_EN", "Hello", CONST_CS|CONST_PERSISTENT);
    REGISTER_STRING_CONSTANT("HELLO_ES", "Hola", CONST_CS|CONST_PERSISTENT);
    REGISTER_STRING_CONSTANT("HELLO_RU", "Привет", CONST_CS|CONST_PERSISTENT);
    return SUCCESS;
}

PHP_FUNCTION(hello){
//Да-да, все тот же баг с char* в K/N 
    char *name = malloc(1);
    name[0] = '\0';
    size_t name_len=0;


    char *lang = malloc(1);
    lang[0] = '\0';
    size_t lang_len=0;


    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|s", &name, &name_len, &lang, &lang_len) == FAILURE) {
        return;
    }

    RETURN_STRING(example_kt_symbols()->kotlin.root.hello(name, lang));
}

Про то, почему только простые типы


Потому, что они один к одному отображаются в типы Kotlin Native. На сегодняшний момент в проекте реализован, по сути, интероп только в одну сторону, т.е. вызов функций K/N из Си. Для обработки сложных типов, таких как zend_value, zend_class_entry или zend_fcall_info, необходимо импортировать соответствующие структуры в проект K/N и писать соответствующие обертки для работы с ними, а там тоже все на макросах и т.д…

Баночка с дёгтем. Ложка прилагается.


  1. Документация на Kotlin Native. Она, вроде бы, есть, но… Пока самым надежным средством изучения является чтение исходников.
  2. Размер получившегося расширения не то, чтоб мал. Для приведенного выше примера получается библиотека примерно на 500КБ.
  3. Можно даже и не надеяться, что расширения, написанные на K/N, попадут в библиотеку расширений PHP. Продукт получается, так сказать, только для внутреннего пользования.

Что дальше


Реализовать все то, что описано в разделе «Про то, почему только простые типы».

Еще раз ссылка на репозиторий.

Спасибо за внимание, пожелайте мне удачи :)

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий