Skip to content

RFC-021: Расширение FFI с управлением через библиотеки и поддержка межъязыковых вызовов

⚠️ Отклонён: Данный документ отклонён, содержимое объединено в RFC-026: Основной механизм FFI.

См. также:

Краткое описание

В данном документе предлагается схема расширения FFI (Foreign Function Interface, внешний интерфейс функций), управляемого через библиотеки. Единственной точкой входа в FFI является объявление native("symbol") + таблица регистрации FfiRegistry во время выполнения; в ядре не вводится второй набор механизмов. Поверх этого стандартная библиотека обеспечивает возможность динамической загрузки библиотек, межъязыковые вызовы и связывание. Связывание для конкретных языков (таких как C, Python, JavaScript) генерируется автоматически официальным инструментарием или создаётся по мере необходимости в рамках проектов.

Мотивация

Недостатки текущей реализации

Текущая реализация FFI уже обеспечивает следующие возможности:

  • Синтаксис объявления внешних функций native("symbol")
  • Таблица регистрации функций FfiRegistry

Однако функциональность относительно ограничена:

  • Отсутствует поддержка динамической загрузки библиотек
  • Нет инфраструктуры для межъязыковых вызовов
  • Отсутствуют инструменты автоматической генерации привязок

Философия проектирования

YaoXiang придерживается принципа «Ядро простое, сложность опускается в библиотеки»:

Хороший вкус (Good Taste): Язык должен предоставлять атомарные возможности, а не универсальный функциональный набор. Сложность следует решать через библиотеки, а не накапливать в компиляторе.

Поэтому данная схема:

  • Нулевые синтаксические изменения — полная обратная совместимость, единственной точкой входа в FFI остаётся native("symbol")
  • Библиотека как язык — функциональность расширяется через стандартную библиотеку
  • Автоматизация инструментарием — привязки автоматически генерируются yx-bindgen, а не поддерживаются вручную
  • Постепенное улучшение — разработчики подключают функциональность по мере необходимости

Предложение

1. Расширение основной библиотеки FFI

Расширение модуля std.ffi. Примечание: все вызовы внешних функций по-прежнему осуществляются через объявление native("symbol"), std.ffi предоставляет только вспомогательные возможности.

1.1 Динамическая загрузка библиотек

yaoxiang
import ffi

# Загрузка динамической библиотеки (.so/.dll/.dylib)
lib = ffi.load_library("./libmyext.so")

# Получение символов функций из библиотеки, возврат имени символа, доступного для native
ffi.register_library_symbols(lib, [
    "my_function",
    "another_func",
])

load_library возвращает дескриптор DynamicLibrary, register_library_symbols регистрирует имена символов в таблице известных символов FfiRegistry. После этого пользователь по-прежнему использует объявление через native:

yaoxiang
my_func: (a: Int, b: Int) -> Int = native("my_function")

Никакого второго синтаксиса вызовов, никаких обёрток try_call.

1.2 Управление библиотеками

yaoxiang
# Список загруженных библиотек
loaded = ffi.loaded_libraries()

# Выгрузка библиотеки
ffi.unload_library(lib)

# Проверка версии библиотеки
ffi.check_version(lib, "1.0.0")

1.3 Разрешение символов

yaoxiang
# Поиск символа по имени (возвращает структуру Symbol)
sin_sym = ffi.dlsym("libm.so", "sin")

Соглашения о межъязыковых вызовах и преобразование типов не обрабатываются во время выполнения через универсальные обёртки, а генерируются yx-bindgen на этапе компиляции.

2. Реализация динамической загрузки библиотек

2.1 Основные структуры данных

rust
pub struct DynamicLibrary {
    handle: *mut std::ffi::c_void,
    path: String,
}

impl DynamicLibrary {
    pub fn load(path: &str) -> Result<Self, FfiError>;
    pub fn get_symbol(&self, name: &str) -> Result<*mut std::ffi::c_void, FfiError>;
    pub fn unload(self) -> Result<(), FfiError>;
}

2.2 Типы ошибок

