В этой статье рассматривается самый наивный и простой подход к созданию расширения PHP с использованием Kotlin Native. Обращаю внимание, что не на, а с использованием.

Это скорее некий туториал с описанием возникших при скрещивании ужа с ежом проблем и путей их решения. Откровений не будет, но возможно кому-то и пригодится.

Итак, если интересно, то добро пожаловать под кат.

Задача — написать расширение с одной функцией `hello($name)`, принимающей строку и печатающей `Hello, $name!`.

Решать будем наивно, как и сказано в названии.

  1. Напишем функцию на Kotlin
  2. Скомпилируем в shared library
  3. Классическим образом (на C) напишем расширение, которое будет переадресовывать вызов функции в эту библиотеку

Вроде просто, но в густой траве уже затаились грабли:

  1. Есть некоторое количество примеров использования C-библиотек в Kotlin, но вот про то, как использовать функции Kotlin-библиотеки в C мне ничего адекватного найти не удалось (ну может плохо искал, чего уж)
  2. Компилятор kotlinc при первом запуске загружает зависимости. Но вот незадача — моя линуксовая виртуалка в корпоративном облаке. Без, даже теоретической, возможности достучаться в интернет.
  3. Эти грабли скорее от отсутствия опыта в С, но все равно расскажу как меня знатно троллил линковщик.

Все происходило на Red Hat Enterprise Linux Server release 7.5.

Поехали!


Для начала устанавливаем… Нет, не компилятор Kotlin. Для начала устанавливаем JDK. Ну вот так.
Потом устанавливаем компилятор Kotlin. Просто скачиваем архив с гитхаба и разархивируем, например, в домашний каталог.

В идеальном мире, при первом запуске, он сам скачает все зависимости, но, в случае отсутствия интернета, действуем следующим образом (тайное знание добыто в слаке у сотрудников JetBrains):

  • Создаем любой простейший скрипт на Kotlin'е, чтобы было что подсунуть компилятору на следующем шаге
  • Запускаем $KOTLIN_HOME/bin/kotlinc SimpleScript.kt, немного ждем и жмем CTRL+C — это для того, чтобы создалась структура папок в домашнем каталоге
  • Смотрим в файл $KOTLIN_HOME/konan/konan.properties, в секцию с нашей архитектурой, и ищем пункт:

dependencies.linux_x64 = 	    clang-llvm-5.0.0-linux-x86-64 	    target-gcc-toolchain-3-linux-x86-64 	    libffi-3.2.1-2-linux-x86-64

  • Идем в специальный репозиторий и скачиваем все вышеперечисленное.
  • Складываем все это в ~/.konan/cache (напоминаю, что тильдой в Linux обозначается домашний каталог)

Теперь при первом запуске компилятор воспользуется этими дистрибутивами и в интернет не полезет.

Учтите, что зависимости очень не маленькие по размеру и, после их установки, мой домашний каталог потяжелел на 3.4ГБ. Для тех, у кого под домашний нарезан отдельный том, может быть критично.

После, стандартным пакетным менеджером, устанавливаем php и соответствующий ему php-devel.
На этом подготовительные мероприятия закончены.

Так как задачи рассказать про написание PHP расширений не стоит, то обойдемся максимально коротким кодом.

Начнем с Kotlin


hellokt.kt

fun kt_print(string:String){
    println("Hello, $string!!!")
}

В различных мануалах и туториалах в качестве компиляторов используются либо kotlinc, либо konanc, что несколько запутывает. Так вот — это одно и то же. Вот пруф:


Компилируем

# $KOTLINC_HOME/kotlinc -opt ./hellokt.kt -o hellokt -produce dynamic

С ключем -opt библиотека получается поменьше. -produce dynamic говорит компилятору, что нужно делать shared library для текущей платформы.

После выполнения у нас появятся два файла: libhellokt.so и hellokt_api.h. Библиотека и заголовочный файл для нее. Обратите внимание, что префиксы и суффиксы генерируются автоматом и повлиять на них мы не можем (наверное).

