Skip to content

RFC-026:Основные механизмы FFI

Справка:

Устарело:

Подчинённые RFC:

Аннотация

В данном документе определяются основные механизмы FFI (Foreign Function Interface) языка YaoXiang. Ключевая идея: внешние библиотеки являются значениями первого класса на этапе компиляции, распределение памяти для данных, передаваемых через границу, фиксируется при определении типа, а объекты кучи YaoXiang структурно изолированы от внешнего кода.

  1. Внешние библиотеки как значенияNative.c("libsqlite3") выполняет компоновку библиотеки на этапе компиляции и возвращает парсер, использующий каррирование для хранения информации о библиотеке
  2. Внешние символы как значения:Применение парсера к имени символа даёт внешнюю ссылку, которая связывается с типом или функцией через name: type = value (RFC-007/010)
  3. Дихотомия типов:Непрозрачные дескрипторы (распределение памяти принадлежит внешней стороне) / прозрачные типы (распределение памяти принадлежит YaoXiang), третьего не дано
  4. Изоляция маршалинга:Данные, передаваемые через границу, по умолчанию копируются во временную зону вызова, объекты кучи YaoXiang изолированы от внешнего кода
  5. Безопасность владения:Единоличное владение дескриптором (Move) + RAII, структурно исключает двойное освобождение и use-after-free
  6. Люк безопасности*T сырой указатель + unsafe {}, пользователь явно принимает риск прямого доступа к памяти без копирования

Основные границы — пять неизменяемых контрактов:

1. Компоновка библиотеки на этапе компиляции, верификация символов на этапе компиляции
2. Принадлежность распределения памяти типа определяется при определении: непрозрачные дескрипторы принадлежат внешней стороне, прозрачные типы принадлежат YaoXiang
3. Маршалинг по умолчанию через копирование во временную зону, объекты кучи YaoXiang изолированы от внешнего кода
4. Единоличное владение дескриптором + Move, структурная защита от двойного освобождения/висячих ссылок
5. Внешний код всегда работает с памятью с определённым распределением и определённым владением, никаких неоднозначностей

Мотивация

Текущая ситуация и цели

Текущий native("symbol") в кодовой базе — это лишь механизм диспетчеризации для вызова функций std языка Rust из байткода YaoXiang (FfiRegistry = HashMap<String, RustFnPtr>), без какой-либо реальной границы ABI — без dlopen, без маршалинга по C ABI, без передачи владения памятью через границы.

Настоящий FFI должен решать четыре проблемы:

ПроблемаОтвет данного RFC
Разрешение символовБиблиотека как значение первого класса с компоновкой на этапе компиляции (Native.c("lib")), верификация символов на этапе компиляции
Маршалинг значенийУправляется сигнатурой, компилятор определяет правила преобразования для каждой позиции параметра на этапе компиляции
Владение памятьюДихотомия типов определяет принадлежность; копирование по умолчанию для изоляции
Безопасность времени жизниMove + RAII + ограничение заимствования для одного вызова

RFC-020 и RFC-021 определяли различные аспекты FFI с перекрытием, данный документ объединяет их в единую спецификацию.

Цели проектирования

  1. Ноль утечек сырых указателей в пользовательском коде:В обычном FFI-использовании, в исходном коде .yx не появляются сырые указатели
  2. Явная принадлежность распределения:Пользователь определяет тип и решает, кому принадлежит эта память, без выводов во время выполнения
  3. Структурная безопасность:Без утечек, без двойного освобождения, без use-after-free — гарантируется системой типов, а не соглашениями
  4. Честные границы доверия:C не может предоставить типы контрактов, верифицируемые на этапе компиляции, доверие локализовано в объявлениях привязок
  5. Совместимость с самохостингом:Без чрезмерных абстракций, уникальных для языка-хоста

За пределами охвата

  • Расширяемая система плагинов для нескольких ABI (Wasm/Python/пользовательские ABI):см. RFC-026a
  • Инструментарий yx-bindgen:см. RFC-026b
  • Экспорт функций YaoXiang для вызова из C (обратный FFI):последующий RFC, в данном документе только объявлены принципы
  • Встроенный ассемблер, SIMD intrinsics:не входят в охват данного RFC