rust
pub enum FfiError {
    LibraryNotFound { name: String, os_error: Option<OsError> },
    SymbolNotFound { name: String, os_error: Option<OsError> },
    CallFailed { message: String, os_error: Option<OsError> },
    Timeout,
}

pub struct OsError {
    pub code: i32,
    pub message: String,
}

OsError содержит коды ошибок платформы (dlerror() в Linux, GetLastError() в Windows), обеспечивая отладку.

3. Привязки для нескольких языков: решение через инструментарий

Отказ от «фантазии» о том, что «сообщество языков будет поддерживать библиотеки привязок». Вместо этого — автоматическая генерация официальным инструментарием.

3.1 Архитектурный дизайн

┌───────────────────────────────────────────────┐
│  Код YaoXiang                                 │
│                                               │
│  // Пользователь пишет только объявление native│
│  my_func: (a: Int) -> Int = native("my_func") │
└───────────────────────────────────────────────┘
         ↑                          ↑
         │  Время компиляции         │  Время выполнения
┌──────────────────┐   ┌────────────────────────┐
│  yx-bindgen       │   │  std.ffi + FfiRegistry  │
│  (C header → .yx) │   │  - dlopen/dlsym         │
│                   │   │  - LoadLibrary/GetProc  │
└──────────────────┘   └────────────────────────┘

3.2 Генератор привязок (yx-bindgen)

yx-bindgen — это автономный CLI-инструмент, который генерирует код привязок YaoXiang FFI из заголовочных файлов C:

bash
yx-bindgen --header /usr/include/sqlite3.h --output sqlite3.yx

Пример сгенерированного кода:

yaoxiang
# Автогенерация, не редактировать вручную
# Source: /usr/include/sqlite3.h

sqlite3_open: (filename: *const u8, ppDb: *mut *mut opaque) -> Int
    = native("sqlite3_open")

sqlite3_close: (db: *mut opaque) -> Int
    = native("sqlite3_close")

sqlite3_exec: (
    db: *mut opaque,
    sql: *const u8,
    callback: *mut opaque,
    arg: *mut opaque,
    errmsg: *mut *mut u8,
) -> Int
    = native("sqlite3_exec")