В нашем случае получится вот такой заголовочный файл.

#ifndef KONAN_HELLOKT_H
#define KONAN_HELLOKT_H
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
typedef bool            hellokt_KBoolean;
#else
typedef _Bool           hellokt_KBoolean;
#endif
typedef char            hellokt_KByte;
typedef unsigned short  hellokt_KChar;
typedef short           hellokt_KShort;
typedef int             hellokt_KInt;
typedef long long       hellokt_KLong;
typedef float           hellokt_KFloat;
typedef double          hellokt_KDouble;
typedef void*           hellokt_KNativePtr;
struct hellokt_KType;
typedef struct hellokt_KType hellokt_KType;

typedef struct {
  /* Service functions. */
  void (*DisposeStablePointer)(hellokt_KNativePtr ptr);
  void (*DisposeString)(const char* string);
  hellokt_KBoolean (*IsInstance)(hellokt_KNativePtr ref, const hellokt_KType* type);

  /* User functions. */
  struct {
    struct {
      void (*kt_print)(const char* string);
    } root;
  } kotlin;
} hellokt_ExportedSymbols;
extern hellokt_ExportedSymbols* hellokt_symbols(void);
#ifdef __cplusplus
}  /* extern "C" */
#endif
#endif  /* KONAN_HELLOKT_H */

Доступ к нашей функции kt_print пойдет по такому пути

hellokt_symbols()->kotlin.root.kt_print(char *);

Про классы и пакеты расскажу ниже — там есть нюансы.

Библиотека готова, переходим к C


config.m4 (как его создавать)

PHP_ARG_ENABLE(hello, whether to enable hello support,[ --enable-hello   Enable hello support])

if test "$PHP_HELLO" != "no"; then
    PHP_ADD_LIBRARY_WITH_PATH(hellokt, /path/to/compiled/library, HELLO_SHARED_LIBADD)
    PHP_NEW_EXTENSION(hello, hello.c, $ext_shared)
    PHP_SUBST(HELLO_SHARED_LIBADD)
fi

Первая строка обязательна!
The very first thing seen in the example config.m4 above, aside from a couple of comments, are three lines using PHP_ARG_WITH() and PHP_ARG_ENABLE().

Every extension should provide at least one or the other with the extension name, so that users can choose whether or not to build the extension into PHP.

Обратите внимание, что в PHP_ADD_LIBRARY_WITH_PATH первым аргументом указывается имя библиотеки. Не libhellokt, а именно hellokt. Два часа угробил, пока нашел, почему ld не может библиотеку найти. (вот он обещанный рассказ про издевательства линкера).

Теперь, собственно, сам код расширения

hello.c

#include "php.h"
//Заголовочный файл нашей библиотеки
#include "hellokt_api.h"    

#define PHP_MY_EXTENSION_VERSION "1.0"
#define PHP_MY_EXTENSION_EXTNAME "hello"

PHP_FUNCTION(hello);

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

zend_module_entry hello_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
        STANDARD_MODULE_HEADER,
#endif
        PHP_MY_EXTENSION_EXTNAME,
        hello_functions,
        NULL,
        NULL,
        NULL,
        NULL,
        NULL,
#if ZEND_MODULE_API_NO >= 20010901
        PHP_MY_EXTENSION_VERSION,
#endif
        STANDARD_MODULE_PROPERTIES
};

ZEND_GET_MODULE(hello)

PHP_FUNCTION(hello) {
        char * name;
        size_t name_len;

        if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &name, &name_len) == FAILURE) {
            RETURN_NULL();
        }

        hellokt_symbols()->kotlin.root.kt_print(name); //Вызов библиотечной функции

        efree(name);
}

Про написание расширений PHP довольно много статей, да и документация присутствует, так что ограничусь ссылкой на использование zend_parse_parameters — оно будет к месту.

Ну а дальше все по накатанному пути:

# $PHP_PATH/phpize
# ./configure  --with-php-config=$PHP_PATH/php-config
# make
# $PHP_PATH/php -dextension=./modules/hello.so -r "echo hello('World');"
Hello, World!!!

