Skip to content

Folk-Project/folk-builder

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

47 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

folk-builder

Собирает PHP-расширение folk.so из Rust-кода. Folk — это application server для PHP, аналог Swoole/RoadRunner/FrankenPHP, но реализованный как нативное PHP-расширение на Rust.

Что это

folk-builder — CLI-утилита. Она генерирует Cargo-проект с выбранными плагинами и компилирует его в folk.so (cdylib). Результат — файл расширения, который PHP загружает через extension=folk.so.

Установка folk-builder

Через cargo (нужен Rust)

# Установить Rust если нет:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Установить folk-builder:
cargo install folk-builder

Из исходников

git clone https://github.com/Folk-Project/folk-builder
cd folk-builder
cargo build --release
./target/release/folk-builder --help

Требования для сборки folk.so

  • Rust 1.88+rustup update
  • PHP 8.2+ с php-config в PATH
  • clang/libclang — для bindgen (генерация FFI-биндингов к PHP)
  • pkg-config — для поиска PHP headers

На Ubuntu/Debian:

apt install php8.4-dev pkg-config libclang-dev clang

На macOS (Homebrew):

brew install php@8.4

Многопоточность (ZTS)

Для запуска нескольких worker-потоков (count > 1 в folk.toml) PHP должен быть собран с Thread Safety (ZTS):

  • Docker: используйте образ php:8.4-zts — ZTS уже включён
  • Ubuntu: apt install php8.4-zts (если есть в репо)
  • Из исходников: ./configure --enable-zts && make && make install
  • Проверить: php -r "echo PHP_ZTS;" — должно вывести 1

С NTS (обычным) PHP Folk работает, но только с одним worker-потоком.

Сборка расширения

1. Создать конфиг сборки

Файл folk.build.toml:

[build]
output = "folk"                    # Имя расширения (-> folk.so)

[[plugin]]
crate_name = "folk-plugin-http"    # HTTP-сервер (axum + hyper + tokio)
version = "0.2"
config_key = "http"

2. Собрать

folk-builder build --config folk.build.toml --output-dir .

Результат: файл folk.so в текущей директории.

3. Проверить

php -d extension=./folk.so -r "echo folk_version();"
# folk-ext 0.2.0

Запуск Folk-сервера

Конфиг сервера

Файл folk.toml:

[workers]
script = "server.php"    # PHP-скрипт воркера
count = 4                # Кол-во worker-потоков (нужен ZTS)
max_jobs = 100000        # Рестарт после N запросов

[http]
listen = "0.0.0.0:8080"

[log]
filter = "warn"          # trace, debug, info, warn, error

Простой HTTP-сервер (без фреймворка)

<?php
// server.php
require __DIR__ . '/vendor/autoload.php';

use Folk\Sdk\Worker\WorkerLoop;
use Folk\Sdk\Http\HttpModeHandler;
use Folk\Sdk\Http\HttpRequest;
use Folk\Sdk\Http\HttpResponse;

class MyHandler implements HttpModeHandler
{
    public function handle(HttpRequest $request): HttpResponse
    {
        return new HttpResponse(
            status: 200,
            headers: ['Content-Type' => 'application/json'],
            body: json_encode(['status' => 'ok', 'uri' => $request->uri]),
        );
    }
}

// ZTS worker threads уже инициализированы — пропускаем start().
if (!folk_is_worker_thread()) {
    $server = new Folk\Server('folk.toml');
    $server->start();
}

$loop = new WorkerLoop();
$loop->registerHttpHandler(new MyHandler());
$loop->run();

Запуск:

composer require folk/sdk
php -d extension=./folk.so server.php

С Laravel

composer require folk/laravel

Файл server.php:

<?php
require __DIR__ . '/vendor/autoload.php';

if (!folk_is_worker_thread()) {
    $server = new Folk\Server('folk.toml');
    $server->start();
}

// Laravel bootstrap.
$app = require __DIR__ . '/bootstrap/app.php';
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();

// WorkerLoop с Laravel handler (регистрируется через ServiceProvider).
$loop = new \Folk\Sdk\Worker\WorkerLoop();
if (isset($GLOBALS['folk_worker_boot_hook'])) {
    ($GLOBALS['folk_worker_boot_hook'])($loop);
}
$loop->run();
php -d extension=./folk.so server.php
# Сервер слушает на :8080

Docker (рекомендуемый способ)

Не нужно ставить Rust и PHP ZTS локально — всё собирается в Docker.

Продакшен (всё в образе)

# === Сборка folk.so ===
FROM php:8.4-zts AS builder

# Rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
    | sh -s -- -y --default-toolchain 1.88.0
ENV PATH="/root/.cargo/bin:${PATH}"

