Skip to content

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

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

Ссылки:

Резюме ​

Данный документ предлагает библиотечно-управляемый подход к расширению FFI (внешнего интерфейса функций). Единственная точка входа в 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")

# Получение символов функций из библиотеки, возвращает имя symbol, доступное 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-заголовок → .yx)│   │  - dlopen/dlsym         │
│                   │   │  - LoadLibrary/GetProc  │
└──────────────────┘   └────────────────────────┘

3.2 Генератор связываний (yx-bindgen) ​

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

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 поддерживается официально, что гарантирует:

  • Полноту отображения типов (int → Int, 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. Кто выделяет? (malloc на стороне C / runtime на стороне YaoXiang)
  2. Кто освобождает? (free на стороне C / runtime на стороне YaoXiang)

При генерации 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, немедленно копируется при преобразовании в String YaoXiang. Владение исходным указателем определяется функцией C (объявляется через аннотацию) и не освобождается автоматически.

6. Соображения безопасности ​

6.1 Потокобезопасность ​

Вызовы FFI-функций по умолчанию не участвуют в планировании DAG и рассматриваются как блокирующие операции. C-функции, подтверждённые как reentrant, могут быть помечены как @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 по возможности аннотирует информацию о reentrancy для функций стандартной библиотеки C (например, варианты _r для strtok).

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

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: требует прояснения модели контракта reentrancy перед рассмотрением
  • Таблица связываний, поддерживаемая сообществом: нереализуемо, заменяется решением на основе 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 для обработки C++ name mangling (опционально, рассмотреть после 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_errorAPI без отладки бесполезен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)

# Использование Result при вызове C-функций, которые могут завершиться неудачей
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")

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