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 Загрузка динамических библиотек
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:
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-заголовок → .yx)│ │ - dlopen/dlsym │
│ │ │ - LoadLibrary/GetProc │
└──────────────────┘ └────────────────────────┘3.2 Генератор связываний (yx-bindgen)
yx-bindgen — это отдельный CLI-инструмент, который генерирует из C-заголовков код FFI-связывания для YaoXiang:
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 должно явно отвечать на два вопроса:
- Кто выделяет? (malloc на стороне C / runtime на стороне YaoXiang)
- Кто освобождает? (free на стороне C / runtime на стороне YaoXiang)
При генерации 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, немедленно копируется при преобразовании в String YaoXiang. Владение исходным указателем определяется функцией C (объявляется через аннотацию) и не освобождается автоматически.
6. Соображения безопасности
6.1 Потокобезопасность
Вызовы FFI-функций по умолчанию не участвуют в планировании DAG и рассматриваются как блокирующие операции. C-функции, подтверждённые как reentrant, могут быть помечены как @concurrent:
# Чистая функция, без глобального состояния, может быть вызвана параллельно
sin_safe: (x: Float) -> Float = native("sin")
# reentrant: true
# Имеет глобальное состояние, не может быть вызвана параллельно
strtok: (s: *const u8, delim: *const u8) -> *const u8 = native("strtok")
# reentrant: falseyx-bindgen по возможности аннотирует информацию о reentrancy для функций стандартной библиотеки C (например, варианты _r для strtok).
Требования к асинхронным вызывающим: Перед вызовом FFI-функции вызывающий должен убедиться, что целевая функция является reentrant. Среда выполнения не выполняет автоматическое обнаружение — это задача, которую невозможно решить статически.
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: требует прояснения модели контракта 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") | Избежание разделения 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)
# Использование Result при вызове C-функций, которые могут завершиться неудачей
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")