Всем доброго дня, уважаемые читатели. В данной статье вы узнаете как добавить новые функции в runtime KPHP.
Совсем вкратце расскажу о том, что такое KPHP и на примере какой задачи вы узнаете о расширении возможностей runtime KPHP.
О KPHP
KPHP — компилируемый PHP. Де-факто PHP, транслированный в C++. В свою очередь это увеличивает производительность исходного кода как минимум потому что скомпилировано в бинарный файл.
О нашей задаче
Задача, которую мы решим, заключается в следующем — реализовать две функции для парсинга строк и файлов в формате ENV. Исключительно для демонстрации всех этапов добавления новых функций в runtime.
Приступаем
Итак, перейдём к нашему плану:
-
Подготовим всё необходимое
-
Добавим новые функции
-
Напишем тесты
-
Проверим работоспособность
Подготовим всё необходимое
Я работаю под Ubuntu 20.04. И для начала нам нужно установить следующее (в том случае, если у вас их нет):
-
git
-
make, cmake
-
g++,
-
python3,
-
pip3
-
php7.4
После установки вышеупомянутых пакетов, необходимо установить vk`шные. А перед тем, надо добавить репозитории:
sudo apt-get update sudo apt-get install -y --no-install-recommends apt-utils ca-certificates gnupg wget sudo wget -qO - https://repo.vkpartner.ru/GPG-KEY.pub | sudo apt-key add - echo "deb https://repo.vkpartner.ru/kphp-focal/ focal main" >> /etc/apt/sources.list
И уже затем установить пакеты:
sudo apt-get update sudo apt install git cmake make g++ gperf python3-minimal python3-jsonschema \ curl-kphp-vk libuber-h3-dev kphp-timelib libfmt-dev libgtest-dev libgmock-dev libre2-dev libpcre3-dev \ libzstd-dev libyaml-cpp-dev libnghttp2-dev zlib1g-dev php7.4-dev libmysqlclient-dev libnuma-dev
Проделав это, переходим к сборке KPHP из исходников:
# Клонируем репозиторий git clone https://github.com/VKCOM/kphp.git # Заходим в папку репозитория cd kphp # Переключаемся на ветку git checkout -b 'pmswga/env_parsing' # Создаём папку build mkdir build # Заходим в папку build cd build # Просим cmake по CMakeLists.txt сотоврить нам чудо cmake .. # Также просим make сотворить чудо make -j6 all
Сборка должна пройти успешно. Что мы получили в итоге? В корне репозитория мы получим новую папку objs и содержимое при ней:
kphp/ ├─ build/ <-- Если вы ещё не вышли из этой папки, то вы тут :) ├─ objs/ │ ├─ bin/ │ │ ├─ kphp2cpp <-- Наш kphp компилятор. Остальное нас не интересует :( │ │ ├─ tl2php │ │ ├─ tl-compiler │ ├─ flex/ │ │ ├─ libvk-flex-data.o │ │ ├─ libvk-flex-data.so │ ├─ generated/* │ ├─ vkext/ │ │ ├─ modules/ │ │ │ ├─ vkext.so │ │ ├─ modules7.4/ │ │ │ ├─ vkext.so
Вот мы и собрали последнюю версию KPHP из исходников. Приготовления завершены, теперь можем переходить к добавлению функций.
Добавим новые функции
Кратко обрисую алгоритм добавления новых функций, в такой схеме:
kphp/ ├─ builin-functions/_functions.txt 1) Добавить интерфейс функции сюда ├─ runtime/ │ ├─ *.h 2) Добавить h-файлы с объявлением функций │ ├─ *.cpp 3) Добавить cpp-файлы с реализацией функций │ ├─ runtime.cmake 4) Добавить имена cpp-файлов в переменную KPHP_RUNTIME_SOURCES
После чего можно смело запускать make и убедиться, что всё добавлено без ошибок и собирается.
Теперь на нашем конкретном примере:
-
В файле _functions.txt добавим интерфейсы функций parse_env_file и parse_env_string. Обратите внимание, на то как указываются типы. В целом всё ясно. Принимают строки, возвращают массивы строк.
function parse_env_file($filename ::: string) ::: string[]; function parse_env_string($env_string ::: string) ::: string[];
-
Добавляем parsing_functions.h со следующим содержимым:
#pragma once #include "runtime/kphp_core.h" #include <regex> #include <fstream> #include <sstream> /* * Cool functions. А именно функции для очистки строк от ненужного */ string clearSpecSymbols(const string &str); string clearSpaces(const string &str); string clearEOL(const string &str); string clearQuotes(const string &str); string clearString(const string &str); string trim(const string &str); /* * The best funtions. * А именно функции, которые проверяют строки по регулярным выражениям * и функции, которые возвращают части одной ENV-записи */ bool isEnvComment(const string &env_comment); bool isEnvVar(const string &env_var); bool isEnvVal(const string &env_val); string get_env_var(const string &env_entry); string get_env_val(const string &env_entry); /* * Env file|string parsing functions. * А именно функции будут подставляться в сгенерированном коде */ array<string> f$parse_env_file(const string &filename); array<string> f$parse_env_string(const string &env_string);
-
Добавляем в parsing_functions.cpp следующий код:
#include "parsing_functions.h" string clearSpecSymbols(const string &str) { return string( std::regex_replace(str.c_str(), std::regex(R"([\t\r\b\v])"), "").c_str() ); } string clearSpaces(const string &str) { return string( std::regex_replace(str.c_str(), std::regex(" += +"), "=").c_str() ); } string clearEOL(const string &str) { return string( std::regex_replace(str.c_str(), std::regex("\\n"), " ").c_str() ); } string clearQuotes(const string &str) { return string( std::regex_replace(str.c_str(), std::regex("[\"\']"), "").c_str() ); } string clearString(const string &str) { string clear_string = clearSpecSymbols(str); clear_string = clearSpaces(clear_string); clear_string = clearQuotes(clear_string); clear_string = trim(clear_string); return clear_string; } string trim(const string &str) { if (str.empty()) { return {}; } size_t s = 0; size_t e = str.size()-1; while (s != e && std::isspace(str[s])) { s++; } while (e != s && std::isspace(str[e])) { e--; } return str.substr(s, (e-s)+1); } /* Example: #APP_NAME=Laravel */ bool isEnvComment(const string &env_comment) { return std::regex_match( env_comment.c_str(), std::regex("^#.*", std::regex::ECMAScript) ); } /* Example: APP_NAME */ bool isEnvVar(const string &env_var) { return std::regex_match( env_var.c_str(), std::regex("^[A-Z]+[A-Z\\W\\d_]*$", std::regex::ECMAScript) ); } /* Example: Laravel */ bool isEnvVal(const string &env_val) { return std::regex_match( env_val.c_str(), std::regex("(.*\n(?=[A-Z])|.*$)", std::regex::ECMAScript) ); } /* Example: APP_NAME=Laravel -> APP_NAME */ string get_env_var(const string &env_entry) { string::size_type pos = env_entry.find_first_of(string("="), 0); if (pos == string::npos) { return {}; } return env_entry.substr(0, pos); } /* Example: APP_NAME=Laravel -> Laravel */ string get_env_val(const string &env_entry) { string::size_type pos = env_entry.find_first_of(string("="), 0); if (pos == string::npos) { return {}; } pos++; return env_entry.substr(pos, env_entry.size() - pos); } /* * Вот собственно реализация parse_env_file */ array<string> f$parse_env_file(const string &filename) { if (filename.empty()) { return {}; } std::ifstream ifs(filename.c_str()); if (!ifs.is_open()) { php_warning("File not open"); return {}; } array<string> res(array_size(1, 0, true)); std::string env_entry; while (getline(ifs, env_entry)) { string env_entry_copy = clearString(string(env_entry.c_str())); if (!env_entry_copy.empty() && !isEnvComment(env_entry_copy)) { string env_var = get_env_var(env_entry_copy); if (env_var.empty()) { php_warning("Invalid env string format %s", env_entry_copy.c_str()); return {}; } string env_val = get_env_val(env_entry_copy); if (isEnvVar(env_var) && isEnvVal(env_val)) { res.set_value(env_var, env_val); } else { php_warning("Invalid env string format %s", env_entry_copy.c_str()); return {}; } } } ifs.close(); return res; } /* * Вот собственно реализация parse_env_string */ array<string> f$parse_env_string(const string &env_string) { if (env_string.empty()) { return {}; } array<string> res(array_size(0, 0, true)); string env_string_copy = clearString(env_string); env_string_copy = clearEOL(env_string_copy); std::stringstream ss(env_string_copy.c_str()); std::string str; while (getline(ss, str, ' ')) { string env_entry = string(str.c_str()); if (!isEnvComment(env_entry)) { string env_var = get_env_var(env_entry); if (env_var.empty()) { php_warning("Invalid env string format %s", env_entry.c_str()); return {}; } string env_val = get_env_val(env_entry); if (isEnvVar(env_var) && isEnvVal(env_val)) { res.set_value(env_var, env_val); } else { php_warning("Invalid env string format %s", env_entry.c_str()); return {}; } } } return res; }
Как видите, для того чтобы функции реально работали в runtime их нужно называть с префиксом f$ в начале. Ибо именно они будут подставляться в сгенерированном коде (позже сами увидите). В остальном, плодите кода столько, сколько хотите 🙂
Поговорим о двух важных вещах — это array<string> и string. Это реализация массивов и строк в самом runtime KPHP, а не std`шная (Сам бы Александр Степанов дал бы по рукам за такие методы как set_value и другие).
array<string> позволяет нам делать ассоциативные и обычные массивы.
string позволяет привести себя в int, float, bool, string.
-
И последнее, добавляем в наш parsing_functions.cpp в cmake файл:
# тут ещё немного cmake prepend(KPHP_RUNTIME_SOURCES ${BASE_DIR}/runtime/ ${KPHP_RUNTIME_DATETIME_SOURCES} ${KPHP_RUNTIME_MEMORY_RESOURCE_SOURCES} ${KPHP_RUNTIME_MSGPACK_SOURCES} ${KPHP_RUNTIME_JOB_WORKERS_SOURCES} ${KPHP_RUNTIME_SPL_SOURCES} ${KPHP_RUNTIME_PDO_SOURCES} ${KPHP_RUNTIME_PDO_MYSQL_SOURCES} allocator.cpp array_functions.cpp bcmath.cpp common_template_instantiations.cpp confdata-functions.cpp confdata-global-manager.cpp confdata-keys.cpp critical_section.cpp curl.cpp exception.cpp files.cpp from-json-processor.cpp instance-cache.cpp instance-copy-processor.cpp inter-process-mutex.cpp interface.cpp json-functions.cpp json-writer.cpp kphp-backtrace.cpp mail.cpp math_functions.cpp mbstring.cpp memcache.cpp memory_usage.cpp migration_php8.cpp misc.cpp mixed.cpp mysql.cpp net_events.cpp on_kphp_warning_callback.cpp openssl.cpp parsing_functions.cpp <-- Наш файл php_assert.cpp profiler.cpp regexp.cpp resumable.cpp rpc.cpp serialize-functions.cpp storage.cpp streams.cpp string.cpp string_buffer.cpp string_cache.cpp string_functions.cpp tl/rpc_tl_query.cpp tl/rpc_response.cpp tl/rpc_server.cpp typed_rpc.cpp uber-h3.cpp udp.cpp url.cpp vkext.cpp vkext_stats.cpp ffi.cpp zlib.cpp zstd.cpp) # и тут ещё немного cmake
Ура! Можно компилировать и проверять работоспособность.
Напишем тесты
Однако, никто же не поверит, что у вас всё работает, если вы не напишите для этого тесты… И никто всерьёз вас не воспримет, когда вы без тестов отправите pull-request. Поэтому приступим.
Что надо знать о тестах?
kphp/ ├─ tests/ │ ├─ cpp/ <---- Здесь cpp тесты │ │ ├─ compiler <---- Тесты компилятора │ │ ├─ runtime <---- Тесты runtime │ │ │ ├─ *.cpp 1) Добавляем cpp файлы тестов │ │ │ ├─ runtime-tests.cmake 2) Добавлем имена cpp файлов в переменную RUNTIME_TESTS_SOURCES │ │ ├─ server <---- Тесты сервера │ ├─ phpt/ <---- Здесь php тесты │ │ ├─ my_folder_with_tests 3) Создаём свою папку для тестов | | | ├─ 001_*.php 4) Создаём свои *.php тесты с нумерацией | ├─ kphp_tester.py 5) Запустить ранее написанные тесты с помощью этого скрипта
CPP тесты написаны с помощью gtest и являются обычными unit-тестами.
Однако, php тесты работают следующим образом. Пишется код на php, в том числе с функциями, которые есть только kphp. Затем они запускаются с помощью kphp_tester.py и выполняются как обычный php код, так и kphp. Затем их результаты сравниваются и делается вывод, тест пройден или нет.
Вопрос вот в чём, откуда обычный php узнает о той же функции parse_env_string и parse_env_file, если их в принципе нет? Для этого нужны php-polyfills (в своём роде заглушки). Далее всё увидите сами.
Для запуска cpp тестов:
# Перейдём в папку build cd build # Соберём всё ещё раз make -j6 all # Запустим тесты ctest -j6
В результате все тесты должны выполниться успешно.
Для запуска php тестов нужно проделать следующее:
# Сначала скачать php-polyfiils git clone https://github.com/VKCOM/kphp-polyfills.git # Зайдём в папку kphp-polyfiils cd kphp-polyfills # Установим пакеты и сгенерируем autoload composer install # Зададим переменную окружения KPHP_TESTS_POLYFIILS_REPO export KPHP_TESTS_POLYFILLS_REPO=$(pwd)
Такая многоходовочка даёт нам следующее, что при запуске php тестов, они будут обращаться по этому пути подтягивать «заглушки».
И вот теперь, действительно, для запуска php тестов:
# Запуск всех тестов tests/kphp_tester.py # Запуск конкретного теста tests/kphp_tester.py 001_*.php
Вуаля!
CPP тесты
Теперь к нашим барашкам (к f$parse_env_string и f$parse_env_file). Добавим parsing-functions-tests.cpp со следующим кодом:
#include <gtest/gtest.h> #include "runtime/parsing_functions.h" TEST(parsing_functions_test, test_isEnvComment) { ASSERT_FALSE(isEnvComment(string(""))); ASSERT_FALSE(isEnvComment(string("APP_NAME=Laravel"))); ASSERT_TRUE(isEnvComment(string("#APP_NAME=Laravel"))); } TEST(parsing_functions_test, test_isEnvVar) { ASSERT_FALSE(isEnvVar(string(""))); ASSERT_FALSE(isEnvVar(string("!APP_NAME"))); ASSERT_TRUE(isEnvVar(string("APP_NAME"))); } TEST(parsing_functions_test, test_isEnvVal) { ASSERT_TRUE(isEnvVal(string(""))); ASSERT_TRUE(isEnvVal(string("true"))); ASSERT_TRUE(isEnvVal(string("local"))); ASSERT_TRUE(isEnvVal(string("80"))); ASSERT_TRUE(isEnvVal(string("127.0.0.1"))); ASSERT_TRUE(isEnvVal(string("https://localhost"))); ASSERT_TRUE(isEnvVal(string("\'This is my env val\'"))); ASSERT_TRUE(isEnvVal(string("\"This is my env val\""))); } TEST(parsing_functions_test, test_get_env_var) { string str("APP_NAME=Laravel"); string env_var = get_env_var(str); ASSERT_STREQ(string("").c_str(), get_env_var(string("")).c_str()); ASSERT_STREQ("APP_NAME", env_var.c_str()); ASSERT_EQ(strlen("APP_NAME"), env_var.size()); } TEST(parsing_functions_test, test_get_env_val) { string str("APP_NAME=Laravel"); string env_val = get_env_val(str); ASSERT_STREQ("Laravel", env_val.c_str()); ASSERT_EQ(string("Laravel").size(), env_val.size()); } /* * Тестируем функцию parse_env_string */ TEST(parsing_functions_test, test_parse_env_string) { string env_string = string(R"(APP_NAME=Laravel APP_ENV=local #APP_KEY=base64:mtlb8hldh5hZ0GlLzbhInsV531MSylspRI4JsmwVal8= APP_DEBUG=true T1="my" T2='my')"); array<string> res(array_size(0, 0, true)); res = f$parse_env_string(env_string); ASSERT_EQ(res.size().string_size, 5); ASSERT_TRUE(res.has_key(string("APP_NAME"))); ASSERT_STREQ(res.get_value(string("APP_NAME")).c_str(), string("Laravel").c_str()); ASSERT_TRUE(res.has_key(string("APP_ENV"))); ASSERT_STREQ(res.get_value(string("APP_ENV")).c_str(), string("local").c_str()); ASSERT_TRUE(res.has_key(string("APP_DEBUG"))); ASSERT_STREQ(res.get_value(string("APP_DEBUG")).c_str(), string("true").c_str()); ASSERT_TRUE(res.has_key(string("T1"))); ASSERT_STREQ(res.get_value(string("T1")).c_str(), string("my").c_str()); ASSERT_TRUE(res.has_key(string("T2"))); ASSERT_STREQ(res.get_value(string("T2")).c_str(), string("my").c_str()); } /* * Тестируем функцию parse_env_file */ TEST(parsing_functions_test, test_parse_env_file) { std::ofstream of(".env.example"); if (of.is_open()) { of << "APP_NAME=Laravel "<< std::endl; of << "APP_ENV=local" << std::endl; of << "APP_DEBUG=true" << std::endl; of.close(); } array<string> res(array_size(0, 0, true)); res = f$parse_env_file(string("file not found")); ASSERT_EQ(res.size().string_size, 0); res = f$parse_env_file(string(".env.example")); ASSERT_TRUE(res.has_key(string("APP_NAME"))); ASSERT_STREQ(res.get_value(string("APP_NAME")).c_str(), string("Laravel").c_str()); ASSERT_TRUE(res.has_key(string("APP_ENV"))); ASSERT_STREQ(res.get_value(string("APP_ENV")).c_str(), string("local").c_str()); ASSERT_TRUE(res.has_key(string("APP_DEBUG"))); ASSERT_STREQ(res.get_value(string("APP_DEBUG")).c_str(), string("true").c_str()); }
Теперь их можно запустить и убедиться, что они успешно выполняются.

