Skip to content

RFC-026:FFI コアメカニズム

参考:

廃棄:

サブ RFC:

概要

本文書は YaoXiang の FFI(外部関数インターフェース)のコアメカニズムを定義する。核心思想:外部ライブラリはコンパイル時リンクの一級値であり、境界を越えるデータのメモリレイアウトは型定義時に確定し、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") は YaoXiang バイトコードから Rust std 関数を呼び出す分发メカニズム(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 組み込み関数:本 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 は通常のメソッドバインディング(RFC-009 RAII 約束)
SqliteDb.drop = SqliteDb.close

コンパイル時検証:sqlite3("sqlite3_open")sqlite3_openlibsqlite3 のシンボルテーブルになければならず、さもなくばコンパイルエラー。

1.3 メソッドバインディングと self 位置

Type.method: (...) -> ... 記法では、self は暗黙的に第一位——db.exec("SELECT") 呼び出し時、db は C 関数 sqlite3_exec の第 0 引数。

独立関数を既に宣言済みのメソッドとしてバインディングする必要がある場合、[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 であり、関数値を = の右辺に置く点では同じで、2つのメカニズムではない。

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*、socket fd)
│   → 不透明ハンドル  =  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 の契約であり、外部のものではない。

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)

YaoXiang 型C 表現マーシャリングアクション所有権
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 で YaoXiang String にコピーYaoXiang がコピーを所有、元メモリに触れない
struct T*(新規ハンドル)不透明ハンドルハンドルを YaoXiang オブジェクトに保存YaoXiang が接管
struct T(値/出力引数)透過型C が임시 区間に書き込み → memcpy で YaoXiang に戻すYaoXiang が所有
char*(静的領域)*const U8生ポインタを保存、コピーなし(unsafe 読み)接管せず、ユーザーが負責

3.3 借用ライフタイム:単一呼び出しを厳密に限定

YaoXiang が外部コードに貸与するポインタ(String 読み取り専用ビュー、透過型 임시 区間、ハンドル)のライフタイムは単一呼び出し内に厳密に限定される

  • 呼び出し期間:ポインタ有効、外部コードは読み書き可能
  • 呼び出し終了後:借用は直ちに失效

外部コードがポインタを保存して呼び出し後に使用した場合、それは外部コードが FFI 標準契約に違反したこと(ライブラリのバグに相当)であり、YaoXiang はその責任负わない。これはすべての言語の C FFI 契約と一致(Rust の &T を C に渡同样约束)。

3.4 String は持久ポインタ永不放手

String は「C は YaoXiang メモリに手を出すな」の鍵である:

  • C へ:C への임시 読み取り専用ビューを一時的に貸与、呼び出し期間有効
  • C から:strlen + memcpy で YaoXiang が所有するコピーに入れる

C は YaoXiang String の持久ポインタを永久に手にできない、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 は既に Move 済み、消费後は読み取り不可

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(エラーコード)非 0 の時に panic——デストラクト失敗は状態異常を意味する、发覚优于沈黙
?Error非 None の時に panic——同上

デストラクト失敗は沈黙不可。特定のエラーを無視する必要がある場合、.drop がバインディングするラッパー関数内で明示的に処理する。


5. spawn ブロック内の FFI 動作

リソース型判定は .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 + 一意的所有権を実装
  • [ ] .drop RAII 自動デストラクト(オプション、缺失ではエラーなし)を実装
  • [ ] 消費追跡(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 + ハンドルの一意的所有権(無条件)構造的保証、.drop とは無関係2026-07-03
信頼境界Native.c(...) 宣言处にC シグネチャはコンパイル時検証不可、信頼を局所化、unsafe は生ポインタのみに使用2026-07-03
Null 処理?T または panicC の問題を隠さない、「沈黙で無視」オプションはない2026-07-03
デストラクト失敗.drop 返り型が決める、统一 panicデストラクト失敗は沈黙不可2026-07-03

参考文献

YaoXiang 公式ドキュメント

外部参考


ライフサイクルと行き先

状態位置説明
审核中docs/design/rfc/review/コミュニティ議論を開放
承認済みdocs/design/rfc/accepted/正式設計ドキュメント