# Системные зависимости для сборки
RUN apt-get update && apt-get install -y \
    pkg-config libclang-dev clang protobuf-compiler \
    && rm -rf /var/lib/apt/lists/*

# Собираем folk-builder и folk.so
WORKDIR /build
RUN cargo install folk-builder

COPY folk.build.toml folk.build.toml
RUN folk-builder build --config folk.build.toml --output-dir /build/

# === Runtime ===
FROM php:8.4-zts

RUN apt-get update && apt-get install -y \
    unzip curl sqlite3 \
    && pecl install redis \
    && docker-php-ext-enable redis \
    && rm -rf /var/lib/apt/lists/*

# folk.so
COPY --from=builder /build/folk.so /usr/local/lib/php/extensions/folk.so
RUN echo "extension=/usr/local/lib/php/extensions/folk.so" > /usr/local/etc/php/conf.d/folk.ini

# Приложение
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY . .
RUN composer install --no-dev --optimize-autoloader

EXPOSE 8080
CMD ["php", "server.php"]

Локальная разработка (код через volume)

Для разработки код не копируется в образ — монтируется через volume. Образ содержит только runtime + folk.so.

docker/app.Dockerfile:

# Stage 1: Build folk.so
FROM php:8.4-zts AS builder

RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain 1.88.0
ENV PATH="/root/.cargo/bin:${PATH}"

RUN apt-get update && apt-get install -y \
    pkg-config libclang-dev clang protobuf-compiler \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /build
RUN cargo install folk-builder

COPY folk.build.toml folk.build.toml
RUN folk-builder build --config folk.build.toml --output-dir /build/

# Stage 2: PHP ZTS runtime
FROM php:8.4-zts

RUN apt-get update && apt-get install -y \
    unzip curl sqlite3 \
    && pecl install redis \
    && docker-php-ext-enable redis \
    && rm -rf /var/lib/apt/lists/*

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

COPY --from=builder /build/folk.so /usr/local/lib/php/extensions/folk.so
RUN echo "extension=/usr/local/lib/php/extensions/folk.so" > /usr/local/etc/php/conf.d/folk.ini

WORKDIR /app
EXPOSE 8080
CMD ["php", "server.php"]

compose.yaml:

services:
  app:
    build:
      context: .
      dockerfile: docker/app.Dockerfile
    ports:
      - "8080:8080"
      - "9090:9090"
    working_dir: /app
    environment:
      - APP_ENV=local
      - APP_DEBUG=true
      - DB_CONNECTION=sqlite
      - REDIS_HOST=redis
      - CACHE_STORE=redis
      - SESSION_DRIVER=redis
      - QUEUE_CONNECTION=redis
      - RUST_LOG=info
    volumes:
      - ./:/app
    depends_on:
      - redis

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

Запуск:

# Собрать образ (folk.so компилируется один раз)
docker compose build

# Установить PHP-зависимости (выполняется внутри контейнера)
docker compose run --rm app composer require folk/laravel

# Запустить
docker compose up

Код приложения (app/, routes/, config/, resources/) монтируется через volume — изменения применяются при рестарте воркера (по max_jobs или docker compose restart app).

folk.build.toml (все плагины):

[build]
output = "folk"

[[plugin]]
crate_name = "folk-plugin-http"
version = "0.2"
config_key = "http"

[[plugin]]
crate_name = "folk-plugin-jobs"
version = "0.2"
config_key = "jobs"

[[plugin]]
crate_name = "folk-plugin-grpc"
version = "0.2"
config_key = "grpc"

[[plugin]]
crate_name = "folk-plugin-metrics"
version = "0.2"
config_key = "metrics"

[[plugin]]
crate_name = "folk-plugin-process"
version = "0.2"
config_key = "process"

folk.toml:

[server]
shutdown_timeout = "10s"

[workers]
script = "server.php"
count = 4
max_jobs = 100000

[http]
listen = "0.0.0.0:8080"

[jobs]
driver = "memory"

[[jobs.queues]]
name = "default"
concurrency = 2

[metrics]
listen = "0.0.0.0:9090"

[log]
filter = "info"
format = "text"

Конфиг сборки — справочник

[build]

Поле Тип Обязательно Описание
output string да Имя расширения (без .so)
folk_ext_path string нет Путь к локальному folk-ext (для разработки)
folk_api_path string нет Путь к локальному folk-api (для разработки)

[[plugin]]

Поле Тип Обязательно Описание
crate_name string да Имя Rust-крейта
version string нет Версия с crates.io (по умолчанию "0.2")
path string нет Локальный путь (вместо crates.io)
git string нет Git URL (вместо crates.io)
config_key string да Ключ секции в folk.toml

Доступные плагины

Плагин Крейт Версия Описание
HTTP folk-plugin-http 0.2 HTTP/1.1 сервер (axum)
gRPC folk-plugin-grpc 0.2 gRPC сервер (tonic)
Jobs folk-plugin-jobs 0.2 Очередь фоновых задач (memory/redis)
Metrics folk-plugin-metrics 0.2 Prometheus /metrics и /health
Process folk-plugin-process 0.2 Supervisor сайдкар-процессов

Как это работает

PHP загружает folk.so
  |
  +-- Main thread (worker #1)
  |     Rust вызывает PHP handler напрямую через call_user_function
  |     Данные передаются как PHP-массивы (zval), без JSON
  |
  +-- folk-tokio thread
  |     axum HTTP-сервер, пул воркеров, реестр плагинов
  |
  +-- ZTS workers #2..N (если count > 1 и PHP собран с ZTS)
        Каждый — отдельный PHP-контекст со своим Zend Engine

Производительность

Docker, Mac (OrbStack), 4 ZTS workers:

Нагрузка Req/sec
Raw JSON ({"status":"ok"}) ~55,000
Laravel Livewire starter ~4,200

Стабильно держит 500 concurrent connections без socket errors.

Лицензия

MIT

About

Custom binary builder for Folk — generates and compiles a Folk binary with selected plugins from folk.build.toml

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages