В этой статье рассматривается самый наивный и простой подход к созданию расширения PHP с использованием Kotlin Native. Обращаю внимание, что не на, а с использованием.
Это скорее некий туториал с описанием возникших при скрещивании ужа с ежом проблем и путей их решения. Откровений не будет, но возможно кому-то и пригодится.
Итак, если интересно, то добро пожаловать под кат.
Задача — написать расширение с одной функцией `hello($name)`, принимающей строку и печатающей `Hello, $name!`.
Решать будем наивно, как и сказано в названии.
- Напишем функцию на Kotlin
- Скомпилируем в shared library
- Классическим образом (на C) напишем расширение, которое будет переадресовывать вызов функции в эту библиотеку
Вроде просто, но в густой траве уже затаились грабли:
- Есть некоторое количество примеров использования C-библиотек в Kotlin, но вот про то, как использовать функции Kotlin-библиотеки в C мне ничего адекватного найти не удалось (ну может плохо искал, чего уж)
- Компилятор kotlinc при первом запуске загружает зависимости. Но вот незадача — моя линуксовая виртуалка в корпоративном облаке. Без, даже теоретической, возможности достучаться в интернет.
- Эти грабли скорее от отсутствия опыта в С, но все равно расскажу как меня знатно троллил линковщик.
Все происходило на 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, минимизация использования С и, чем черт не шутит, мини фреймворк — такие дела. Пожелайте мне удачи! :)
Автор: rjhdby