PhantomData 4/4: OwnedFd と BorrowedFd

BorrowedFd は、PhantomData が実際に活用されている代表的な例です。

// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0

use std::marker::PhantomData;
use std::os::raw::c_int;

mod libc_ffi {
    use std::os::raw::{c_char, c_int};
    pub unsafe fn open(path: *const c_char, oflag: c_int) -> c_int {
        3
    }
    pub unsafe fn close(fd: c_int) {}
}

struct OwnedFd {
    fd: c_int,
}

impl OwnedFd {
    fn try_from_fd(fd: c_int) -> Option<Self> {
        if fd < 0 {
            return None;
        }
        Some(OwnedFd { fd })
    }

    fn as_fd<'a>(&'a self) -> BorrowedFd<'a> {
        BorrowedFd { fd: self.fd, _phantom: PhantomData }
    }
}

impl Drop for OwnedFd {
    fn drop(&mut self) {
        unsafe { libc_ffi::close(self.fd) };
    }
}

struct BorrowedFd<'a> {
    fd: c_int,
    _phantom: PhantomData<&'a ()>,
}

fn main() {
    // 書き込み専用と作成権限を指定した生の syscall でファイルを作成します。
    let fd = unsafe { libc_ffi::open(c"c_str.txt".as_ptr(), 065) };
    // 整数のファイルディスクリプタの所有権を `OwnedFd` に渡します。
    // `OwnedFd::drop()` はファイルディスクリプタを閉じます。
    let owned_fd =
        OwnedFd::try_from_fd(fd).expect("syscall でファイルを開けませんでした!");

    // `OwnedFd` から `BorrowedFd` を作成します。
    // `BorrowedFd::drop()` はファイルを所有していないため、ファイルを閉じません!
    let borrowed_fd: BorrowedFd<'_> = owned_fd.as_fd();
    // std::mem::drop(owned_fd); // ❌🔨
    std::mem::drop(borrowed_fd);
    let second_borrowed = owned_fd.as_fd();
    // owned_fd はここで drop され、ファイルは閉じられます。
}
  • ファイルディスクリプタは、特定のプロセスによるファイルへのアクセスを表します。

    補足: デバイスや OS 固有の機能は、unix 系システムではファイルであるかのように公開されます。

  • OwnedFd は、ファイルディスクリプタの所有権を持つラッパー型です。これはファイルディスクリプタを_所有_し、 drop されるとそれを閉じます。

    注: ここでは独自実装を使っているので、明示的な Drop 実装に注目してください。

    BorrowedFd はその借用版にあたる型であり、drop されてもファイルを閉じる必要はありません。

    注: BorrowedFd には Drop を明示的に実装していません。

  • BorrowedFd は、PhantomData で捉えたライフタイムを使って、 「このファイルディスクリプタが存在するなら、それを閉じる責任を負っていなくても、 OS のファイルディスクリプタは依然として開いている」という不変条件を強制します。

    BorrowedFd のライフタイムパラメータは、プログラム内に、その特定の BorrowedFd と同じだけ長く生存するか、あるいはそれより長く生存する別の値が存在することを要求します (この場合は OwnedFd です)。

    実演: std::mem::drop(owned_fd) の行をコメント解除してコンパイルしてみると、 borrowed_fdowned_fd のライフタイムに依存していることを示せます。

    これは API 設計者によって、「その別の値こそがファイルへのアクセスを開いたままにしている」 という意味になるよう API にエンコードされています。

    Rust の borrow checker は、一方の値がもう一方の値と少なくとも同じだけ長く生存しなければならない というこの関係を強制するため、この API の利用者は、正しいファイルディスクリプタのエイリアシングや クローズのロジックを自分で扱う必要がありません。