yx-bindgen поддерживается официально, что гарантирует:

  • Полноту отображения типов (intInt, char**const u8, void**mut opaque)
  • Выравнивание структур в памяти (автоматическая #[repr(C)] и её эквиваленты)
  • Преобразование сигнатур callback-функций

3.3 Официально поддерживаемые пакеты привязок

Команда YaoXiang не берёт на себя обязательств по поддержке универсальных библиотек привязок для всех языков, но предоставляет официальный пакет привязок libc (подмножество POSIX + Windows API) в качестве примера лучших практик FFI и базовых возможностей.

Привязки для других языков и библиотек:

  • Генерируются самостоятельно с помощью yx-bindgen
  • Могут быть опубликованы как пакеты YaoXiang (например, libsqlite3, libcurl, libsdl2)
  • Команда ядра не несёт ответственности за поддержку, но предоставляет механизм публикации и управления версиями пакетов

4. Уровень преобразования типов

4.1 Преобразование типов на этапе компиляции

Преобразование типов выполняется не через обёртки времени выполнения, а статически определяется при генерации yx-bindgen:

Тип CТип YaoXiangСпособ преобразования
intIntПрямая передача значения
char**const u8Передача указателя
void**mut opaqueНепрозрачный указатель
struct Textern struct TСовпадение раскладки в памяти
int**mut IntПередача указателя (изменяемый)
const int**const IntПередача указателя (только чтение)

4.2 Ручное преобразование (вспомогательные средства стандартной библиотеки)

yaoxiang
# Явное преобразование
raw_ptr = ffi.to_pointer(my_bytes)
c_string = ffi.to_c_string(my_string)

5. Модель владения памятью

5.1 Основные принципы

Для каждого выделения памяти через границу FFI необходимо дать чёткий ответ на два вопроса:

  1. Кто выделяет? (сторона C malloc / сторона YaoXiang runtime)
  2. Кто освобождает? (сторона C free / сторона YaoXiang runtime)

При генерации yx-bindgen добавляет аннотации для типичных паттернов:

yaoxiang
# Выделено C, вызывающая сторона должна освободить
sqlite3_exec: (...) -> Int
    = native("sqlite3_exec")
    # memory: C-allocated, caller must free errmsg via sqlite3_free

# Вызывающая сторона выделяет память для указателя
read: (fd: Int, buf: *mut u8, count: Int) -> Int
    = native("read")
    # memory: caller-allocated buf

Среда выполнения не выполняет автоматическое управление памятью для ссылок на указатели, пересекающих границу FFI — ответственность за владение лежит на вызывающей стороне.

5.2 Обработка строк

При преобразовании char*, возвращённого функцией C, в YaoXiang String выполняется немедленное копирование. Владение исходным указателем определяется соглашением функции C (объявляется через аннотацию), автоматическое освобождение не выполняется.

6. Вопросы безопасности

6.1 Безопасность в условиях 并作

Вызовы функций FFI по умолчанию не участвуют в планировании DAG и рассматриваются как блокирующие операции. Функции C, подтверждённые как реентерабельные, могут быть помечены как @concurrent:

yaoxiang
# Чистая функция, без глобального состояния, может быть并发ной
sin_safe: (x: Float) -> Float = native("sin")
    # reentrant: true

# Имеет глобальное состояние, не может быть并发ной
strtok: (s: *const u8, delim: *const u8) -> *const u8 = native("strtok")
    # reentrant: false

yx-bindgen по возможности аннотирует информацию о реентерабельности для стандартных функций C (варианты strtok_r и т.д.).

Требования к асинхронным вызывающим сторонам: Перед вызовом FFI функции вызывающая сторона должна убедиться, что целевая функция является реентерабельной. Среда выполнения не выполняет автоматическую проверку — это неразрешимая статическая задача.

6.2 Изоляция ошибок

  • Ошибки вызовов FFI распространяются через тип Result (если функция объявила тип возврата Result)
  • Механизм таймаута предотвращает взаимоблокировку внешних функций
yaoxiang
# Вызов с таймаутом (реализуется на уровне FfiRegistry)
result = ffi.call_with_timeout("blocking_func", 5000)  # 5-секундный таймаут

6.3 Безопасность указателей

  • Параметры-указатели требуют маркировки unsafe со стороны YaoXiang
  • Время жизни указателей, пересекающих границу FFI, гарантируется вызывающей стороной

7. Изменения в компиляторе

Нулевые синтаксические изменения — требуется только объявление native("symbol"), которое уже реализовано в текущем компиляторе.

В интерпретаторе/среде выполнения добавляются:

  • Инструкции динамической загрузки библиотек (привязки FFI для DynamicLibrary)
  • Механизм таймаута

8. Функциональность, отклонённая к рассмотрению

Следующая функциональность после аудита явно исключена и больше не рассматривается в RFC:

  • ffi.try_call: Избыточно, уже есть native + тип возврата Result
  • ffi.verify_signature: Среда выполнения выполняет работу компилятора, неправильный уровень абстракции
  • ffi.async_call: Требуется сначала прояснить модель контракта реентерабельности
  • Таблица привязок, поддерживаемая сообществом: Невозможно выполнить, заменена схемой инструментария yx-bindgen

Компромиссы

Преимущества

  • Нулевые синтаксические изменения — единственной точкой входа в FFI является native("symbol"), полная обратная совместимость
  • Библиотека как язык — функциональность постепенно вводится через стандартную библиотеку
  • Инструментарий как драйверyx-bindgen автоматически обрабатывает генерацию привязок
  • Безопасность памяти — чёткая модель владения, отсутствие use-after-free из-за автоматической сборки мусора
  • Отладка — ошибки содержат коды ошибок ОС

Недостатки

  • ⚠️ Безопасность типов ограничена выразительностью заголовочных файлов C (void* невозможно статически различить)
  • ⚠️ yx-bindgen требует постоянной поддержки для отслеживания эволюции стандарта C
  • ⚠️ Привязки для языков, отличных от C (Python/JS/Java), требуют индивидуальной обработки в каждом проекте, единого решения нет

Стратегия реализации

Этап 1: Основная библиотека (v0.7)

  • [ ] Расширение модуля std.ffi
  • [ ] Реализация структуры DynamicLibrary
  • [ ] Поддержка Linux/macOS (dlopen/dlsym)
  • [ ] Поддержка Windows (LoadLibrary/GetProcAddress)
  • [ ] Добавление механизма таймаута в среду выполнения
  • [ ] Модульные тесты

Этап 2: yx-bindgen (v0.8)

  • [ ] Реализация парсера заголовочных файлов C (на основе существующих привязок Clang или самописного парсера)
  • [ ] Система отображения типов
  • [ ] Генерация объявлений native("symbol")
  • [ ] Генерация раскладки структур
  • [ ] Интеграционное тестирование: генерация привязок для реальных библиотек C — SQLite3, libcurl и др.

Этап 3: Фундамент экосистемы (v0.9)

  • [ ] Публикация официального пакета привязок libc (подмножество POSIX + Windows API)
  • [ ] Разработка спецификации публикации пакетов привязок
  • [ ] Документация: лучшие практики FFI, владение памятью, контракты безопасности 并作

Связь с другими RFC

  • RFC-001: Вызовы FFI как внешние функции, по умолчанию @block (не участвуют в планировании DAG)
  • RFC-008: Развязанный дизайн планировщика, вызовы FFI как независимые задачи
  • RFC-020: Семантика планирования FFI-узлов в DAG, Phi-узлы, разворот циклов и другие аспекты планирования на уровне调度

Открытые вопросы

  • [ ] Должен ли yx-bindgen быть интегрирован в систему сборки (yaoxiang build)?
  • [ ] Как спроектировать поддержку FFI для платформы WASM? (Механизм импорта WASM полностью отличается от dlopen)
  • [ ] Нужна ли поддержка cxx-bindgen для обработки name mangling C++ (опционально, после v1.0)?

Приложение A: Журнал решений по дизайну

РешениеРешениеПричинаДатаАвтор
Унификация точки входа FFIТолько native("symbol")Избежать расщепления API2026-05-29晨煦
Исключение try_callНе реализовыватьИзбыточно, есть тип Result2026-05-29晨煦
Исключение verify_signatureНе реализовыватьСреда выполнения делает работу компилятора2026-05-29晨煦
Привязки сообщества → инструментарийyx-bindgen автогенерацияНевозможная фантазия2026-05-29晨煦
Коды ошибок ОСFfiError обязательно содержит os_errorНеотлаживаемый API бесполезен2026-05-29晨煦
Нулевые синтаксические измененияРеализация через библиотекуПринцип простоты ядра2026-03-14晨煦
Динамическая загрузка библиотекИспользование dlopen/dlsymСтандартные интерфейсы ОС2026-03-14晨煦
Обработка ошибокИспользование типа ResultСогласованность2026-03-14晨煦

Приложение B: Примеры кода

Полный пример: использование библиотеки C

yaoxiang
# Загрузка математической библиотеки C
libm = ffi.load_library("libm.so")

# Регистрация символов C в таблице времени выполнения (yx-bindgen делает это на этапе компиляции)
ffi.register_library_symbols(libm, ["sin", "cos", "sqrt"])

# Использование через объявление native
sin_f: (x: Float) -> Float = native("sin")
cos_f: (x: Float) -> Float = native("cos")

# Прямой вызов
result = sin_f(3.14159 / 2)

# Для потенциально неудачных вызовов C-функций используется Result
file_open: (path: *const u8, mode: *const u8) -> Result(*mut opaque, Int)
    = native("fopen")

Использование yx-bindgen

bash
# Автогенерация всех объявлений, ручное написание не требуется
yx-bindgen --header /usr/include/math.h --output math_bindings.yx

# Подключение в YaoXiang
import "math_bindings.yx"
# sin_f / cos_f и др. автоматически объявлены как native("sin") / native("cos")

Список литературы