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 Динамическая загрузка библиотек
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:
my_func: (a: Int, b: Int) -> Int = native("my_function")Никакого второго синтаксиса вызовов, никаких обёрток try_call.
1.2 Управление библиотеками
# Список загруженных библиотек
loaded = ffi.loaded_libraries()
# Выгрузка библиотеки
ffi.unload_library(lib)
# Проверка версии библиотеки
ffi.check_version(lib, "1.0.0")1.3 Разрешение символов
# Поиск символа по имени (возвращает структуру Symbol)
sin_sym = ffi.dlsym("libm.so", "sin")Соглашения о межъязыковых вызовах и преобразование типов не обрабатываются во время выполнения через универсальные обёртки, а генерируются yx-bindgen на этапе компиляции.
2. Реализация динамической загрузки библиотек
2.1 Основные структуры данных
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 Типы ошибок
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:
yx-bindgen --header /usr/include/sqlite3.h --output sqlite3.yxПример сгенерированного кода:
# Автогенерация, не редактировать вручную
# 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 | Способ преобразования |
|---|---|---|
int | Int | Прямая передача значения |
char* | *const u8 | Передача указателя |
void* | *mut opaque | Непрозрачный указатель |
struct T | extern struct T | Совпадение раскладки в памяти |
int* | *mut Int | Передача указателя (изменяемый) |
const int* | *const Int | Передача указателя (только чтение) |
4.2 Ручное преобразование (вспомогательные средства стандартной библиотеки)
# Явное преобразование
raw_ptr = ffi.to_pointer(my_bytes)
c_string = ffi.to_c_string(my_string)5. Модель владения памятью
5.1 Основные принципы
Для каждого выделения памяти через границу FFI необходимо дать чёткий ответ на два вопроса:
- Кто выделяет? (сторона C
malloc/ сторона YaoXiang runtime) - Кто освобождает? (сторона C
free/ сторона YaoXiang runtime)
При генерации yx-bindgen добавляет аннотации для типичных паттернов:
# Выделено 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:
# Чистая функция, без глобального состояния, может быть并发ной
sin_safe: (x: Float) -> Float = native("sin")
# reentrant: true
# Имеет глобальное состояние, не может быть并发ной
strtok: (s: *const u8, delim: *const u8) -> *const u8 = native("strtok")
# reentrant: falseyx-bindgen по возможности аннотирует информацию о реентерабельности для стандартных функций C (варианты strtok_r и т.д.).
Требования к асинхронным вызывающим сторонам: Перед вызовом FFI функции вызывающая сторона должна убедиться, что целевая функция является реентерабельной. Среда выполнения не выполняет автоматическую проверку — это неразрешимая статическая задача.
6.2 Изоляция ошибок
- Ошибки вызовов FFI распространяются через тип
Result(если функция объявила тип возврата Result) - Механизм таймаута предотвращает взаимоблокировку внешних функций
# Вызов с таймаутом (реализуется на уровне 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+ тип возвратаResultffi.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") | Избежать расщепления API | 2026-05-29 | 晨煦 |
Исключение try_call | Не реализовывать | Избыточно, есть тип Result | 2026-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
# Загрузка математической библиотеки 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
# Автогенерация всех объявлений, ручное написание не требуется
yx-bindgen --header /usr/include/math.h --output math_bindings.yx
# Подключение в YaoXiang
import "math_bindings.yx"
# sin_f / cos_f и др. автоматически объявлены как native("sin") / native("cos")