Confused about {From,Into}RawFd vs {From,Into}<OwnedFd>

I'm trying to figure out whether a type should implement FromRawFd and IntoRawFd as well as, or instead of, From<OwnedFd> for MyType and From<MyType> for OwnedFd.

The type wraps an OwnedFd and endows it with a bunch of methods (that make ioctl calls on the device that we presume the file descriptor refers to, but that's not important right now -- what matters is that this type stands in basically the same relation to OwnedFd that File does, but its purpose is different).

All the trait documentation says on the subject is

This [trait] is typically used to consume ownership of the specified file descriptor. When used in this way, the returned object will take responsibility for closing it when the object goes out of scope.

However, consuming ownership is not strictly required. Use a From<OwnedFd>::from implementation for an API which strictly consumes ownership.

In my case, the API definitely is intended to transfer ownership (for both conversions) and so it seems like maybe I ought to implement only From<OwnedFd>? But it's not saying anything concrete or definite enough for me to be sure.

1 Like

It sounds like implementing conversions from OwnedFd is desirable since your API is intended to transfer ownership from native Rust types which own file descriptors (e.g. File) and whether FromRawFd should be implemented is the question. My interpretation is RawFd is intended for cases where raw integers are used as fds (e.g. FFI) and an unsafe conversion to an owned type is needed. I think this is implied in the subsequent section of the from_raw_fd

Safety
The fd passed in must be an owned file descriptor; in particular, it must be open.

If these use cases are required, then implementing the FromRawFd trait would make sense, like File does. Implementing this would put the onus on the user to ensure their fd is "owned" (I/O safe).

The docs for FromRawFd do mention both "However, consuming ownership is not strictly required" and "The fd passed in must be an owned file descriptor". My interpretation of this is that the provided file raw descriptor must be owned by the caller such that the FromRawFd implementation could consume and own the file descriptor. However, the implementation is not required to take ownership (i.e. can borrow) and the "source" of the file descriptor can maintain ownership. I think this would depend on implementation and documentation.

it depends on the intention of your API design. the documentation doesn't mandate you must do it this way.

since the associated function FromRawFd::from_raw_fd() is unsafe (as opposed to an unsafe trait), it makes practically no difference with regard to safety than From<OwnedFd>::from(), if you implement it in the same way. the caller is responsible to hold the safety precondition, that the argument fd must be an owned file descriptor.

if you only implement From<OwnedFd>, then the caller must use <OwnedFd as FromRawFd>::from_raw_fd() first, which requires the exactly the same safety precondition.

in other word, it is just a "shorthand" for the user if you implement FromRawFd:

impl FromRawFd for MyType {
    unsafe fn from_raw_fd(fd: RawFd) -> Self {
        // safety: the caller guarantees `fd` is an owned descriptor
        OwnedFd::from_raw_fd(fd).into()
    }
}

EDIT:

note, if your type is some kind of reference type, you cannot implement From<OwnedFd> (without leaking resources), but it is still ok to implement FromRawFd.

the caller of (the unsafe function) FromRawFd::from_raw_fd() is required to hold the safety requirement, but is also made aware it does NOT necessarily take ownership of the argument, as per the trait's documentation.

impl<'a> FromRawFd for MyType<'a> {
    /// the `fd` will be borrowed for the lifetime of `'a`
    /// caller must ensure it is open during `'a`
    unsafe fn from_raw_fd(fd: RawRd) -> Self {
        todo!()
    }
}

generally, the safety requirement for an unsafe trait is for the implementer, while the safety requirement for an unsafe function is for the caller. to me, the semantics of safe trait with unsafe functions is always somewhat vague. in the example above, the trait's documentation requires the argument to be an owned descriptor and must be open, but it didn't say anything about lifetimes. on the other hand, I would argue it's reasonable to assume some lifetime requirement is implied if the type contains a lifetime. that's just the way I interpret it.