Предложение

1. Внешние библиотеки и символы: значения первого класса с каррированием

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

1.1 Библиотека как значение

yaoxiang
// Native.c применяет имя библиотеки → компонует библиотеку на этапе компиляции, возвращает парсер символов
sqlite3 = Native.c("libsqlite3")

Native.c("libsqlite3") — это действие на этапе компиляции + значение на этапе выполнения:

  • Этап компиляции:Компоновщик -lsqlite3, библиотека входит в таблицу символов, существование символа верифицируемо
  • Значениеsqlite3 — это парсер, применение имени символа даёт внешнюю ссылку на эту библиотеку

.c — метка механизма ABI (C ABI). В ядре встроена только .c; другие механизмы (.wasm и др.) см. RFC-026a.

1.2 Символ как значение, связывание как name: type = value

Применение парсера к имени символа даёт внешнюю ссылку, которая связывается через унифицированный синтаксис RFC-007/010. Типовая аннотация LHS определяет, является ли эта ссылка типом или функцией:

yaoxiang
sqlite3 = Native.c("libsqlite3")

// LHS — это Type → связывается как непрозрачный тип
SqliteDb: Type = sqlite3("sqlite3")

// LHS — это сигнатура функции → связывается как функция
SqliteDb.open: (file: String) -> ?SqliteDb = sqlite3("sqlite3_open")
SqliteDb.exec: (sql: String) -> Int32 = sqlite3("sqlite3_exec")
SqliteDb.close: () -> Int32 = sqlite3("sqlite3_close")

// .drop — обычное связывание метода (соглашение RAII из RFC-009)
SqliteDb.drop = SqliteDb.close

Верификация на этапе компиляции:sqlite3("sqlite3_open") — символ sqlite3_open должен присутствовать в таблице символов libsqlite3, иначе ошибка компиляции.

1.3 Связывание методов и позиция self

В записи Type.method: (...) -> ... self неявно на первой позиции — при вызове db.exec("SELECT"), db передаётся как нулевой параметр C-функции sqlite3_exec.

Если нужно связать уже объявленную независимую функцию как метод, используется синтаксис [N] для указания позиции self (каррирование с несколькими позициями из RFC-004):

yaoxiang
// Независимая функция
sqlite3_close_v2: (db: SqliteDb) -> Int32 = sqlite3("sqlite3_close_v2")

// Связывается как метод, [0] означает, что db — это self
SqliteDb.soft_close = sqlite3_close_v2[0]

Прямое связывание методов Native.c(...) и ручное связывание [N] — оба используют name: type = value, оба помещают значение функции справа от =, никаких двух механизмов.

1.4 Использование пользователем: ноль unsafe, ноль сырых указателей

yaoxiang
import sqlite3_bindings

db = SqliteDb.open("test.db")
db.exec("SELECT * FROM users")
// ← конец области видимости, RAII автоматически вызывает SqliteDb.drop → sqlite3_close(db)

2. Дихотомия типов: принадлежность распределения фиксируется при определении

Когда внешние данные попадают в YaoXiang, задаётся только один вопрос: кто определяет распределение этой памяти?

├─ Распределение — чёрный ящик внешней стороны (sqlite3, FILE*, дескриптор сокета)
│   → Непрозрачный дескриптор  =  lib("symbol")
│   → YaoXiang только хранит указатель, никогда не разыменовывает, передаёт только между функциями библиотеки
│   → Внешний код читает свою память, YaoXiang не трогает

└─ Распределение определено YaoXiang (timespec, point, struct с полями для чтения)
    → Прозрачный тип  =  { field: Type, ... }
    → YaoXiang владеет памятью, определяет распределение, читает/записывает поля
    → Внешний код читает/записывает в память с определённым YaoXiang распределением

Третьего не дано. Трёхуровневая модель памяти (копирование/接管/системный уровень) из предыдущих проектов — это паттерное мышление; истина — дихотомия принадлежности распределения.

2.1 Непрозрачные дескрипторы: распределение принадлежит внешней стороне