PHP тесты
Вспоминаем про php-polyfills, идём в соседний репозиторий, в корне которого находим файл kphp_polyfiils.php добавляем в него следующий код:
#ifndef KPHP # тут много какого-то кода #region env parsing /** * parse_env_string return associative array by parsed string * */ function parse_env_string(string $env_string) { if (empty($env_string)) { return []; } $get_env_entry = function ($env_string) { $env_entry = explode('=', $env_string, 2); if (count($env_entry) !== 2) { die("parse error\n"); } return [ 'env_var' => trim($env_entry[0]), 'env_val' => trim($env_entry[1]) ]; }; $lines = explode(' ', $env_string); $env = []; foreach ($lines as $line) { $env_entry = $get_env_entry($line); $env[trim($env_entry['env_var'])] = trim($env_entry['env_val']); } return $env; } /** * parse_env_string return associative array by parsed file * */ function parse_env_file(string $filename) { if (empty($filename)) { return []; } if (!is_file($filename)) { return []; } if (!file_exists($filename)) { return []; } $env_string = file_get_contents($filename); return parse_env_string($env_string); } #endregion #endif
По существу мы реализовали парсинга env строк и файлов в формате ENV. Что собственно и можно было сделать изначально, создав даже целую либу (kenv).
Теперь же создадим по пути tests/phpt/parsing/001_parsing_env.php и добавим в него следующий код.
@ok # <-- Тег обозначает должен ли этот код компилироваться на KPHP <?php require_once 'kphp_tester_include.php'; # <-- Подключаем php-polyfills function test_parse_env_string_empty() { # <-- Сами "тесты" var_dump(parse_env_string('')); } function test_parse_env_string_one() { var_dump(parse_env_string('APP_NAME=Laravel')); } function test_parse_env_string_many() { var_dump(parse_env_string('APP_NAME=Laravel APP_DEBUG=true APP_ENV=local')); } function test_parse_env_file_empty() { var_dump(parse_env_file('')); } function test_parse_env_file_not_found_empty() { var_dump(parse_env_file('file not found')); } function test_parse_env_file_one() { $filename = tempnam("", "wt"); $fp = fopen($filename, "a"); fwrite($fp, "APP_NAME=Laravel"); fclose($fp); var_dump(parse_env_file($filename)); } function test_parse_env_file_many() { $filename = tempnam("", "wt"); $fp = fopen($filename, "a"); fwrite($fp, "APP_NAME=Laravel"); fwrite($fp, "APP_DEBUG=true"); fwrite($fp, "APP_ENV=local"); fclose($fp); var_dump(parse_env_file($filename)); } test_parse_env_string_empty(); # <-- Вызов функций test_parse_env_string_one(); test_parse_env_string_many(); test_parse_env_file_empty(); test_parse_env_file_not_found_empty(); test_parse_env_file_one(); test_parse_env_file_many();
Запустим написанный тест:
tests/kphp_tester.py 001_parse_env
И вот, заветное слово passed.

