Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rustdoc JSONからFFIバインディングを自動生成してみた

Rustdoc JSONからFFIバインディングを自動生成してみた

Kyoto.rs #1

Avatar for Itto Hiramoto

Itto Hiramoto

May 11, 2026

More Decks by Itto Hiramoto

Other Decks in Programming

Transcript

  1. 自己紹介 平本 一桐 ヒラモト イットウ 放送大学 教養学部 教養学科 情報コース 3

    年 普段はアルバイトとして、以下の開発をしています。 Web フロントエンド と バックエンド Elasticsearch を使った検索基盤の構築 ソースコードからコールグラフを生成する静的解析ツールの開発 プログラミング言語処理系を作るのが好きで、その中で出てきた小ネタを今日は 持ってきました。 2
  2. 自作言語 Kaede Web サーバーを簡潔に書くための静的型付け言語 LLVM バックエンド・GC 軽量スレッド + 非同期I/O —

    多数の接続を素直に捌ける Rust に近い表現力 性能が要る所は Rust をそのまま呼べる import rust::mylib msg := rust::mylib::greet("Kyoto.rs") import rust::<crate> の一行で Rust の公開関数を直接呼べる — 今日はこの裏側の話 3
  3. 素朴なやり方 syn でソースを AST にする cbindgen と同じ路線 型はソースのトークンのまま — String

    を見ても std::string::String なのか独自定義の型かは判別できない マクロ展開前の AST なので、 #[derive] などが生やした関数は 見えない visibility ・再エクスポート・依存クレート横断を すべて自前で解決 する必要 rustc が済ませた仕事を、自分でもう一度やるハメになる 6
  4. アイデア rustdoc JSON で型情報を取る nightly の --output-format json で全部取れる cargo

    +nightly rustdoc --manifest-path Cargo.toml --lib -- \ -Z unstable-options --output-format json target/doc/<crate>.json に 型・シグネチャ・visibility が構造化 で出力さ れる rustc 自身が解析した結果なので マクロ展開後・型解決済み これを読めば「公開関数の一覧と型」が機械的に取れる 代償は nightly 依存・フォーマット unstable ・rustdoc 実行が遅い 7
  5. 実装:rustdoc JSON を読む rustdoc-types の型に向けて serde_json::from_str let krate: Crate =

    serde_json::from_str(&json_content)?; rustdoc-types 側は serde derive 付きの型定義を提供しているだけ pub struct Crate { pub index: HashMap<Id, Item>, // すべてのアイテム pub paths: HashMap<Id, ItemSummary>, // 他クレートの型解決用 pub format_version: u32, /* ... */ } pub enum Type { Primitive(String), // i32 / bool / char / ... Tuple(Vec<Type>), // () / (T, U) / ... BorrowedRef { /* .. */ }, // &T / &mut T ResolvedPath(Path), // 任意の名前付き型 /* ... */ } 8
  6. 素朴なやり方 ① flutter_rust_bridge 流 生成ファイルを元クレートに取り込んでもらう extern "C" ラッパーを frb_generated.rs のような

    Rust ファイルとして 書き出す 元クレートの lib.rs から mod frb_generated; で取り込み crate-type = ["cdylib"] でビルド 結局 元クレートの改変が必須 未改変の crates.io クレートを取り込みたい用途には弱い 10
  7. 素朴なやり方 ② napi-rs 流 proc-macro でコンパイル時に extern を注入 #[napi] pub

    fn greet(name: String) -> String { /* ... */ } proc-macro が extern "C" ラッパーを コンパイル時に展開 展開後のコードは元クレートにそのままコンパイルされ cdylib に含まれる 結局 元クレートに #[napi] を貼る改変が必要 機構は違っても、結論は ① と同じ 11
  8. アイデア Shim crate を生やす こんな小さな crate を、import ごとに自動生成 [package] name

    = "kaede_rust_shim_mylib" [lib] crate-type = ["cdylib"] [dependencies] mylib = { path = "../../../mylib" } # ← 元クレートをそのまま参照 元クレートは 未改変 できあがった cdylib は Kaede がビルドする実行ファイルにリンク 12
  9. 実装①:Shim crate を生成する ディレクトリ丸ごと書き出して cargo build build/kaede_rust_shim/mylib/ ├── Cargo.toml #

    cdylib + path 依存(前スライド) ├── src/ │ └── lib.rs # extern "C" ラッパー(次スライド) └── build.rs # Kaede ランタイム (libkd) を rustc-link-* で繋ぐ cdylib ・依存解決・リンクは全部 cargo に丸投げ 13
  10. 実装②:生成される lib.rs (イメージ) 元の Rust 関数を extern "C" ラッパーで包む //

    元クレート mylib にある公開関数 pub fn format_user(id: u64, name: &str) -> String { /* ... */ } // 自動生成される shim/src/lib.rs #[unsafe(no_mangle)] pub extern "C" fn kaede_rust_shim_mylib_format_user( id: u64, name: *const KaedeStr, // KaedeStr &str は次スライド ) -> *mut KaedeString { rust_str_into_kaede_string( mylib::format_user( // ← 元関数を呼ぶ id, unsafe { kaede_str_as_rust_str(name) }, ) ) } 関数名はサニタイズして kaede_rust_shim_<crate>_<fn> に統一。 14
  11. 文字列の橋渡し &str / String をどう FFI 越しに運ぶか 引数 &str —

    KaedeStr { ptr, len } をスライス化 → &str unsafe fn kaede_str_as_rust_str<'a>(v: *const KaedeStr) -> &'a str { let v = unsafe { &*v }; let bytes = unsafe { std::slice::from_raw_parts(v.ptr, v.len as usize) }; unsafe { std::str::from_utf8_unchecked(bytes) } } 戻り値 String — Kaede の GC ヒープに コピーして *mut KaedeString を返す fn rust_str_into_kaede_string(value: impl AsRef<str>) -> *mut KaedeString { let bytes = value.as_ref().as_bytes(); let ptr = unsafe { kaede_mem_alloc(bytes.len()) }.cast::<u8>(); unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, bytes.len()); } /* KaedeVectorU8 と KaedeString を同じく alloc して書き込む */ } 15
  12. まとめ rustdoc JSON で FFI 自動化が素直になる ユーザーは import rust::<crate> の

    1 行 入力は rustdoc JSON に、出力は cargo に任せる 元クレートを改変せず、そのまま使える 16