yaoxiang
SqliteDb: Type = sqlite3("sqlite3")
  • Внутри YaoXiang хранится только дескриптор размером с указатель
  • Пользователь не может конструировать (SqliteDb {} → ошибка компиляции), не может обращаться к полям (нет полей для доступа)
  • Единственный источник: внешняя функция, возвращающая SqliteDb
  • При вызове метода дескриптор передаётся обратно в библиотеку, библиотека читает свою память (структура sqlite3 в куче библиотеки)

Внешний код "читает внутренности" — это чтение структуры, которую он сам выделил, YaoXiang просто переносит дескриптор. Без конфликтов памяти.

2.2 Прозрачные типы: распределение принадлежит YaoXiang

yaoxiang
// Поля значимы, нужно читать/записывать → прозрачный тип, распределение объявлено YaoXiang
Timespec: Type = {
    tv_sec: Int64,
    tv_nsec: Int64
}
clock_gettime: (clk: Int32, ts: *Timespec) -> Int32 = Native.c("librt")("clock_gettime")

ts = clock_gettime(CLOCK_REALTIME)   // см. §3, маршалинг через временную зону
print(ts.tv_sec)                      // YaoXiang читает согласно своему определению полей

Внешний код читает/записывает в память с распределением, определённым YaoXiang, которой YaoXiang владеет. Распределение — это контракт YaoXiang, а не внешней стороны.

2.3 Правило принятия решения

Пользователю нужно определить только одно: нужно ли мне читать поля этого типа?

РешениеТипПринадлежность распределения
Поля не читаю, передаю дескриптор между функциями библиотекиНепрозрачный дескриптор = lib("sym")Внешняя сторона
Нужно читать/записывать поляПрозрачный тип { ... }YaoXiang

3. Маршалинг: управляется сигнатурой, изоляция через временную зону

Преобразование данных, передаваемых через границу, управляется сигнатурой, компилятор определяет правила преобразования для каждой позиции параметра. Основная гарантия безопасности: внешний код читает/записывает в маршальную временную зону, а не в объекты кучи YaoXiang.

3.1 По умолчанию через копирование во временную зону

YaoXiang → C (входные параметры):
    Копирование данных во временную зону вызова → передача указателя на временную зону в C
    → C вышел за границы/испортил → повреждена только временная зона, объекты кучи YaoXiang изолированы

C → YaoXiang (возвращаемые/выходные параметры):
    C пишет во временную зону → YaoXiang копирует обратно в свой объект через memcpy
    → C не касается финального объекта YaoXiang

Внешний код всегда читает/записывает в маршальную временную зону, полностью изолированную от объектов кучи YaoXiang. Неправильное объявление распределения, C сохранил висячий указатель, C вышел за границы — всё это повреждает только временную зону, объект YaoXiang цел. Цена — одно memcpy.

3.2 Таблица правил маршалинга

Направление входных параметров (YaoXiang → C):

Тип YaoXiangC представлениеДействие маршалингаВладение
Int32/Int64/Floatint/long/doubleПомещается напрямую в регистр, без преобразованияСемантика значения
Stringconst char*Предоставляется временное представление только для чтения (временно, действительно во время вызова)YaoXiang сохраняет, C только читает
Прозрачный типstruct T*Копирование во временную зону, передача указателя на временную зонуYaoXiang владеет объектом, C читает копию
Непрозрачный дескрипторvoid*Извлекается внутренний указатель дескриптораYaoXiang владеет, предоставляется C
*TT*Передаётся напрямую сырой указатель (unsafe)Ответственность пользователя

Направление возврата (C → YaoXiang):

Возвращаемое CТип YaoXiangДействие маршалингаВладение
int/doubleInt32/FloatЧтение напрямую из регистраСемантика значения
char*Stringstrlen + memcpy в String YaoXiangYaoXiang владеет копией, исходная память не трогается
struct T* (новый дескриптор)Непрозрачный дескрипторДескриптор сохраняется в объекте YaoXiangYaoXiang принимает владение
struct T (значение/выходной параметр)Прозрачный типC пишет во временную зону → memcpy в YaoXiangYaoXiang владеет
char* (статическая область)*const U8Сохранение сырого указателя без копирования (unsafe чтение)Не принимается владение, ответственность пользователя

3.3 Жизненный цикл заимствования: строго ограничен одним вызовом