Бонус, про классы и пакеты


Допустим мы захотели сделать по феншую и слепили вот такой код.

package hello.kt;
public class HelloKt {
  fun kt_print(string: String) {
      println("Hello, $string!!!")
  }
}

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

hellokt_kref_hello_kt_HelloKt helloKt = { 0 };

if(!helloKt.pinned){
        helloKt = hellokt_symbols()->kotlin.root.hello.kt.HelloKt.HelloKt();
}

hellokt_symbols()->kotlin.root.hello.kt.HelloKt.kt_print(helloKt, name);

Что дальше? Отказ от shared library, минимизация использования С и, чем черт не шутит, мини фреймворк — такие дела. Пожелайте мне удачи! :)

Комментарии (34)


  1. pewpew
    28.06.2018 10:48

    [offtopic]Шутка про мсье и про трллейбус.[/offtopic]

    Пожелайте мне удачи! :)

    Удачи! Вы — странный, но быть странным лучше, чем как все.
    И всё же… В заголовке сказано, что это часть первая. Но вроде бы всё целостно. Оптимизации — это уже рутина.


    1. rjhdby Автор
      28.06.2018 11:05

      В планах на вторую часть предоставить полноценный туториал/инструментарий для написания расширений на Kotlin Native без порождения лишних артефактов и погружения в С. Пока задача видится не самой простой, потому про «пожелайте удачи» и упомянул


  1. staticlab
    28.06.2018 11:15

    Если всё равно писать на Котлине, то зачем тогда ПХП? Не проще ли сразу на Котлине весь сервер написать?


    1. rjhdby Автор
      28.06.2018 11:18

      У этих языков, по большому счету, разные области применения. Вас же не смущает, что расширения для PHP, в 99.9% случаев, пишутся на С? А Kotlin чем хуже?


      1. staticlab
        28.06.2018 11:20

        Дело в том, что и ПХП, и Котлин подходят для написания веб-приложений, а вот на Си это делать очень неудобно и сложно. Но расширения на нём писать выгодно из-за производительности и возможности интеграции с внешними библиотеками.


        1. rjhdby Автор
          28.06.2018 11:35

          Собственно вы очень здорово сформулировали ответ на вопрос «зачем я это делаю?» (отчасти)

          на Си это делать очень неудобно и сложно


          Kotlin Native, так же как и Си, генерирует нативный код и интегрируется с внешними библиотеками, но, при этом, на порядок более удобен и прост. За производительность пока ничего не скажу — надо тестировать.


          1. staticlab
            28.06.2018 11:51

            Настолько хорошо, что прокладку между котлином и пхп всё равно приходится писать на си? :)


            1. VolCh
              28.06.2018 12:01

              Прокладка нужна не для интеграции Котлина с внешними библиотеками, а для интеграции PHP с внешними библиотеками, на каком бы языке они не были написаны. Возможно скоро PHP интегрировать с внешними библиотеками можно будет на самом PHP, без использования таких прокладок.


            1. rjhdby Автор
              28.06.2018 12:02

              Тут есть два стремных момента:
              1) Расширения PHP, в конечном итоге, должны реализовывать некий «интерфейс», описания которого в открытом доступе я не нашел. То API, которое генерирует Kotlin, прямо вот вообще не дает надежды на полный отказ от Си.
              2) Специфика написания расширений PHP такова, что вся оснастка взаимодействия с Zend engine пишется макросами чуть менее, чем на 100%

              Ну и в целом, я отношусь к этой активности как к интересной задаче и возможности поизучать Kotlin Native. :)


              1. VolCh
                28.06.2018 12:38

                Ну вот вроде в PHP 8.0 будет реализован FFI, позволяющий обращаться к библиотекам, предоставляющим обычный для C интерфейс вызова.


                1. rjhdby Автор
                  28.06.2018 13:11

                  Не факт, что будет. Опять же когда его ждать? По сегодняшним прогнозам это конец 21-го года.


  1. untilx
    28.06.2018 16:06

    Хм. А зачем для этого использовать Kotlin, когда есть Zephir?


    1. rjhdby Автор
      28.06.2018 16:19

      Ответ «потому, что мне это интересно» подойдет? ;)


      1. untilx
        28.06.2018 16:35
        +1

        Вполне. Мсье знает толк…


    1. Fesor
      29.06.2018 00:43
      +1

      Потому что писать на котлине веселей при том что результат примерно одинаков?


      1. rjhdby Автор
        29.06.2018 00:55

        Bingo!
        Ну и кроме прочего, всегда импонировал подход «а еще это можно сделать вот так»


      1. untilx
        29.06.2018 13:43

        Синтаксис на любителя, переключение контекста, необходимость лезть в сишный код… Ну, так себе. Зефир лишён этих недостатков.


        1. Fesor
          29.06.2018 14:39

          Синтаксис на любителя

          сказал phpник


          переключение контекста

          в реальности незначительно, если рассматривать зачем вообще такие штуки делают.


          необходимость лезть в сишный код

          написать кодогенератор и вжух. будет получше чем зефир.


          1. untilx
            29.06.2018 15:15

            сказал phpник

            А ещё питонщик, рубист, эликсирщик и ещё кое-кто, в том числе сишник и 6502-ассемблерщик. И это всё не разные люди, но к делу не имеет совершенно никакого отношения.

            в реальности незначительно, если рассматривать зачем вообще такие штуки делают.

            Ну так-то, если проект на php, то с практической точки зрения, гораздо ценнее возможность писать расширения на языке, максимально приближенном к нему.


            1. VolCh
              29.06.2018 16:25

              С практической точки зрения, если на проекте на php дошло до написания расширений, то должна быть гораздо ценнее возможность писать эффективно эффективный код, чем приближенность.


              1. untilx
                29.06.2018 16:39

                Ну, тогда точно не нужен Kotlin, потому что полно более лаконичных языков без дурной наследственности java: хоть те же Rust или Golang.


                1. Fesor
                  30.06.2018 09:47

                  а можно чуть больше аргументов? вы же понимаете что у вас уже может быть существующая система на Kotlin а вам просто нужно интегрировать что-то в проект. Или много других кейсов где нужен interop между php и kotlin.

                  Мне нравится rust (а вот на go я бы не стал писать расширения — специфика языка не подразумевает этого) для написания экстеншенов для php но сегодня очень маленький процент бэкэнд проектов реализуется на rust в той мере что бы имело смысл организовывать interop между php и rust.


        1. rjhdby Автор
          29.06.2018 15:17

          Не поймите превратно, но вот решил я, после вашего поста, попробовать zephir.
          Первое, что насторожило — первая не альфа версия датируется 2015 годом, а текущая версия 0.10.10. Т.е. сами создатели не готовы заявить о стабильном релизе, за который они ручаются.
          Второе. Начал устанавливать по инструкции — ругается «Note: Zephir no longer distributed with internal parser.». Еще раз — официальная дока не точна начиная с раздела «Install» — могу ли я ей после такого доверять в остальном? Не самый простительный косяк.

          Ну и по сообщению агенства «Одна Бабка Сказала» вспоминаются не совсем лесные отзывы об итоговой производительности. Вполне осознаю, что это не аргумент — надо самому мерить (попробуйте найти хоть один бенчмарк зефира в сравнении с голым PHP7, ага), но, как в том анекдоте — «осадок остался».


          1. untilx
            29.06.2018 15:27

            Т.е. сами создатели не готовы заявить о стабильном релизе, за который они ручаются

            Тем не менее, это им не мешает разрабатывать на нём Phalcon Framework, для которого, собственно, и был создан Зефир.

            Начал устанавливать по инструкции — ругается «Note: Zephir no longer distributed with internal parser.».

            Не ругается, а сообщает о том, где найти парсер зефира для php, если вдруг случится такое, что он нужен. Это совсем не значит, что что-то пошло не так и в документации ошибка.


            1. rjhdby Автор
              29.06.2018 15:51

              Тем не менее, это им не мешает разрабатывать на нём Phalcon Framework, для которого, собственно, и был создан Зефир.
              О чем тогда вообще можно говорить? Завязываться на технологию, которая пилится под один конкретный проект — это не самая лучшая идея.
              Не ругается, а сообщает о том, где найти парсер зефира для php, если вдруг случится такое, что он нужен. Это совсем не значит, что что-то пошло не так и в документации ошибка.
              Нет уж простите. Если я иду четко по гайду, с самого первого шага, ничего не пропуская и, на очередном шаге, все ломается (не по моей вине), то это именно вина гайда.


              1. untilx
                29.06.2018 16:23

                Нет уж простите. Если я иду четко по гайду, с самого первого шага, ничего не пропуская и, на очередном шаге, все ломается (не по моей вине), то это именно вина гайда.

                Специально сейчас обновился по инструкции. Даже этого сообщения не было. Но, как я уже сказал, оно не означает ошибку. Убедиться в этом можно классическим методом: zephir help.


                1. rjhdby Автор
                  29.06.2018 16:38

                  А при обновлении оно и не должно ругаться, зависимости то уже установлены.

                  Вы посмотрите на это с точки зрения человека, который ничего не знает о технологии и пытается, для начала, написать «hello world», пользуясь официальным гайдом.

                  Вот, что написано в гайде:
                  $ zephir build
                  ...
                  Extension installed!
                  Add extension=utils.so to your php.ini
                  Don't forget to restart your web server


                  Но вместо «Extension installed!» человек видит невнятный стектрейс. Все еще считаете, что ошибки в документации нет?


                  1. untilx
                    29.06.2018 21:50

                    Вот это уже другой разговор. Изначально про установку писали, а тут, оказывается, при сборке проекта. Да, есть такая штука. Когда я в прошлый раз с ним работал, такого ещё не было, либо было, но я по привычке просто поставил и забыл. PR в документацию сделаете или я сделаю?


                    1. rjhdby Автор
                      29.06.2018 22:31

                      Уж я не знаю, каким образом вы с этим проектом связаны — видно, что он вам сильно не безразличен, но давайте все же соблюдать некоторые этические нормы. Вы достаточно агрессивно пытаетесь меня убедить, что мне стоит перестать «заниматься фигней» и начать использовать вашего фаворита, вплоть до того, что пытаетесь подписать делать в него PR. Смотрится это… странно.
                      Я ничего не имею против Zephir, но и особого интереса к его развитию у меня, если честно, нет.
                      Давайте все же уважать желания друг друга заниматься тем, что нам кажется более интересны и полезным, хорошо?


              1. VolCh
                29.06.2018 16:27

                > Завязываться на технологию, которая пилится под один конкретный проект — это не самая лучшая идея.

                Между «изначально создана для» и «пилится под» всё же есть разница. Вы же завязались на язык, созданный для размещения резюме? :)


                1. rjhdby Автор
                  29.06.2018 16:41

                  Мне кажется, что мы попали в какой то порочный круг.
                  Я: За N лет ни одного стабильного релиза. Последний 0.10.10
                  Мне: Так они его для себя пишут
                  Я: Ну ок. Раз для себя, то я пожалуй мимо пройду
                  Мне: Да нет же! Сейчас-то для всех!
                  GOTO 1 :D

                  При этом я не имею ничего против Zephir'а, я просто пытаюсь понять, зачем мне его так активно навязывают в статье про Kotlin Native


  1. POPSuL
    29.06.2018 07:15

    Еще было бы классно сделать автоматическую генерацию расширения для PHP, чтобы не приходилось писать руками эту прокладку. ;)


    1. rjhdby Автор
      29.06.2018 08:24

      Да, конечно. Это же главный критерий успешности всех этих упражнений. :)


  1. rjhdby Автор
    29.06.2018 08:22

    .