Проверяем работоспособность
Итого, вот какие изменения мы внесли, чтобы реализовать функции parse_env_file и parse_env_string.
# Репозиторий kphp kphp/ ├─ builin-functions/_functions.txt <-- Добавили интерфейсы parse_env_file и parse_env_string ├─ runtime/ │ ├─ parsing_functions.h <-- Добавили объявление функций │ ├─ parsing_functions.cpp <-- Добавили реализацию функций │ ├─ runtime.cmake <-- Добавили parsing_functions.cpp в переменную KPHP_RUNTIME_SOURCES ├─ tests/ │ ├─ cpp/ │ │ ├─ runtime │ │ │ ├─ parsing-functions-tests.cpp <-- Добавили cpp тесты │ │ │ ├─ runtime-tests.cmake <-- Добавили parsing-functions-tests.cpp в переменную RUNTIME_TESTS_SOURCES │ ├─ phpt/ │ │ ├─ parsing <-- Создали папку parsing | | | ├─ 001_parse_env.php <-- Добавили php тесты # Репозиторий kphp-polyfills kphp-polyfills/ ├─ kphp_polyfills.php <-- Добавили php`шные реализации parse_env_file и parse_env_string
Теперь можем посмотреть на наши плоды. Создадим index.php и напишем следующий код:
<?php $env_string = "APP_NAME=Laravel APP_DEBUG=true APP_ENV=local"; $res = parse_env_string($env_string); print_r('<pre>'); print_r($res); print_r('</pre>'); $res = parse_env_file('.env.example'); print_r('<pre>'); print_r($res); print_r('</pre>');
Скомпилируем его:
./kphp2cpp index.php
Запустим и получим следующее:
./kphp_out/server -H 8000 -f 2 ^ ^ | | | Поднимаем двух рабочих работу работать | Поднимаем localhost:8000