Указатели, которые YaoXiang предоставляет внешнему коду (временное представление String только для чтения, временная зона прозрачного типа, дескриптор), имеют жизненный цикл, строго ограниченный одним вызовом:

  • Во время вызова: указатель действителен, внешний код может читать/записывать
  • После возврата из вызова: заимствование немедленно прекращается

Если внешний код сохранил указатель для использования после вызова — это нарушение внешним кодом стандартного FFI-контракта (эквивалентно багу библиотеки), YaoXiang за это не отвечает. Это согласуется с контрактами FFI C во всех языках (заимствование &T в Rust при передаче в C также ограничено).

3.4 String никогда не отдаёт персистентный указатель

String — ключ к "C не вмешивается в память YaoXiang":

  • В C: предоставляется временное представление только для чтения, действительное во время вызова
  • Из C: strlen + memcpy в копию, которой владеет YaoXiang

C никогда не получает персистентный указатель на String YaoXiang, YaoXiang никогда не хранит долгосрочную ссылку на C char*. Структурная изоляция.


4. Владение и жизненный цикл: Move + RAII

Непрозрачные дескрипторы следуют модели владения из RFC-009, без новых концепций.

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

  • Семантика Move:Непрозрачные дескрипторы по умолчанию Move, присваивание/передача параметров/возврат = передача владения, нельзя копировать
  • Единоличное владение дескриптором:В любой момент времени дескриптор имеет только одного владельца → структурно исключает двойное освобождение
  • RAII-освобождение:В конце области видимости, если привязан .drop, автоматически вызывается
  • Отслеживание потребления:После явного уничтожения или Move переменная потреблена, нельзя использовать снова → исключает use-after-free

4.2 .drop — это опциональный побочный эффект

yaoxiang
SqliteDb.drop = SqliteDb.close     // в конце области видимости вызывается sqlite3_close

.drop — это не механизм предотвращения утечек YaoXiang — хранение дескриптора на стороне YaoXiang (значение размером с указатель) автоматически освобождается, не связано с .drop. .drop — это опциональный побочный эффект вызова внешней функции в конце области видимости:

  • Привязан .drop → в конце области видимости вызывается (очистка внешних ресурсов)
  • Не привязан .drop → ничего не делается, не ошибка, не предупреждение

Нужно ли очищать внешние ресурсы — это вопрос спецификации внешней библиотеки (getenv возвращает статическую область, не надо освобождать; глобальный синглтон не надо освобождать), YaoXiang не берёт на себя полномочия强制. Защита от утечек через Move + единоличное владение (безусловно, структурно), а не через .drop.

4.3 Автоматическое уничтожение и порядок

yaoxiang
{
    db = SqliteDb.open("test.db")
    stmt = db.prepare("SELECT * FROM users")
    // ← конец области видимости, обратный порядок автоматического уничтожения (вызывается только если есть .drop):
    //   stmt.drop()  → sqlite3_finalize(stmt)
    //   db.drop()    → sqlite3_close(db)
}

Порядок уничтожения: обратный порядку определения, как в RAII.

4.4 Move и потребление

yaoxiang
db = SqliteDb.open("test.db")
db2 = db                // Move: передача владения
db.exec("...")          // ❌ Ошибка компиляции: db уже перемещён, после потребления нельзя читать

process_db: (db: SqliteDb) -> Void = {
    db.exec("...")
    // ← конец функции, db уничтожается здесь
}
process_db(some_db)     // Move в функцию
// some_db здесь уже недействителен

4.5 Обработка Null

yaoxiang
// Может вернуть null → ?T, пользователь должен обработать
SqliteDb.open: (file: String) -> ?SqliteDb = sqlite3("sqlite3_open")

db = SqliteDb.open("test.db")
match db {
    Some(db) => db.exec("SELECT 1"),
    None => print("Ошибка открытия")
}

// Соглашение не возвращать null → не помечать, при null panic

C возвращает null либо пользователь обрабатывает (?T), либо panic. Третьего "тихо игнорировать" нет.

4.6 Обработка ошибок уничтожения

Возвращаемое значение функции, привязанной к .drop, определяет поведение:

