Rust と C が互いに呼び出すためには、呼び出し規約、型のレイアウト、リソースのセマンティクスを同時に整合させる必要があります。関数のシグネチャからは引数がポインタであることは分かりますが、通常、それが指す要素数、有効期間、解放元、そしてコールバックが別のスレッドから実行される可能性があるかどうかは分かりません。
bindgen は宣言とレイアウトの自動生成を担い、安全なラッパーは外部契約を Rust が維持できる不変条件に変換します。生成された関数に unsafe を付けるだけでは、安全なラッパーにはなりません。
C ヘッダーとビルド設定 対象 ABI・マクロ定義・フィールド配置・公開シンボル 1 生のRustバインディング extern 宣言、裸のポインタ、および C 互換型 2 手動による安全な抽象化の構築 長さ、所有権、ライフサイクル、スレッド、およびエラー規約の検証 3 安全なRustの呼び出し元 不変条件が維持されているインターフェースを通じてのみ操作可能 4 安全なFFIラッパー: シグネチャは契約の一部に過ぎない bindgen は宣言を生成できますが、ポインタのシグネチャからはすべてのセマンティクスを推論できません。
3つの「互換性」をそれぞれ見る
extern "C" は対象プラットフォームの C 呼び出し規約を指定し、repr(C) は構造体などの型の C レイアウト規則を指定し、C 型エイリアスは対象プラットフォームの基礎となる型と一致します。これらは互いに代替できません。
C インターフェースの型 Rust での表現 確認すべき点
int、long core::ffi::c_int、c_long 対象のエイリアスを使用し、ホストの幅をハードコードしない
size_t bindgen が生成する型(通常は usize) 対象の ABI と生成オプション
const char* *const c_char NUL 終端文字列か、長さ付きのバイト領域か
void* *mut c_void 所有権、実際のオブジェクト型、および null 許容性
関数ポインタ(NULL 可) Option<extern "C" fn(...)> など 引数、戻り値の型、unsafe、およびスレッド規約
C enum 通常は整数型に定数、または生成された新しい型 列挙された項目だけでなく、あり得るすべての入力値
一般的な 64 ビット Unix では LP64、64 ビット Windows では LLP64 が使用されるため、long の幅が異なります。解決策は実際の C long に一致させることです。Windows で強制的に i64 に置き換えると、移植性を修復するのではなく宣言を直接壊してしまいます。
repr(C) はフィールド内部型のデフォルトのレイアウトを再帰的に変更しません。String や Vec を repr(C) 構造体に含めても、C が理解できるコンテナにはなりません。フィールドの互換性、アラインメント、パッキング、およびコンパイルオプションを一緒に確認する必要があります。C enum の基底型はコンパイラのオプションによって影響を受ける可能性があり、また C では Rust の列挙型で定義されていない整数値を保持できます。無効な Rust の discriminant を直接構築すると未定義動作になります。Rust 型レイアウト
完全な bindgen プロジェクト
以下では、バイトスライスを同期的に読み取る C 関数を例に契約を説明します。この関数はポインタを保持せず、n=0 の場合は p にアクセスしません。n>0 の場合、呼び出し元は少なくとも n バイトの読み取り可能領域を提供する必要があります。結果はすべてのバイトの排他的論理和(XOR)です。
Cargo.toml:
[package]
name = "ffi-demo"
version = "0.1.0"
edition = "2024"
[build-dependencies]
bindgen = "0.72"
cc = "1"
wrapper.h:
#ifndef DEMO_WRAPPER_H
#define DEMO_WRAPPER_H
#include < stddef.h>
#include < stdint.h>
uint8_t demo_xor ( const uint8_t * p , size_t n ) ;
#endif
demo.c:
#include " wrapper.h"
uint8_t demo_xor ( const uint8_t * p , size_t n )
{
uint8_t result = 0 ;
for ( size_t i = 0 ; i < n; + + i)
result ^= p[ i] ;
return result;
}
build.rs:
use std:: { env, path:: PathBuf} ;
fn main ( ) {
println! ( " cargo:rerun-if-changed=wrapper.h" ) ;
println! ( " cargo:rerun-if-changed=demo.c" ) ;
cc:: Build:: new( ) .file ( " demo.c" ) .compile ( " demo" ) ;
let bindings = bindgen:: Builder:: default( )
.header ( " wrapper.h" )
.allowlist_function ( " demo_xor" )
.parse_callbacks ( Box :: new( bindgen:: CargoCallbacks:: new( ) ) )
.generate ( )
.expect ( " generate bindings" ) ;
bindings.write_to_file (
PathBuf:: from( env:: var( " OUT_DIR" ) .expect ( " OUT_DIR" ) )
.join ( " bindings.rs" ) ,
) .expect ( " write bindings" ) ;
}
src/main.rs:
mod raw {
include! ( concat! ( env! ( " OUT_DIR" ) , " /bindings.rs" ) ) ;
}
fn xor_bytes ( bytes : & [u8 ]) -> u8 {
unsafe { raw:: demo_xor( bytes.as_ptr ( ) , bytes.len ( ) ) }
}
fn main ( ) {
assert_eq! ( xor_bytes ( & [ ] ) , 0 ) ;
assert_eq! ( xor_bytes ( & [ 1 , 2 , 3 , 4 ] ) , 4 ) ;
println! ( " FFI checks passed" ) ;
}
cargo run を実行するには、C コンパイラと bindgen が使用する libclang が必要です。cc は C 実装をビルドしてリンクし、bindgen は Rust の宣言を生成するだけです。生成されたファイルを含めるだけでは、ネイティブ関数の実装が自動的に提供されるわけではありません。bindgen ビルドチュートリアル
クロスコンパイルの場合、Clang のターゲット、sysroot、インクルードパス、およびマクロが、実際にライブラリをコンパイルする設定と一致している必要があります。生成に成功したからといって、ヘッダーが解析できるだけでなく、バインディングとデプロイされたバイナリの ABI が一致するとは限りません。重要な構造体については、両端で sizeof、alignof、およびフィールドのオフセットを比較できます。
ポインタのライフサイクルと解放元
CStr::from_ptr は有効な NUL 終端文字列を要求し、スキャン範囲と借用期間の両方で読み取り可能である必要があります。任意の裸のアドレスを検証するものではなく、バイトが UTF-8 であることを証明するものでもありません。CString は Rust が割り当てた C 文字列を所有し、as_ptr の借用はオブジェクトのライフサイクルを超えてはなりません。
境界を跨ぐリソースの移譲には、対となるプロトコル(例:create/destroy、または Rust による割り当てと Rust がエクスポートする解放関数)を提示する必要があります。Box や CString を無造作に C の free で解放したり、Rust の割り当てレイアウト契約に適合しない C メモリに対して Vec::from_raw_parts を呼び出したりしてはいけません。
C がポインタを保持する場合、一時的なスタックオブジェクトや 1 回の関数呼び出し内の借用では通常不十分です。登録解除や保留中のコールバックの完了待機後に解放を保証する所有権オブジェクトが必要です。単にアドレスを usize に変換しても、ライフサイクルは延長されません。
create/destroy をコピーできないオブジェクトとしてラップする
同期スライスの例ではメモリを借用しているだけです。C が長期リソースを返す場合、不透明型を使用してレイアウトを隠蔽し、Rust が関数を通じてのみアクセスできるようにできます。以下は、前述のプロジェクトに拡張を加えた完全な手順です。wrapper.h の #endif の前に宣言を追加します。
typedef struct demo_counter demo_counter ;
demo_counter * demo_counter_new ( uint32_t initial ) ;
uint32_t demo_counter_get ( const demo_counter * p ) ;
int demo_counter_add ( demo_counter * p , uint32_t amount ) ;
void demo_counter_free ( demo_counter * p ) ;
demo.c に実装を追加します。教学インターフェースでは、初期値は 100 を超えられないこと、作成に失敗した場合は NULL を返すこと、add のオーバーフローは 1 を返し元の値を変更しないこと、成功した場合は 0 を返すことが規定されています。get/add の引数は、まだ存続しているオブジェクトを指している必要があります。
#include < stdlib.h>
struct demo_counter { uint32_t value; } ;
demo_counter * demo_counter_new ( uint32_t initial ) {
if ( initial > 100 ) return NULL ;
demo_counter * p = malloc ( sizeof * p) ;
if ( p) p-> value = initial;
return p;
}
uint32_t demo_counter_get ( const demo_counter * p ) {
return p-> value;
}
int demo_counter_add ( demo_counter * p , uint32_t amount ) {
if ( UINT32_MAX - p-> value < amount) return 1 ;
p-> value += amount;
return 0 ;
}
void demo_counter_free ( demo_counter * p ) {
free ( p ) ;
}
build.rs で allowlist_function("demo_xor") を allowlist_function("demo_.*") に変更し、新しい関数も宣言が生成されるようにします。必要なパラメータ型は依存関係に従って含まれます。その後、src/main.rs を以下の所有型ラッパーに置き換えます。bindgen allowlist
demo_counter_new NULL: 拒否または割り当て失敗 Counter(NonNull) 一意の所有と非公開フィールド Drop C の free 関数を呼び出す add 失敗: エラーを返し、数値は変更されない 同じ Counter によって所有され、解放される 不透明ハンドル: 作成に成功して初めて所有状態に入る NonNull は NULL を除外するのみです。有効なオブジェクト、一意の解放権、およびスレッドの規則は依然としてインタ ーフェース契約に由来します。
use std:: ptr:: NonNull;
# [ allow ( non_camel_case_types ) ]
mod raw {
include! ( concat! ( env! ( " OUT_DIR" ) , " /bindings.rs" ) ) ;
}
struct Counter ( NonNull< raw:: demo_counter> ) ;
impl Counter {
fn new ( initial : u32 ) -> Result< Self , & 'static str > {
let pointer = unsafe { raw:: demo_counter_new( initial) } ;
NonNull:: new( pointer) .map ( Self ) .ok_or ( " counter creation failed" )
}
fn value ( & self ) -> u32 {
unsafe { raw:: demo_counter_get( self .0. as_ptr ( ) ) }
}
fn add ( & mut self , amount : u32 ) -> Result< ( ) , & 'static str > {
match unsafe { raw:: demo_counter_add( self .0. as_ptr ( ) , amount) } {
0 => Ok ( ( ) ) ,
_ => Err ( " counter addition failed" ) ,
}
}
}
impl Drop for Counter {
fn drop ( & mut self ) {
unsafe { raw:: demo_counter_free( self .0. as_ptr ( ) ) }
}
}
fn main ( ) {
assert_eq! ( unsafe { raw:: demo_xor( std:: ptr:: null( ) , 0 ) } , 0 ) ;
assert! ( Counter:: new( 101 ) .is_err ( ) ) ;
let mut counter = Counter:: new( 10 ) .expect ( " create counter" ) ;
counter.add ( 5 ) .unwrap ( ) ;
assert_eq! ( counter.value ( ) , 15 ) ;
assert! ( counter.add ( u32 :: MAX ) .is_err ( ) ) ;
assert_eq! ( counter.value ( ) , 15 ) ;
drop ( counter) ;
}
ラッパーは Clone を実装せず、生のポインタコンストラクタも公開しないため、2 つの所有者がそれぞれ同じリソースを解放することを防ぎます。&mut self は更新要件を Rust の呼び出し元に明示します。もし元のライブラリが別のスレッドからオブジェクトを自分で変更する場合は、追加で同期契約を構築する必要があり、このシグネチャだけでは不十分です。
NonNull は Send/Sync を自動的に実装しないため、このラッパーは勝手にスレッド間の使用を保証するものではありません。また、アドレスの読み取り可能性やオブジェクト型の正しさを証明するものでもなく、new の外部保証が有効性の源となります。Drop は通常のスコープ外への退出と展開パスを処理します。mem::forget やプロセス終了の場合、リソースが解放されない可能性があるため、メモリ安全性は「デストラクタが常に実行されること」に依存してはいけません。NonNull
この作成インターフェースは、「初期値が許可されない」と「割り当て失敗」を NULL に統合しているため、Rust は C が捨てたエラー原因を信頼できる形で再構築できません。区別が必要な場合は、NULL から具体的な原因を推測するのではなく、C レベルでステータスコードと出力ハンドルプロトコルを設計すべきです。
コールバック:スレッド、再帰呼び出し、および解除は関数ポインタよりも重要
callback + void* context を登録する際は、以下の 4 点を明確にしてください:呼び出しはどのスレッドで行われるか、並行して行われるか、再帰呼び出しされるか、そして解除が返った後に保留中のコールバックがないことが保証されるか。Rust 側では、ロック、アトミック操作、スレッド制限、または他の所有権構造を使用してラップするかを決定します。
C のシグネチャに const がないからといって、すぐに &mut T を構築しないでください。排他参照は、借用期間全体で Rust のエイリアスと有効性の規則を満たすことを要求しますが、外部コールバックの再帰呼び出しや並行アクセスはこれを違反する可能性があります。Rustonomicon FFI
登録解除後も context にアクセスするのは誰か
登録解除だけでは足りません。このプロトコルでは先に入ったコールバックがまだアクセス中です。終了前に context の解放を試すと依存が残ることが分かります。安全なラッパは、新規アクセスがないことと最後の既存アクセスが終わったことの両方を確立します。
図を準備しています 再読み込み
登録解除後も context にアクセスするのは誰か 新規コールバックの停止と実行中の終了待ちの両方が必要で、関数ポインタの型はこの時系列を表しません。
{"id":"rust-ffi-callback-lifetime","title":"登録解除後も context にアクセスするのは誰か","summary":"新規コールバックの停止と実行中の終了待ちの両方が必要で、関数ポインタの型はこの時系列を表しません。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"enter\" data-i18n=\"enter\"></button><button id=\"leave\" data-i18n=\"leave\"></button><button id=\"unregister\" data-i18n=\"unregister\"></button><button id=\"free\" data-i18n=\"free\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><svg id=\"map\" class=\"diagram\" viewBox=\"0 0 360 180\" role=\"img\" data-i18n-label=\"heading\"></svg><div class=\"metrics\"><div><span data-i18n=\"active\"></span><strong id=\"active\"></strong></div><div><span data-i18n=\"registration\"></span><strong id=\"registration\"></strong></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"initial\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"safe\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"bad\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:550;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:550 20px ui-monospace,monospace}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px ui-monospace,monospace;background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px ui-monospace,monospace}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 ui-monospace,monospace;white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:ui-monospace,monospace}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px ui-monospace,monospace;padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 ui-monospace,monospace;overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.chart-wrap{margin:18px 0}.chart-label{font-size:12px;color:var(--muted);min-height:3.2em}.curve{display:block;width:100%;height:auto;max-height:320px;overflow:visible}.curve text{font:11px ui-monospace,monospace;fill:var(--muted)}.gridline{stroke:var(--rule);stroke-width:1}.metrics strong{font-size:20px;min-height:3.2em}.formula{font:12px/1.8 ui-monospace,monospace;min-height:9em}.readout{min-height:8em}.presets button{flex-basis:30%}.scope p{overflow-wrap:anywhere} .unit-note{font-size:12px;color:var(--muted);margin:10px 0}\n\n.metrics span{word-break:break-all;line-break:strict}.metrics>div{padding-right:6px}\n\n.cell{min-height:72px}.metrics strong{min-height:3.6em}\n\n ","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';row.style.gridTemplateColumns='repeat('+r.length+',minmax(0,1fr))';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function plot(id,series,xr,yr,opts={}){const e=$('#'+id),X=x=>42+(x-xr[0])/(xr[1]-xr[0])*298,Y=y=>175-(y-yr[0])/(yr[1]-yr[0])*148;let out='<defs><clipPath id=\"clip-'+id+'\"><rect x=\"42\" y=\"27\" width=\"298\" height=\"148\"/></clipPath></defs>';for(let i=0;i<3;i++){let x=xr[0]+i*(xr[1]-xr[0])/2,y=yr[0]+i*(yr[1]-yr[0])/2;out+='<path class=\"gridline\" d=\"M '+X(x)+' 27V175M42 '+Y(y)+'H340\"/><text x=\"'+X(x)+'\" y=\"194\" text-anchor=\"middle\">'+esc(opts.xfmt?opts.xfmt(x):Number(x.toFixed(2)))+'</text><text x=\"36\" y=\"'+(Y(y)+4)+'\" text-anchor=\"end\">'+esc(opts.yfmt?opts.yfmt(y):Number(y.toFixed(2)))+'</text>';}out+='<g clip-path=\"url(#clip-'+id+')\">';for(const s of series){out+='<path fill=\"none\" stroke=\"'+(s.color||'var(--accent)')+'\" stroke-width=\"2.5\" '+(s.dash?'stroke-dasharray=\"5 4\"':'')+' d=\"'+s.pts.map((p,i)=>(i?'L':'M')+X(p[0]).toFixed(2)+','+Y(p[1]).toFixed(2)).join(' ')+'\"/>';}for(const p of opts.points||[])out+='<circle cx=\"'+X(p[0])+'\" cy=\"'+Y(p[1])+'\" r=\"4\" fill=\"var(--second)\" stroke=\"var(--paper)\" stroke-width=\"1.5\"/>';if(opts.cursor!==undefined)out+='<path d=\"M'+X(opts.cursor)+' 27V175\" stroke=\"var(--muted)\" stroke-dasharray=\"3 3\"/>';e.innerHTML=out+'</g>';e.dataset.curves=JSON.stringify(series.map(s=>s.pts));}const curve=(fn,lo,hi,n=120)=>Array.from({length:n+1},(_,i)=>{const x=lo+(hi-lo)*i/n;return[x,fn(x)]});function inputs(draw){document.querySelectorAll('input,select').forEach(e=>{e.addEventListener('input',draw);e.addEventListener('change',draw)});draw()}const n=id=>+$('#'+id).value;const show=(id,v,d=3)=>set(id,Number(v.toFixed(d)));let open=true,active=0,alive=true,msg='initial';function draw(){graph('map',[{name:'C API',x:60,y:90,cls:open?'on':''},{name:'calls',x:180,y:90,cls:active?'gold':''},{name:'context',x:300,y:90,cls:alive?'on':''}],alive?[...(open?[[0,2,true]]:[]),...(active?[[1,2,true]]:[])]:[]);set('active',active);set('registration',t(open?'open':'closed'));$('#enter').disabled=!open||!alive||active>=3;$('#leave').disabled=!active;$('#unregister').disabled=!open;$('#free').disabled=!alive;set('explanation',t(msg));document.body.dataset.alive=alive;}$('#enter').onclick=()=>{active++;draw()};$('#leave').onclick=()=>{active--;draw()};$('#unregister').onclick=()=>{open=false;draw()};$('#free').onclick=()=>{if(open||active)msg='bad';else{alive=false;msg='safe'}draw()};$('#reset').onclick=()=>{open=true;active=0;alive=true;msg='initial';draw()};draw();","audio":false,"strings":{"active":"進行中のコールバック","alive":"context 生存中","bad":"解放を拒否します。新規が入る可能性があるか、進行中の呼び出しが context にアクセスします。","closed":"停止","enter":"コールバック開始","free":"context の解放を試す","gone":"context 解放済み","heading":"登録解除後も context にアクセスするのは誰か","initial":"呼び出しを開始してから入口を止めます。入口の停止だけでは実行中の呼び出しは終わりません。","intro":"進行中のコールバックを開始し、新規を停止してから context の解放を試します。","leave":"コールバック終了","open":"有効","registration":"新規コールバックの入口","reset":"リセット","safe":"入口が停止し実行中もゼロなので、context を解放できます。","scope":"unregister は新規だけを止め、進行中は join/drain で確認する前提です。実際はライブラリの契約に従います。","scopeLabel":"モデルの範囲と前提","unregister":"新規コールバック停止"}}
パニックと言語間展開
通常の extern "C" は、展開(unwinding)が越えることを許可しない ABI です。Rust のパニックがこのような境界を越えようとするとプロセスは終了します。「すべての Rust パニックは C 境界を跨いで未定義動作(UB)になる」という一般的な言い伝えをそのまま受け継ぐことはできません。一方、許可されていない展開の Rust 境界に外部例外が入ると、未定義動作のリスクがあります。
本当に境界を跨ぐ展開が必要な場合は、双方のツールチェーンがサポートする C-unwind を使用し、全体としての例外プロトコルを構築する必要があります。通常の C ライブラリインターフェースでは、Rust のコールバック内で捕捉可能なパニックをエラーに変換するのが一般的ですが、catch_unwind は展開型パニックのみを捕捉し、panic=abort は捕捉できません。また、任意の外部例外に対する汎用捕捉器でもありません。復元後にデータ構造が依然として一貫しているかどうかは、アプリケーション側で責任を負います。FFI 展開ルール
Rust 2024 では、外部宣言には unsafe extern ブロックの使用が義務付けられています。固定されたシンボル名をエクスポートするには、#[unsafe(no_mangle)] などの対応する構文を使用します。unsafe 属性は名前衝突などのグローバルな義務を強調しますが、関数に C 呼び出し規約を自動的に付与するものではありません。Rust 外部ブロック
自動生成の境界
bindgen は、長さパラメータ間のセマンティクス、ポインタの保持に関する約束、またはスレッド制約を理解しません。複雑な C++ テンプレートやインライン関数の処理も、「すべてサポートできない」と単純化できるものではありません。一部のシナリオでは、ラッパーの生成、具体化されたインスタンスの処理、または追加のコンパイル補助ファイルの処理が可能であり、これはオプションと制限に依存します。複雑な C++ インターフェースでは、まず明確な C インターフェース層を整理することが推奨されることがよくあります。bindgen FAQ
生成層は機械的かつ再構築可能であり、安全層は各 unsafe が依存する外部保証を記録し、呼び出し元が通常の安全な操作を通じてそれを破壊できないようにします。契約の推論の詳細については、unsafe の soundness を参照してください。