Why there are no network socket traits in the core/std libraries?

Hello everyone,

I have probably a dumb question – why there are no traits for network sockets in the standard/core library? I am working on library that depends on UDP socket implementation. Initially I binded to std::net::UdpSocket and created socket objects internally. When I decided that it may worth providing no_std implmenetation, it became a hell though. I have looked through several popular crates in order to find out how to deal with std/no_std switching and wast majority of crates' implementations look something like that:

// ...
mod str {
    #[cfg(not(feature = "std))]
    use core::str;
    #[cfg(feature = "std)]
    use std::str;
}
// ...

I found no-std-net crate that provides base no_std network related primitives like ToSocketAddrs trait and SocketAddr and IpAddr related stuff. But that's it. Other libraries like smoltcp, async-std define their own socket structures.

I could make in my library something like pattern above:

// ...
mod net {
    #[cfg(not(feature = "std))]
    use smoltcp::net;
    #[cfg(feature = "std)]
    use std::net;
}
// ...

and deal with reexporing things and resolving interface differences, but that seems as tremendous overhead, since my library just want to send some data over UDP and it does not really care about crate's implementation. For example:

  • UDP socket bind() signature in the smoltcp:
pub fn bind<T: Into<IpEndpoint>>(&mut self, endpoint: T) -> Result<()>
  • UDP socket bind() signature in the std:
pub fn bind<A: ToSocketAddrs>(addr: A) -> Result<UdpSocket>

So, I came to a solution with defining my own traits that implies library required interface to be implemented:

pub trait NtpUdpSocket {
    fn send_to<T: net::ToSocketAddrs>(
        &self,
        buf: &[u8],
        addr: T,
    ) -> core::result::Result<usize, Error>;

    fn recv_from(
        &self,
        buf: &mut [u8],
    ) -> core::result::Result<(usize, net::SocketAddr), Error>;
}

It comes another point that Rust currently does not have - rebalancing coherence. Someone cannot just implement my library trait on the UdpSocket structure they wants. Another wrapper required:

// ...
use std::net::UdpSocket;

struct MyNtpUdpSocketWrap {
    socket: UdpSocket;
}

impl NtpUdpSocket for MyNtpUdpSocketWrap {
    // ...
}

That is, every user of my library has to write such a wrapper every time. Although I can provide trait implementation for std, smoltcp, etc., that may be burdensome since other TCP/IP stack implementation may come to scene.

So, I would like to ask for help with those points:

  • is defining own trait for such a base thing can be considered ideomatic?
  • is it worth preparing RFC for introducing traits for TCP/UDP sockets to be included into the core?
  • maybe there are better options to solve the problem mentioned?
1 Like

Good question! I'm also interested in learning how some experienced Rust programmers would deal with this!

If you want to support no_std, I wouldn't bother writing any code to work with the std::net types. Just write a single no_std-compatible implementation (based on smoltcp or whatever) and that will work fine for std-using consumers too.

2 Likes

Thank you for the response!

Would like to confirm that: my library needs a UDP socket to performs some network operations. I am not completely following the idea to write no_std-only with a TCP/IP stack implementation (e.g. smoltcp). How std users would pass UDP socket objects? Seems like I still end up with providing custom library specific trait.

Code that uses std can call into code that is no_std. The no_std annotation doesn't mean that nobody is allowed to use std. It means that you promise not to depend on it.

Yes, that concept (no_std can be used within std) is quite obvious for me. The part that is not obvious, is that I can not (at least right now I do not see any other options) specify std/no_std compliant inteface for the networking stuff. core lib does not have anything related to network - neither taits nor structs for networking.
And initial question was to confirm whether trait definition is the most appropriate way to generilize my lib interface or not. :thinking:
Also I was wondering why there are no such traits in std/core, because it seems quite common to have a lib that may acquire a network resource, but does not care about implementation.
So, I am looking for the most ideomatic way to allow my library to deal with network objects. Right now my interface looks something like that:

pub fn request_with_addrs<A, U, T>(
    pool_addrs: A,
    socket: U,
    mut context: NtpContext<T>,
) -> core::result::Result<NtpResult, Error>
where
    A: net::ToSocketAddrs + Copy + Debug,
    U: NtpUdpSocket + Debug,
    T: NtpTimestamp + Copy,
{
    // ...
}

Where NtpUdpSocket (presented in the initial message) and NtpTimestamp are traits that specify required behavior of objects required, that a library user has to implement. But it seems quite annoying to force to implement at least UdpSocket trait, because an user cannot implement my traits in theirs crate without introducing a wrapper object. Possible implementation:

#[derive(Debug)]
struct UdpSocketWrapper(UdpSocket);

impl NtpUdpSocket for UdpSocketWrapper {
    fn send_to<T: ToSocketAddrs>(
        &self,
        buf: &[u8],
        addr: T,
    ) -> Result<usize, Error> {
        match self.0.send_to(buf, addr) {
            Ok(usize) => Ok(usize),
            Err(_) => Err(Error::Network),
        }
    }

    fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, SocketAddr), Error> {
        match self.0.recv_from(buf) {
            Ok((size, addr)) => Ok((size, addr)),
            Err(_) => Err(Error::Network),
        }
    }
}

fn foo() {
    let sock_wrapper = UdpSocketwrapper(std::net::UdpSocket::bind("0.0.0.0:0").expect("..."));
    let context = // ...;
    let result = request_with_addrs("addr:port", sock_wrapper, context);
}

Quite burdensome for me. :smiley:
It looks like it should be a set of traits in the core that allows specifying required methods, for example:

trait SendTo {
    fn send_to(...);
}

trait RecvFrom {
    fn recv_from(...);
}

That way we would have more clear boundaries and any new TCP/IP stack writers can maintain generic interface that common among all such implementation (at least that is how I see it right now).

There was discussion years ago about allowing crate authors to pick and choose which parts of std to depend on, but I'm not sure how that ended or fizzled out. Mozilla layoffs sure didn't help.

1 Like

The default answer for traits in general is to not have them in std. See, for example, how there's not a trait for "unsigned number" either, despite it being a common request.

One important reason for this is that, unlike OOP interfaces, you can implement your own traits for std's types. So if std doesn't need the trait, then it can just not decide what it should look like, and leave it up to others -- who, importantly, are allowed to make breaking changes on major version bumps of their crates, unlike std which is basically stuck forever.

1 Like

I think it is. If you want to expose or consume common functionality generically, traits are the way to go.

I think in std, it might just be a missing piece of functionality. If they are added to std, I'm pretty sure they still won't be added to core, since core is expected to work with no dependencies or assumptions on the existence of any sort of runtime/operating system, which is required for networking.

1 Like

Thank you. I have the same thoughts, but would like to confirm that with someone else. That makes sense. Having generic network interface in terms of traits may be useful though. Since traits do not oblige to have any dependencies, it may be just a contract other developers can follow or not.
On the other hand, it may be easier to provide a crate with such a traits, like num does.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.