Тип возврата .dropПоведение
VoidБез ошибок
Int32 (код ошибки)Ненулевое → panic — ошибка уничтожения означает ненормальное состояние, лучше обнаружить чем молчать
?ErrorНе None → panic — аналогично

Ошибка уничтожения не должна замалчиваться. Если нужно игнорировать определённые ошибки, явно обработайте их в обёрточной функции, привязанной к .drop.


5. Поведение FFI в блоках spawn

Определение типа ресурса определяется привязкой .drop (RFC-024), без дополнительных标记:

ОпределениеПоведение
Непрозрачный дескриптор с .dropТип ресурса — операции с одним экземпляром в блоке spawn автоматически сериализуются
Непрозрачный дескриптор без .dropНе тип ресурса — можно параллельно (чистый дескриптор данных, без побочных эффектов освобождения)
Прозрачный тип / тип значенияНе тип ресурса — можно параллельно
yaoxiang
SqliteDb.drop = SqliteDb.close   // → тип ресурса

(a, b) = spawn {
    r1 = db.exec("SELECT ..."),   // тот же экземпляр, автоматическая сериализация
    r2 = db.exec("INSERT ...")    // ожидание r1
}

(x, y) = spawn {
    db1 = SqliteDb.open("a.db"),   // разные экземпляры, можно параллельно
    db2 = SqliteDb.open("b.db")
}

Типы с .drop автоматически сериализуют операции с одним экземпляром в spawn, гарантируя уничтожение без гонок параллелизма.


6. Люк безопасности: сырые указатели + unsafe

Маршалинг по умолчанию через копирование во временную зону безопасен, но имеет накладные расходы memcpy. В сценариях, чувствительных к производительности (большие структуры, частые вызовы), когда нужно копирование без копирования, пользователь явно использует сырой указатель через люк безопасности:

yaoxiang
// C напрямую читает память YaoXiang, ноль копирования — пользователь явно принимает риск
ptr: *const U8 = Native.c("libc")("getenv")("HOME")
unsafe {
    value = read_c_string(ptr)   // пользователь гарантирует действительность ptr
}

unsafe используется только для операций с сырыми указателями, полностью ортогонален непрозрачным дескрипторам и прозрачным типам. Обычный FFI (дескрипторы + прозрачные типы) не требует unsafe. Написание unsafe {} = пользователь явно подписывается на риск прямого доступа к памяти.

Граница доверия: C не может предоставить типы контрактов, верифицируемые на этапе компиляции (.h — это не ABI-контракт, таблица символов содержит только имена без сигнатур). Поэтому правильность C-сигнатур не может быть автоматически верифицирована — это гарантируется автором привязки при написании Native.c(...) + сигнатуры. Доверие локализовано в объявлении привязки: автор привязки гарантирует, пользователи пакета получают безопасный API. Это согласовано с Rust extern "C" (написание extern — это акт доверия, после оборачивания в безопасную обёртку вызов безопасен).


Компромиссы

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

  1. Полная информация:Компоновка библиотеки на этапе компиляции, верификация символов на этапе компиляции, нет размытости "библиотека не найдена" во время выполнения
  2. Явная принадлежность распределения:Дихотомия типов, фиксируется при определении, без выводов во время выполнения
  3. Структурная безопасность:Изоляция через временную зону + Move + RAII, внешний код не касается объектов кучи YaoXiang
  4. Ноль новых ключевых словNative.c каррирование + name: type = value, всё переиспользует существующий синтаксис
  5. Честные границы:Не притворяемся, что можем верифицировать C-сигнатуры, доверие локализовано в объявлениях

Недостатки

  1. Накладные расходы memcpy:Маршалинг по умолчанию с копированием, для больших структур и частых вызовов нужно явно использовать люк безопасности
  2. Гарантия распределения ручная:Соответствие распределения прозрачного типа и C struct обеспечивается автором привязки/yx-bindgen
  3. C-сигнатуры не верифицируются на этапе компиляции:Фундаментальное ограничение FFI, YaoXiang не может устранить из C

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

