FFIライブラリ使用状況調査 — `rigor-ffi`設計の基礎(2026-05-25)
ステータス: 調査ノート、設計コミットメントなし。
FFIバインディングgemをADR-0 / ADR-1とロバストネス原則が要求するレベルで静的解析可能にする将来のrigor-ffiプラグインの前提条件。
最終目標はこれらのgemを使用するRubyコードの型チェック — つまり、Ethon::Easy#perform、RbNaCl::SecretBox#encrypt、ZMQ::Context#socket等を呼び出すユーザーコード。中間目標(FFIプラグイン)は、Dynamic[Top]に全面的に頼らずに型情報がFFI境界を越えて流れるための静的基板。
ソースは/tmp/ffi-survey/<lib>/内のシャロークローンで読んだ;クローンはコミットされておらず、サブモジュールとして参照もされていない。引用はクローンのファイルパスを指すので、将来の読者が検証のために再クローンできる。
設計空間を意図的に網羅する5ライブラリを調査:
| ライブラリ | バインディングスタイル | 目的 |
|---|---|---|
| ethon | 純FFI | libcurlバインディング(typhoeusエンジン) |
| rbnacl | 純FFI + カスタムDSLラッパー | libsodium / NaCl暗号 |
| ffi-rzmq | 純FFI (gem境界を越えて — ffi-rzmq-core) | ZeroMQバインディング |
| childprocess | FFIなし(Ruby Process / IOのみ) | クロスプラットフォームの子プロセス制御 |
| sassc-ruby | 純FFI + ベンダーCソースビルド | libsassバインディング |
childprocessはFFIを使わないgemの意図的な否定ケースとして含まれている——FFIを使うと思われながら実際は使わないgemを検証する。rigor-ffiがFFIバインディングコードの存在を根拠に有効化されるべきであり、「このgemはOSと対話する」という理由ではないことを確認する。
各セクションで同じ10の質問に答えており、ライブラリ間の知見をクロスリードできる。集約された評価(プラグインの設計形状の決定)はクロージングセクションにある。
ethon — libcurlバインディング
Section titled “ethon — libcurlバインディング”1. FFIモジュール構成
Section titled “1. FFIモジュール構成”lib/ethon/下の5ファイル:
curl.rb— トップレベルのEthon::Curlモジュール、FFI::Libraryをextendし、カタログモジュールをmix-in(curl.rb:9-30)。curls/settings.rb— 5つのcallback宣言 +ffi_libターゲット。curls/functions.rb— すべてのattach_function呼び出し。curls/classes.rb—FFI::Struct/FFI::Union定義。libc.rb—select(2)とgetdtablesizeのための最小libcシム。
attach_function呼び出し合計約32件(libcurl 29件 + libc 2件 + select 1件)。
2. ffi_libターゲット
Section titled “2. ffi_libターゲット”ffi_lib ['libcurl', 'libcurl.so.4'](curls/settings.rb:10)。select向けのプラットフォームシムはWindowsではws2_32、それ以外ではFFI::Library::LIBCを選択(curls/functions.rb:48-52)。env-varやGem::Util.find_library解決なし。
3. typedefの使用
Section titled “3. typedefの使用”明示的なtypedef呼び出しなし——FFIビルトイン(:size_t、:time_t、:suseconds_t、:long_long、:int64)を直接使用。カスタム名はenum名として導入され、typedefとしてではない(§7参照)。
4. attach_functionパターン
Section titled “4. attach_functionパターン”3つの代表的な形状(curls/functions.rb):
# (a) 単純なポインター返しbase.attach_function :easy_init, :curl_easy_init, [], :pointer # :15
# (b) enum + varargsbase.attach_function :easy_setopt, :curl_easy_setopt, [:pointer, :easy_option, :varargs], :easy_code # :18
# (c) .ptr構文でのstruct-to-pointerの返しbase.attach_function :version_info, :curl_version_info, [], Curl::VersionInfoData.ptr # :42エラーラッピングはFFI境界ではなくRubyレイヤーにある——例: curl.rb:61はglobal_initが非ゼロを返した場合にraiseする。
5. コールバック定義
Section titled “5. コールバック定義”5つのcallback typedef(curls/settings.rb:4-8):
callback :callback, [:pointer, :size_t, :size_t, :pointer], :size_tcallback :socket_callback, [:pointer, :int, :poll_action, :pointer, :pointer], :multi_codecallback :timer_callback, [:pointer, :long, :pointer], :multi_codecallback :debug_callback, [:pointer, :debug_info_type, :pointer, :size_t, :pointer], :intcallback :progress_callback, [:pointer, :long_long, :long_long, :long_long, :long_long], :intRubyのProcはラッピングオブジェクトに保存され、easy_setoptに直接渡される(easy/callbacks.rb:23, :38-44)。FFIがブリッジする。
6. FFI::Struct / Unionの使用
Section titled “6. FFI::Struct / Unionの使用”classes.rb内の4つのキャリア:
MsgData(Union、:5-7) —:whatever, :pointer / :code, :easy_code。Msg(Struct、:10-12) —:code, :msg_code, :easy_handle, :pointer, :data, MsgData。VersionInfoData(Struct、:14-24) — ランタイムバージョンメタデータ。FDSet(Struct、:27-52) — プラットフォーム条件付きレイアウト(Windows対POSIX)。Timeval(Struct、:55-62) — 同様。
ManagedStructなし、BitStructなし。FDSetが唯一のプラットフォーム分岐レイアウト。
7. enumの使用
Section titled “7. enumの使用”FFI::Library#enumの多用(constants.rb:17-78):
EasyCode = enum(:easy_code, easy_codes)EasyOption = enum(:easy_option, easy_options(:enum).to_a.flatten)PollAction = enum(:poll_action, [:none, :in, :out, :inout, :remove])SocketReadiness = bitmask(:socket_readiness, [:in, :out, :err])カタログはEthon::Curls::Codes、Options、Infosから来る——シンボル配列を返す単純なRubyモジュール。
8. Pointer / MemoryPointer / AutoPointerラッピング
Section titled “8. Pointer / MemoryPointer / AutoPointerラッピング”FFI::AutoPointerが主要なライフサイクルパターン(easy/operations.rb:14、multi/operations.rb:17):
@handle ||= FFI::AutoPointer.new(Curl.easy_init, Curl.method(:easy_cleanup))マルチループ中のin/outパラメータ向けの明示的なMemoryPointer.new(:long) / Curl::Timeval.new。ProcはGCルートを保つために@procsハッシュに格納される(easy/options.rb:42-45)。
9. ユーザー向けRuby API → FFIマッピング
Section titled “9. ユーザー向けRuby API → FFIマッピング”Easy#url=が標準的なブリッジ(easy/options.rb:10-13):
def url=(value) @url = value Curl.set_option(:url, value, handle)endCurl.set_option(curls/options.rb:13-109)はオプションカタログの:typeフィールドでディスパッチし、varargs経由でeasy_setopt(handle, const, va_type, value)を呼ぶ。各オプションの:type(:string / :long / :callback / :string_as_pointer)がランタイム型識別子。
10. 静的展開可能性評価
Section titled “10. 静的展開可能性評価”回収可能: FFIシグネチャ(ロード時)、structレイアウト、5つのcallback typedef、enumシンボル→intマッピング(カタログモジュールから)。
オプションディスパッチャーをモデル化しないと回収不可:
easy_setopt内のvarargs — 第3引数のFFI型はカタログを経由してoption enum値によって決まる。ナイーブなウォーカーは[:pointer, :easy_option, :varargs]を見てお手上げになる。- オプションカタログ上の
define_method(easy/options.rb:32-47) — すべてのセッター(url=、ssl_verifypeer=、writefunction)はロード時にCurl.easy_options(nil)を反復して生成される。セッターはASTに存在しない。
rigor-ffiにとって、オプションカタログ + 生成セッターのモデル化が標準的なFFIバインディング認識を超える主要な精度レバー。
rbnacl — libsodium / NaClバインディング
Section titled “rbnacl — libsodium / NaClバインディング”1. FFIモジュール構成
Section titled “1. FFIモジュール構成”プリミティブごとの分割——暗号構成ごとに1モジュール(SecretBoxes::XSalsa20Poly1305、Signatures::Ed25519、Hash::{SHA256, SHA512, Blake2b}、HMAC::*、AEAD::*、PasswordHash::Argon2、OneTimeAuths::Poly1305など)。各モジュールがSodiumをextendしてFFIマシナリーを継承(lib/rbnacl/sodium.rb:6-66)。
attach相当呼び出し合計約52件、大半がリテラルのattach_functionではなくカスタムDSLラッパーのsodium_function経由。
2. ffi_libターゲット
Section titled “2. ffi_libターゲット”ffi_lib ["sodium", "libsodium.so.18", "libsodium.so.23", "libsodium.so.26"]ABIバージョンにまたがる静的フォールバックリスト(init.rb:8と、遅延extendされるモジュールのためにsodium.rb:11にも)。sodium/version.rb:22-25でlibsodium < 0.4.3の場合にraise;feature gate(例: lib/rbnacl.rb:81のArgon2)が検出されたバージョンに基づいて条件付きでrequireする。
3. typedefの使用
Section titled “3. typedefの使用”なし。プリミティブFFI型(:pointer、:ulong_long、:size_t、:uint{8,32,64}、:string、:int)を全体で直接使用。
4. attach_functionパターン — カスタムDSLラッパー
Section titled “4. attach_functionパターン — カスタムDSLラッパー”静的解析の重大なハザード。sodium_function(sodium.rb:47-55)がmodule_eval内の補間ヘレドックでattach_functionをラップする:
def sodium_function(name, function, arguments) module_eval <<-RUBY, __FILE__, __LINE__ + 1 attach_function #{function.inspect}, #{arguments.inspect}, :int def self.#{name}(*args) ret = #{function}(*args) ret == 0 end RUBYendrigor-ffiにとって重要な3つのプロパティ:
- 戻り型が暗黙的 — すべての
sodium_functionがFFIレイヤーで:intを返し、Rubyレイヤーでtrue|falseを返す。 - 引数はヘレドック内のクォートされたシンボルであり、実際の
attach_function呼び出しノードではない——attach_functionをgrep-スキャンするウォーカーはそれらを完全に見逃す。 - 2つの名前 — C関数(
:crypto_secretbox_xsalsa20poly1305)とRubyメソッド(:secretbox_xsalsa20poly1305)——がどちらもリテラル引数。
代表的な呼び出しサイト(secret_boxes/xsalsa20poly1305.rb:32-38):
sodium_function :secretbox_xsalsa20poly1305, :crypto_secretbox_xsalsa20poly1305, %i[pointer pointer ulong_long pointer pointer]このパターンがrbnaclの圧倒的に支配的なバインディング形状。伴うDSL sodium_constant(sodium.rb:32-45)がKEYBYTES / NONCEBYTES / … 定数に同じ技法を使う。
5. コールバック定義
Section titled “5. コールバック定義”なし。libsodiumはAPIサーフェスにコールバックを持たない。
6. FFI::Structの使用
Section titled “6. FFI::Structの使用”4つのストリーミングハッシュ状態struct(HMAC::SHA256::State、HMAC::SHA512::State、HMAC::SHA512256::State、Hash::Blake2b::State)。代表例(hmac/sha256.rb:95-106):
class SHA256State < FFI::Struct layout :state, [:uint32, 8], :count, :uint64, :buf, [:uint8, 64]endManagedStruct、Union、BitStructなし。
7. enumの使用
Section titled “7. enumの使用”なし。
8. Pointer / MemoryPointer / バッファラッピング
Section titled “8. Pointer / MemoryPointer / バッファラッピング”rbnaclはMemoryPointerを意図的に回避する — バッファはBINARYエンコーディングの単純なRuby String。ユーティリティはUtil.zeros(n)(util.rb:22-27):
def zeros(n = 32) zeros = "\0" * n zeros.respond_to?(:force_encoding) ? zeros.force_encoding("ASCII-8BIT") : zerosendFFIが呼び出し時にString → :pointerをオートコーションする。検証はUtil.check_string / Util.check_length(util.rb:111-117)を通じる。
9. ユーザー向けRuby API → FFIマッピング
Section titled “9. ユーザー向けRuby API → FFIマッピング”SecretBox#boxが標準的(secret_boxes/xsalsa20poly1305.rb:49-76):
def box(nonce, message) Util.check_length(nonce, nonce_bytes, "Nonce") msg = Util.prepend_zeros(ZEROBYTES, message) ct = Util.zeros(msg.bytesize) success = self.class.secretbox_xsalsa20poly1305(ct, msg, msg.bytesize, nonce, @key) raise CryptoError, "Encryption failed" unless success Util.remove_zeros(BOXZEROBYTES, ct)endalias encrypt box5ステップのブリッジ: 長さ検証 → ゼロパディング → Stringとして出力確保 → sodium_function生成ラッパーを呼ぶ → パディング除去。ユーザーサーフェスは(String, String) -> String;FFI型はそれを何も伝えない。
10. 静的展開可能性評価
Section titled “10. 静的展開可能性評価”回収可能、プラグインがsodium_functionをバインディング定義マクロとして理解する場合:
- 各
sodium_function(ruby_name, c_name, arg_types)がattach_function c_name, arg_types, :int+ Booleanを返すself.{ruby_name}メソッドを生成する。 - 各
sodium_constant(...)がattach_function由来の定数を生成する。
AST単独では回収不可(DSL認識なし):
- 生成された
attach_functionとdef self.…(補間されたヘレドック文字列内にあり、呼び出しノードとしてではない)。 - 条件付きfeatureロード(
rbnacl.rb:81)——ランタイムバージョン検出で暗号プリミティブ全体をゲートする。
rbnaclはrigor-ffiプラグインがプラグイン側DSL認識器をサポートすることの最も強い根拠——リテラルのattach_functionが生成するのと同じバインディングファクトを合成するsodium_function対応拡張。
ffi-rzmq — ZeroMQバインディング
Section titled “ffi-rzmq — ZeroMQバインディング”1. FFIモジュール構成
Section titled “1. FFIモジュール構成”FFIバインディングは別gemに存在する — ffi-rzmq-core、ffi-rzmq.gemspec:24でランタイム依存として宣言されlib/ffi-rzmq.rb:66でrequireされる。ffi-rzmqコードベース自体(Context、Socket、Message、Poller、PollItem、Device、Util、例外クラスにまたがる約1642行)はコアgemからLibZMQ.*上の純Rubyのラッパー。
これはFFI解析のクロスgem境界ケース。
2. ffi_libターゲット
Section titled “2. ffi_libターゲット”ffi-rzmqからは不透明——ffi-rzmq-coreで処理される。README(README.rdoc:167-170)はOSパッケージマネージャー / Homebrewでlibzmqをインストールするよう指示する。
3. typedefの使用
Section titled “3. typedefの使用”:size_tが参照される(socket.rb:531);カスタムtypedefはffi-rzmq-coreにあるだろう。
4. attach_functionパターン
Section titled “4. attach_functionパターン”ffi-rzmqにはない。ラッパーが「負のリターン = エラー、LibZMQ.zmq_errno経由でフェッチ」イディオムでリテラルのLibZMQ.*呼び出しを使う(util.rb:71-78):
rc = LibZMQ.zmq_setsockopt @socket, name, pointer, lengthZMQ::Util.error_check 'zmq_setsockopt', rc5. コールバック定義
Section titled “5. コールバック定義”LibC::Freeがzmq_msg_init_dataデストラクターとして渡される(message.rb:134)。ファイナライザーはLibZMQ.zmq_close / zmq_ctx_termを呼ぶクロージャでObjectSpace.define_finalizerを使う(context.rb:132、socket.rb:580-583)。
6. FFI::Structの使用
Section titled “6. FFI::Structの使用”LibZMQ::PollItem(ffi-rzmq-coreで定義、poll_item.rb:11、poll_items.rb:13で参照)。zmq_msg_tはLayoutなしにFFI::MemoryPointer.new(Message.msg_size, 1, false)(message.rb:99)として不透明にラップされる。
7. enumの使用
Section titled “7. enumの使用”なし。ソケットタイプ(ZMQ::REQ = 3、ZMQ::REP = 4、…)とオプションキー(ZMQ::RCVMORE、ZMQ::LINGER、…)は単純なRuby定数。オプションディスパッチはロード時に1度構築された3つの配列——IntegerSocketOptions、LongLongSocketOptions、StringSocketOptions(socket.rb:569-571)——を通じる。
8. Pointer / MemoryPointer / バッファラッピング
Section titled “8. Pointer / MemoryPointer / バッファラッピング”ファイナライザーベースのライフサイクル: すべてのcontext / socketがforkの後のダブルクローズを避けるためにProcess.pid == pidでゲートされた対応するzmq_*クリーンアップ関数を呼ぶdefine_finalizerを登録する。オプションバッファはLibC.malloc / free(socket.rb:129, 145)またはキャッシュされたFFI::MemoryPointer(socket.rb:537-547)のいずれか。
9. ユーザー向けRuby API → FFIマッピング
Section titled “9. ユーザー向けRuby API → FFIマッピング”ZMQ::Context#socket(type)(context.rb:109-118)がSocket.new(@context, type)(socket.rb:65-88)に委譲し、LibZMQ.zmq_socketを呼んで結果のポインタを保存する。ユーザー向けの型識別子はSymbolではなく整数定数type(:REQ短縮形なし)。
Socket#send_string(socket.rb:244-247)がStringをMessageにラップし、zmq_msg_tを確保してバイトをコピーする。
10. 静的展開可能性評価
Section titled “10. 静的展開可能性評価”難しい問題: ffi-rzmqのシグネチャはgem境界を越えた場所にある。ユーザーのプロジェクトのみをスキャンするrigor-ffiプラグインは、以下なしにLibZMQ.zmq_socketのarity / 戻り型を見られない:
- ffi-rzmq-coreのソースにウォークインする(ADR-10オプトインの依存関係ソース推論を参照)、または
LibZMQ向けのバンドルRBSを出荷する(ADR-25プラグイン提供RBSを参照)。
追加の精度ブロッカー:
Socket.new(socket.rb:32, 68)内の:receiver_classポリモーフィズム —recvmsgがユーザーの選択したクラス(デフォルトZMQ::Message)を返す。- ZeroMQ 3.2.xと4.xの間のバージョン条件付きFFI——ffi-rzmq-core内で処理され、ラッパーレイヤーでは不可視。
socket.rb:569-571の3つの配列を通じた動的オプションディスパッチ。
自然なrigor-ffi形状: ffi-rzmq-coreのattach_function呼び出しを依存関係としてスキャンし、次にそれらのシグネチャ上でffi-rzmqのラッパークラスメソッドを型付けする。
childprocess — 意図的な否定ケース
Section titled “childprocess — 意図的な否定ケース”発見: FFIをまったく使わない。
Section titled “発見: FFIをまったく使わない。”require 'ffi'なし、childprocess.gemspecにFFI依存なし、attach_function / callback / FFI::Struct呼び出しゼロ。
アーキテクチャ: childprocess.rb:17-25のプラットフォームディスパッチャーがUnix::ProcessまたはWindows::Processを選択し、どちらもRubyビルトインの::Process.spawn / ::Process.waitpid2 / ::Process.killに委譲する。WindowsのSIGKILLフォールバックはtaskkill /F /T /PIDへのシェルアウト(process_spawn_process.rb:117-123)。Pipeは::IO.pipeを使用。
rigor-ffiへの示唆
Section titled “rigor-ffiへの示唆”childprocessはプラグインが検出されたFFIバインディングコード(require 'ffi' + extend FFI::Library + 少なくとも1つのattach_functionまたは同等のもの)を根拠に有効化されるべきであり、「このgemはローレベルに見える」という理由ではないことを検証する。RubyのProcess / IOスタンダードライブラリを使うgemはコアRubyの問題であり、FFIプラグインの問題ではない。
sassc-ruby — FFI経由のベンダーlibsass
Section titled “sassc-ruby — FFI経由のベンダーlibsass”1. FFIモジュール構成
Section titled “1. FFIモジュール構成”Cエクステンションを宣伝するgemであるにもかかわらず純FFI。sassc.gemspec:27がspec.extensions = ["ext/extconf.rb"]を宣言するが、エクステンションはrb_define_method Cエクステンションではない——extconf.rb(:59-64)がベンダードのlibsassサブモジュールをシンプルな共有ライブラリ(libsass.{bundle,so})にコンパイルし、その後ランタイムでFFI経由でロードされる(lib/sassc/native.rb:9-14):
dl_ext = RbConfig::MAKEFILE_CONFIG['DLEXT']begin ffi_lib File.expand_path("libsass.#{dl_ext}", __dir__)rescue LoadError ffi_lib File.expand_path("libsass.#{dl_ext}", "#{__dir__}/../../ext")endlib/sassc/native/下の3つのサブモジュール: native_context_api.rb、native_functions_api.rb、sass2scss_api.rb。
2. ライブラリターゲット
Section titled “2. ライブラリターゲット”ext/libsass下のベンダードlibsass(サブモジュール)。gem installビルド時にビルド;上記パス経由でランタイムにロード。システムlibsassのフォールバックなし。
3. 型エイリアス(typedef)
Section titled “3. 型エイリアス(typedef)”調査対象のgemの中で最も広範なtypedef使用(lib/sassc/native.rb:18-29)。10個の不透明ポインタエイリアス: :sass_options_ptr、:sass_context_ptr、:sass_file_context_ptr、:sass_data_context_ptr、:sass_value_ptr、:sass_import_list_ptr、:sass_import_ptr、:sass_importer、:sass_c_function_list_ptr、:sass_c_function_callback_ptr。
すべて:pointerに解決するが名前による区別を提供する——調査コーパスで最も優れた静的解析フック。
4. attach_functionパターン
Section titled “4. attach_functionパターン”標準的、加えてプレフィックスストリップ規約 —— attach_functionがオーバーライドされ(lib/sassc/native.rb:39-47)、sass_make_optionsをNative.make_optionsとして登録する。例(native_context_api.rb:9, 24, 44, 106):
attach_function :sass_make_options, [], :sass_options_ptrattach_function :sass_compile_file_context, [:sass_file_context_ptr], :intattach_function :sass_delete_data_context, [:sass_data_context_ptr], :voidattach_function :sass_option_set_output_style, [:sass_options_ptr, SassOutputStyle], :void5. コールバック定義
Section titled “5. コールバック定義”2つのtypedef(lib/sassc/native.rb:31-32):
callback :sass_c_function, [:pointer, :pointer], :pointercallback :sass_c_import_function, [:pointer, :pointer, :pointer], :pointerユーザー提供RubyのProcはFFI::Function.newでラップされ(functions_handler.rb:23-26、import_handler.rb:26-33)、ラッパーを生かしておくために@callbacksに格納される。
6. FFI::Structの使用
Section titled “6. FFI::Structの使用”Sass値のタグ付きユニオンが最大のレイアウト(lib/sassc/native/sass_value.rb:84-95):
class SassValue layout :unknown, SassUnknown, :boolean, SassBoolean, :number, SassNumber, :color, SassColor, :string, SassString, :list, SassList, :map, SassMap, :null, SassNull, :error, SassError, :warning, SassWarningend各バリアントが独自のSassTag識別子を持つ(:33-37等)。
7. enumの使用
Section titled “7. enumの使用”3つのenum(sass_output_style.rb:5-10、sass_value.rb:7-22):
SassOutputStyle = enum(:sass_style_nested, :sass_style_expanded, :sass_style_compact, :sass_style_compressed)SassTag = enum(:sass_boolean, :sass_number, :sass_color, :sass_string, :sass_list, :sass_map, :sass_null, :sass_error, :sass_warning)SassSeparator = enum(:sass_comma, :sass_space)attach_functionの引数リストで直接使用されるため、FFIが呼び出しサイトでRubyのシンボルを自動変換する。
8. Pointer / MemoryPointer / ライフサイクルラッピング
Section titled “8. Pointer / MemoryPointer / ライフサイクルラッピング”2つの注目すべきパターン:
-
文字列入力 → FFIオーナーシップ転送(
lib/sassc/native.rb:54-58):def self.native_string(string)m = FFI::MemoryPointer.from_string(string)m.autorelease = falsemendautorelease = falseはバッファをlibsassに渡し、libsassがそれを解放する。 -
ensureブロックライフサイクル(lib/sassc/engine.rb:63) —Engine#renderのrescue/ensureテールでNative.delete_data_context(data_context)。
AutoPointerなし。
9. ユーザー向けRuby API → FFIマッピング
Section titled “9. ユーザー向けRuby API → FFIマッピング”SassC::Engine#render(lib/sassc/engine.rb:22-64)はストレート8ステップのFFIパイプライン: make_data_context → contextにアンラップ → optionsにアンラップ → 約10個のoption_set_*セッターを呼ぶ → コールバックを登録 → compile_data_context → 出力文字列を抽出 → フリー。メタプログラミングなし——すべてのステップがリテラルのNative.something呼び出し。
10. 静的展開可能性評価
Section titled “10. 静的展開可能性評価”調査の中でrigor-ffiの観点から最も行儀の良いライブラリ:
- すべての
attach_function呼び出しがリテラルの呼び出しノード。 - typedef済み不透明ポインタがすべてのCリソースに別個のnominal型を与える;プラグインが
sass_data_context_ptrをNominal[SassC::Native::SassDataContextPtr]として運び、混合を拒否できる。 - enumが静的に列挙されている。
- Structレイアウトが静的に宣言されている。
残るギャップはFFI固有ではない:
FFI::Function.newコールバック(functions_handler.rb:23-26)は内部のRuby呼び出しに通常のフロー解析を必要とする匿名Proc。.rbsスタブファイルなし——FFI宣言が唯一の契約。
集約された知見
Section titled “集約された知見”バインディングスタイル分類
Section titled “バインディングスタイル分類”| パターン | ライブラリ | 静的解析可能性 |
|---|---|---|
リテラルのattach_function呼び出し | ethon、sassc-ruby | 高 |
attach_functionをラップするカスタムDSL(sodium_function等) | rbnacl | DSL認識なしでは低 |
別gem内のバインディング(ffi-rzmq-core) | ffi-rzmq | 依存関係ソース推論なしでは低 |
FFIなし(Ruby Process / IOを使用) | childprocess | N/A |
プラグインが理解しなければならない繰り返すFFIプリミティブ
Section titled “プラグインが理解しなければならない繰り返すFFIプリミティブ”extend FFI::Library+ffi_libattach_function name, c_name, [arg_types], return_type— ストレートおよび末尾のオプションHash付き(:blocking、:enums)callback name, [arg_types], return_typetypedef existing_type, alias_name— 主として名前付き不透明ポインタエイリアス(sassc-rubyパターン)としてenum :name, [:a, :b, …]とbitmask(ethon、sassc-ruby)FFI::Struct、FFI::Union、FFI::ManagedStruct、FFI::AutoPointerFFI::MemoryPointer.{new, from_string}、MemoryPointer#put_bytes / read_stringFFI::Pointer.{null?, address}、FFI::Pointer::NULLFFI::Function.new(return, [args]) { |…| … }— 匿名コールバック- 呼び出し境界でのString ↔
:pointer/:stringオートコーション
静的推論のクロスカッティングハザード
Section titled “静的推論のクロスカッティングハザード”-
attach_function周りのDSLラッパー(rbnaclのsodium_function、プレフィックスストリップ用のsassc-rubyのオーバーライドされたattach_function)。プラグインはリテラルのattach_functionが記録するバインディングファクトを再生するDSL認識器を必要とする。これはsodium_function向けのADR-16 Tier C(ヘレドックテンプレート展開)とプレフィックスストリップオーバーライド向けのTier B(トレイトインライニング)にきれいにマップする。 -
クロスgemバインディング定義(ffi-rzmq → ffi-rzmq-core)。どちらもロードマップにすでにある2つの妥当な答え:
- プラグイン提供RBS(ADR-25)——
rigor-ffi-rzmq内に手でキュレートされたLibZMQスタブを出荷。 - 依存関係ソース推論(ADR-10)——
ffi-rzmq-coreのattach_function呼び出しのオプトインウォーク。
ADR-25が低FPパス;ADR-10が低メンテナンスパス。
- プラグイン提供RBS(ADR-25)——
-
enumによってディスパッチされるvarargs(ethonの
easy_setopt)。va_typeは外部カタログを通じて2番目の引数のenum値によって決まる。オプションディスパッチャーをモデル化しないと3番目の引数はDynamic[Top]として型付けされる;カタログがあるとオプションごとの小さなユニオンとして型付けされる。ライブラリごとの特別扱いが多すぎてrigor-ffiコアに属せない——ライブラリごとのプラグイン(rigor-ethon、rigor-rbnacl)の上位に属する。 -
FFIメタデータからの動的メソッド生成(
easy/options.rb:32-47のoption catalogueへのethonのdefine_method)。生成されたセッター / ゲッターはASTに存在しない。これはADR-16 Tier B / CがDevise / dry-structのために解くのと同じ問題。 -
nominalサブタイピングとしてのtypedef済み不透明ポインタ。これを多用しているのはsassc-rubyだけだが、最もクリーンなフック——
typedef :sass_data_context_ptrはNominal[SassC::Native::SassDataContextPtr]になり、ベアの:pointerと暗黙的に統一されるべきではない。これはサーベイの中でロバストネス原則との最も強い整合: 戻り型は厳密(型付きエイリアス)、入力はエイリアスかベースの:pointerのどちらも受け付ける。 -
匿名コールバックProcの返し値の型(
FFI::Function.new(rt, [args]) { … })。ブロック本体は通常の推論エンジンを必要とし、callbacktypedefがブロックのパラメータ型をバインドする。実装面では最も近いアナローグはADR-28のバインダーティアメカニズム(パラメータ置換のため)。 -
リソースライフサイクル。
AutoPointer.new(ctor, dtor_method)、ObjectSpace.define_finalizer、delete_*関数を呼ぶensureブロック。これらはそれ自体では型の問題ではない;プラグインがuse-after-free / double-free診断ファミリーを持つ場合にのみ重要になる。初期rigor-ffiのスコープ外。
rigor-ffiの提案形状(初稿、設計コミットメントではない)
Section titled “rigor-ffiの提案形状(初稿、設計コミットメントではない)”コアプラグインが出荷すべきもの(ethon + sassc-ruby + バニラFFI gemの80%をカバー):
extend FFI::Libraryを認識してホストモジュールをバインディングレジストリとして扱う。attach_function、callback、typedef、enum、bitmask、FFI::Struct.layout、FFI::Union.layout呼び出しをウォークしてファクト(メソッドシグネチャ、コールバック型、enumシンボルセット、structフィールドオフセットと型)をエンジンに貢献する。- よく知られたFFIキャリア——
FFI::Pointer、FFI::MemoryPointer、FFI::Buffer、FFI::AutoPointer、FFI::Function、FFI::Struct/Unionサブクラス——を正確なメソッドシグネチャで型付けする(read_string : (?Integer) -> String、put_bytes : (Integer, String, ?Integer, ?Integer) -> self等)。 LibZMQ.zmq_send(socket, "hello", 5, 0)がエラーを出さないよう、呼び出し境界でのString ↔:string/:pointerコーションをモデル化する。- typedef済みポインタエイリアスを別個のnominalsとして扱い、入力のみでベースの
:pointerを部分型として受け付ける(ロバストネス)。
ライブラリごとのサブプラグイン(rigor-rbnacl、rigor-ethon、rigor-ffi-rzmq、rigor-sassc)に属するもの:
- DSL認識器(
sodium_function、sassc-rubyのプレフィックスストリップオーバーライド)。 - バンドルされた
LibZMQ形状のRBS(ADR-25)。 - オプションカタログ → セッターメソッド生成(ethon)。
- 高レベルAPI精緻化(
SecretBox#encrypt: (String, String) -> String、Easy#perform: () -> Integer)。
初期プラグインで意図的にスコープ外のもの:
- FFIハンドルのuse-after-free / double-free / リーク診断。
- 具体的なstructフィールド境界(
FDSet/SHA256Stateの配列レイアウトは型付けには十分だが境界チェックには不十分)。 - Cサイドのコンパイル正確性(sassc-rubyの
ext/libsassビルド)。
childprocessからの否定ケースの教訓
Section titled “childprocessからの否定ケースの教訓”- FFIバインディングコードを根拠に有効化し、「ネイティブリソースを使用する」ことを根拠にしない。
::Process.spawn/::IO.pipe/::IO.popenで偽陽性を出さない——それらはコアRubyであり、stdlib RBSで処理される(またはされない)。
設計ADRへのオープンクエスチョン
Section titled “設計ADRへのオープンクエスチョン”- DSL認識器形状:
rigor-ffiはプラガブルな認識器テーブルを持つ単一プラグインか、それともsodium_functionはrigor-ffi提供の貢献APIを消費するrigor-rbnaclが出荷するか?(ADR-13のTypeNodeリゾルバチェーン議論の反響。) - クロスgemシグネチャ取得: ffi-rzmq-coreケースでADR-25(プラグイン提供RBS)に頼るか、ADR-10(依存関係ソース推論)を待つか? 2つは異なるメンテナンスコスト / 精度のトレードオフを持つ。
- typedef済み不透明ポインタはデフォルトでnominalサブタイピングにすべきか、それとも
typedef呼び出しごとのオプトインか(例:typedef :pointer, :sass_data_context_ptr, nominal: true)? 純「常にnominal」はドキュメントのみの目的でtypedefを使うgemを壊すリスクがある。 - プラグインは
FFI::Function.newとブロックをどの程度積極的にモデル化すべきか? 匿名Proc推論はエンジン全体の作業であり、FFI固有ではない。
補遺 — tenderloveのffxエコシステム(2026-05-25、同日)
Section titled “補遺 — tenderloveのffxエコシステム(2026-05-25、同日)”rigor-ffiがffi gemの代わりに(またはそれに加えて)ffxをターゲットにするプロジェクト向けに別のコードパスを必要とするかどうかを明確化するためのAaron Pattersonの最近のFFI置換作業のフォローアップサーベイ。
読んだソース:
- ブログ: Tiny JITs for a Faster FFI(2025-02-12)
- カンファレンス: RubyKaigi 2026 — “A Faster FFI”(Patterson, 2026)
- 解説(JP): note.com/hatai_hatai — RubyKaigi 2026まとめ
- リポジトリ: tenderlove/ffx、tenderlove/sqliteffx、tenderlove/fisk、tenderlove/aarch64
アーキテクチャスケッチ
Section titled “アーキテクチャスケッチ”ffxはブログタイトルが1年の進化後に示唆するようなランタイムJITではない。ffx@HEADで実際に出荷されsqliteffxで実証されている形状は:
-
ビルド時 —
extconf.rb内のFFX.create_makefile(name, src.rb, headers: [...])がsrc.rbをロードし、FFI::Libraryに対して記録されたすべてのattach_function宣言を内省し、本物のCエクステンションを$srcdirにemitする。mkmfがそれを.so/.bundleにビルドする。 -
ランタイム — ロードされるのはコンパイルされたネイティブエクステンションであり、FFIマーシャリングではない。すべての
attach_functionバインドメソッドはrb_define_singleton_method経由で公開される本物のC関数。 -
JITヒント — 各生成C関数は2つのパートで出荷される:
_impl関数(マーシャリング本体)と、jmp _…_implに続くバイナリメタデータブロブ[FFI0マジック | param_count | type_bytes | function_name]のインライン__asm__を含むネイキッドトランポリン。ZJITはオフセット4のブロブを読んで特化した直接呼び出しコードを生成する。
ブログの「tiny JITs」フレームはプロトタイプパスの一部だったfisk / aarch64コードジェンライブラリにマップする;現在の出荷形状は生成されたCのインライン__asm__を使い、アセンブリはmkmf / Cコンパイラに任せる。fisk / aarch64は独立したJITライブラリとして有用なまま、ffxのランタイムパスにはない。
報告されたパフォーマンス(コンテキスト用、rigorにとって重要ではない)
Section titled “報告されたパフォーマンス(コンテキスト用、rigorにとって重要ではない)”| キャリア(strlenラッパー) | ops/sec |
|---|---|
| 純ffi gem | 約15 M |
| 手書きCエクステンション | 約45 M |
| ffx + ZJIT | 約54 M |
(JP要約の数字;ブログの初期〜32.5 M FJIT対〜29.8 M Cエクステンション(異なるベンチ)と一致。)「RubyのFFIがCエクステンションより速い」という実用的な主張はRubyKaigi 2026キーノートスロットを獲得するほど実証されている。
ffxサーフェス解析(同じ10の質問に対して)
Section titled “ffxサーフェス解析(同じ10の質問に対して)”ユーザー向けDSLはクラシックFFIとビット同一。ffxがmodule FFX::Libraryを出荷してFFI::Libraryにエイリアスするので既存のバインディングファイルが変更なしにコンパイルされる(ffx.rb:244-246)。extend FFI::Library、ffi_lib "sqlite3"、attach_function :name, [args], ret — すべて同じ構文形式。
サーフェスレベルでffi gemと異なるもの:
| 構文 | クラシックffi | ffx |
|---|---|---|
attach_function(プリミティブ) | ✓ | ✓ |
ffi_lib | ✓ | ✓ |
typedef | ✓ | ✗ 非対応 |
callback | ✓ | ✗ 非対応(sqliteffx README:46-47が確認) |
enum / bitmask | ✓ | ✗ 非対応 |
FFI::Struct / Union / ManagedStruct | ✓ | ✗ 非対応 |
可変長引数(:varargs) | ✓ | ✗ 非対応 |
| カスタム型コンバーター | ✓ | ✗ 非対応 |
| プリミティブ型シンボル | 数十 | 25 — void, int, long, string, uint, size_t, double, float, pointer, bool, char, uchar, short, ushort, int{8,16,32,64}, uint{8,16,32,64}, long_long, ulong_long, ulong(ffx.rb:12-38) |
ffxはFFIの厳密なサブセット。これはrigor-ffiにとって最も重要な発見: クラシックFFIを処理するプラグインはffxターゲットコードを自動的に処理する——より少ない認識器サーフェスが発火し、より多くはない。
sqliteffx — 標準的なffx消費者
Section titled “sqliteffx — 標準的なffx消費者”集中した単一ファイルのバインディング宣言(ext/sqliteffx/sqliteffx.rb、44行)。2つのパターンがプラグイン設計にとって重要:
(a) ffi.rbスタブリダイレクトトリック。sqliteffxは空のext/sqliteffx/ffi.rbをスタブとして出荷する。sqliteffx.rbがextconf時にrequire "ffi"を実行するとき、インストール済みのffi gemよりext/sqliteffx/がRubyロードパスの先にあれば、スタブが勝ってffxのmixinにextend FFI::Libraryが解決する。ランタイムではFFI gemは何もロードされない——コンパイルされたCエクステンションがロードされる。
rigor-ffiにとって、これはextend FFI::Libraryを宣言するプロジェクトファイルがffiまたはffxのどちらかをターゲットにしている可能性があることを意味し、gemビルドプロセスがどちらを使うかをASTだけでは判断できない。プラグインは(a)共通サブセットを無条件にカバーし、(b)プロジェクトが実際にffi gemに依存している場合にのみFFI専用機能認識器(callback、struct、enum、typedef)を発火させなければならない。
(b)生のRuby Integerとしての不透明ポインタ。sqliteffxはSQLiteハンドルをFFI::Pointerでラップしない。8バイトスロットをmallocで確保し、スロットをアウトパラメータとしてsqlite3_openを呼び出し、次にFiddle::Pointer.new(@slot)[0, 8].unpack1("Q<")経由でスロットを読み戻す——単純なRuby Integerが得られる(lib/sqliteffx.rb:41)。そのIntegerを:pointer引数としてSqliteffx.sqlite3_close(@handle)に返す。
ffxの:pointerのマーシャリングは(void *)NUM2ULL(value) — 任意のRuby Integerを受け付ける。クラシックFFIはFFI::Pointerを受け付けて特定の型を自動変換するが、ベアのIntegerは受け付けない。
これはrigor-ffiの実際の精度上の問題: :pointerパラメータ型の受け付けられる入力セットはffxではクラシックFFIより広い。ロバストネス原則の答えは、:pointerパラメータの入力をFFI::Pointer | Integer | nilに全面的に広げ、非対応型でのffi-gemランタイムエラーはrigor-ffiではなくgem自体が診断するに任せること。戻り型は厳密のまま——クラシックFFIはFFI::Pointerを返し、ffxはIntegerを返す;プラグインはプロジェクトのターゲットに従う。
fisk / aarch64 / JITBuffer — スコープ外の判定
Section titled “fisk / aarch64 / JITBuffer — スコープ外の判定”どちらのgemもFFIバインディングサーフェス自体を持たない純Rubyの命令エンコーダーDSL(attach_functionなし、extend FFI::Libraryなし、FFI依存なし——実行可能ページをmmapするためにStdlibのFiddleを使用)。ffxを消費するエンドユーザーコードはFisk::*またはAArch64::*シンボルを参照しない。ffx自体ですらそれらに依存しない——インライン__asm__トランポリンはコードジェンgemを完全に回避する。
推奨: rigor-ffiはfiskとaarch64を無視する。それらはJITを書く人が使うコードジェンバックエンドであり、FFIバインディングを書く人が使うものではない。将来のrigor-jitプラグイン(もし要望があれば)がそれらを拾い上げるかもしれない;それは別のサーフェス領域。
集約されたrigor-ffi形状の更新
Section titled “集約されたrigor-ffi形状の更新”メインノートのクロージングセクションの計画は3つの調整とともに維持される:
(1)コアプラグインはすでにffxを処理する。ffxは「コアプラグイン」の箇条書きリストにすでにあるFFIサブセット(リテラルのattach_function、ffi_lib、プリミティブ型シンボル)のみを受け付けるため、ffx固有の認識器は不要。これは実質的に朗報——rigor-ffiの最初の具体的な消費者をffx固有の作業ゼロでsqliteffxにできることを意味する。
(2)新しい診断ファミリーの機会 — ffx.unsupported-feature。プロジェクトのgemspecがffx gemを解決する(またはextconf.rbでFFX.create_makefileを使用する)場合、プラグインはffx非互換な宣言を静的に検出して診断できる:
callback :foo, [...], :int→ffx.unsupported-callbackclass S < FFI::Struct→ffx.unsupported-structtypedef :pointer, :handle→ffx.unsupported-typedefenum :state, [:open, :closed]→ffx.unsupported-enumattach_function :printf, [:string, :varargs], :int→ffx.unsupported-varargs- ffx-25リスト外の任意の型シンボル →
ffx.unsupported-type
これらはffxがコンパイルに失敗するケースそのものなので、診断は実際の今日のバグを表面化する——スタイルの好みではない。プロジェクトの設定経由のオプトイン(rigor-ffi設定のtarget: ffx軸)の強力な候補でもある——「ffx gemを使う」は存在ベースであり、デュアルターゲットgemでノイズを表面化すべきでない。
(3)ポインタパラメータ入力セットを拡大。:pointerパラメータの推奨型はFFI::Pointer | Integer | nil(ロバストネス: 入力には寛容に)。:pointerを返す関数の戻り型は厳密でターゲット依存のまま: ffi-gemターゲットはFFI::Pointer、ffxターゲットはInteger。
(4)長期的、オプション — 二次シグネチャソースとしてのFFI0トランポリンメタデータの解析。プラグインがRubyバインディングファイルが隠れている(ベンダーブロブ、カスタムラッパー)がインストール済みの.soが存在するgemに遭遇したとき、FFI0マジック + type-byteブロブを解析することで(name, arity, param_types, return_type)をバイナリから回収できる。これはトランポリンメタデータの「静的解析フレンドリー性」が取れる最も強い形式。初期プラグインのスコープ外(プラットフォーム対応のELF / Mach-O / PE解析が必要)だが将来の方向として記録に値する——ffxエコシステムが持つバイナリタイプスタブに最も近いもの。
更新されたオープンクエスチョン
Section titled “更新されたオープンクエスチョン”メインノートのクロージングセクションの4つの質問に加えて:
- ターゲット検出 —
rigor-ffiはffxターゲットをGemfile.lock/ gemspecのランタイム依存から自動検出すべきか、明示的設定を要求すべきか、それとも両方か? 自動検出はユーザーフレンドリーだがバンドラー / gemspec解析に結合する。 - デュアルターゲットgem — 実世界のgemがffi-gemとffxのコードパスを条件付きで出荷しているか? もしそうなら、
ffx.unsupported-feature診断には宣言ごとの抑制メカニズムが必要(既存の# rigor:disableで十分なはず)。 rigor-fiddleとの境界 —Fiddleは別のStdlib FFIメカニズム(sqliteffxがアウトパラメータスロット読み取りに使用)。rigor-ffiのスコープ内に入れるか、それとも兄弟rigor-fiddleプラグインか? おそらく兄弟——異なるDSLサーフェス、異なるバインディング登録形状。
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.