А вот что сгенерировано на С++:
//crc64:912a10e8beed9098 //crc64_with_comments:bc9f187534f26ce2 #include "runtime-headers.h" #include "o_6/src_indexbccbb8a09559268e.h" extern string v$env_string; extern array< string > v$res; extern bool v$src_indexbccbb8a09559268e$called; extern string v$const_string$us3e8066aa5eeccc54; extern string v$const_string$us531c70314bd2d991; extern string v$const_string$usd04f12c090cf2e22; extern string v$const_string$use301963cf43e4d3a; //source = [index.php] //3: $env_string = "APP_NAME=Laravel APP_DEBUG=true APP_ENV=local"; Optional < bool > f$src_indexbccbb8a09559268e() noexcept { v$src_indexbccbb8a09559268e$called = true; v$env_string = v$const_string$us3e8066aa5eeccc54; //4: //5: $res = parse_env_string($env_string); v$res = f$parse_env_string(v$env_string); <-- Вот вызов нашей функции //6: //7: print_r('<pre>'); f$print_r(v$const_string$usd04f12c090cf2e22); //8: print_r($res); f$print_r(v$res); //9: print_r('</pre>'); f$print_r(v$const_string$us531c70314bd2d991); //10: //11: //12: $res = parse_env_file('.env.example'); v$res = f$parse_env_file(v$const_string$use301963cf43e4d3a); <-- И вот вызов нашей функции //13: //14: print_r('<pre>'); f$print_r(v$const_string$usd04f12c090cf2e22); //15: print_r($res); f$print_r(v$res); //16: f$print_r(v$const_string$us531c70314bd2d991); return Optional<bool>{}; }
Заключение
Спасибо всем кто дочитал до конца. Надеюсь что поставленная цель выполнена.
Если вы хотите присоединиться к сообществу KPHP, то добро пожаловать в чат.
По изложенной теме:
Дополнительные ссылки:
ссылка на оригинал статьи https://habr.com/ru/post/701216/
Добавить комментарий