Этап 1: Внешние библиотеки и символы (v0.8)

  • [ ] Реализовать Native.c("lib") компоновку на этапе компиляции + возврат парсера значений
  • [ ] Реализовать применение парсера символов (lib("symbol")) + верификацию таблицы символов на этапе компиляции
  • [ ] Реализовать дихотомию типов (непрозрачные дескрипторы / прозрачные типы)
  • [ ] Реализовать связывание методов (прямое связывание + позиционное связывание [N])

Этап 2: Маршалинг и безопасность (v0.8)

  • [ ] Реализовать генерацию кода маршалинга, управляемую сигнатурой
  • [ ] Реализовать изоляцию через копирование во временную зону (копирование входных параметров, возврат через memcpy)
  • [ ] Реализовать временное представление String только для чтения + возврат с копированием
  • [ ] Реализовать ограничение жизненного цикла заимствования одним вызовом

Этап 3: Владение и жизненный цикл (v0.9)

  • [ ] Реализовать Move непрозрачных дескрипторов + единоличное владение
  • [ ] Реализовать RAII автоуничтожение .drop (опционально, отсутствие не вызывает ошибку)
  • [ ] Реализовать отслеживание потребления (отключение после Move)
  • [ ] Реализовать интеграцию ?T с возвратом null
  • [ ] Реализовать сериализацию типа ресурса в spawn

Последующая работа

  • Расширяемый механизм FFI (RFC-026a):Абстракция FfiMechanism, плагины .wasm/.python и др., динамическая загрузка
  • yx-bindgen (RFC-026b):Заголовочные файлы C → привязки .yx + генерация распределения с учётом платформы

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

  • RFC-004:Каррирование с несколькими позициями — источник синтаксиса позиционного связывания методов [N]
  • RFC-007:Унификация синтаксиса определения функций — привязка Native.c(...) это name: type = value
  • RFC-009:Модель владения — Move, RAII, ?T, жизненный цикл дескрипторов полностью на этом основан
  • RFC-010:Унифицированный синтаксис типов — типовая аннотация LHS определяет привязку как тип или функцию
  • RFC-024:Модель параллелизма — определение типа ресурса в spawn основано на .drop
  • RFC-020/021 (устаревшие):Содержимое объединено в данный документ
  • RFC-026a:Расширяемая система механизмов FFI
  • RFC-026b:Инструментарий yx-bindgen

Журнал принятия решений

РешениеРешениеПричинаДата
Библиотека как значениеNative.c("lib") каррирование возвращает парсерИнформация о библиотеке становится видимым на этапе компиляции значением первого класса, заполняет пробел "какую библиотеку компоновать", ноль новых ключевых слов2026-07-03
Компоновка на этапе компиляцииNative.c("lib") вызывает -llibТаблица символов читаема на этапе компиляции, существование символов верифицируемо, типы реальны2026-07-03
Дихотомия типовНепрозрачные дескрипторы / прозрачные типыПринадлежность распределения二分全覆盖; удалён патч "трёхуровневая модель памяти"2026-07-03
Изоляция через временную зону маршалингаПо умолчанию копирование, куча изолирована от внешней стороныВыход за границы/висячая ссылка внешней стороны повреждает только временную зону, объект YaoXiang цел; копирование ноль требует явно использовать люк2026-07-03
.drop опциональноПри отсутствии ничего не делает, не ошибкаХранение дескриптора YaoXiang автоматически освобождается; очистка внешних ресурсов — это вопрос спецификации внешней стороны, не берём полномочия强制2026-07-03
Механизм защиты от утечекMove + единоличное владение дескриптором (безусловно)Структурная гарантия, не связана с .drop2026-07-03
Граница доверияВ объявлении Native.c(...)C-сигнатуры не верифицируются на этапе компиляции, доверие локализовано, unsafe только для сырых указателей2026-07-03
Обработка Null?T или panicПроблемы C не скрываются, нет варианта "тихо игнорировать"2026-07-03
Ошибка уничтоженияТип возврата .drop определяет,统一ный panicОшибка уничтожения не должна замалчиваться2026-07-03

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

Официальная документация YaoXiang

Внешние ссылки


Жизненный цикл и судьба

СтатусРасположениеОписание
На проверкеdocs/design/rfc/review/Открыто для обсуждения сообщества
Принятоdocs/design/rfc/accepted/Официальный проектный документ