Porting a C library to Rust

Hello everyone,

I've been fiddling with UDP sockets in Rust within the past two months with the hope of creating something stable to use for a multiplayer game. Though I was able to write a functionally bare UDP server and client, I realized I needed more than just the ability to send and receive packets. I went through several iterations, once using mioco to circumvent blocking, and another separate attempt at UDP reliability (based on this).

After all this, I came to the conclusion that what I had written so far would be incomplete, missing vital features like reliability and ordering. Given that this was something I would want to release to the community eventually, I wanted to ensure that it could be suitable for other projects. This is when I remembered libenet, which is a popular C/C++ networking library for applications.

ENet's purpose is to provide a relatively thin, simple and robust network communication layer on top of UDP (User Datagram Protocol).The primary feature it provides is optional reliable, in-order delivery of packets.

ENet omits certain higher level networking features such as authentication, lobbying, server discovery, encryption, or other similar tasks that are particularly application specific so that the library remains flexible, portable, and easily embeddable.

Their design and implementation has mileage so I think it would be a good option to port over to Rust. I know there exists bindings to this library, but its usage is unsafe, one thing I am trying to avoid. Since safety is at the core of Rust, I am hoping to keep that mantra in mind. I do not think there is anything out there right now which provides what I (and many others might) want.

So I've decided to take a stab at it myself. I don't have any real networking programming experience besides the sandboxing I mentioned above, and I've only begun to wet my feet in Rust recently. In essence I'm a few scratch marks on a tabula rasa.

Something that popped into my head was tokio but I'm not sure if it'd apply or how it would be used to be quite frank. I saw this post on Reddit today which gives a good intro it tokio with a TCP Server in mind. But again, I'm not sure if this is something I'd want to consider for the port.

I am hoping I am not biting off more than I can chew. I was wondering if any of my fellow rustaceans had any ideas or tips (even warnings) before I go about doing this.

Thanks,
Manghi

1 Like

So I started scoping the ENet library to see how things are done. I think asking these specific questions pertaining to their mirroring in Rust is what I should have done in the first place. But hey :wink:.

  1. Heap allocation
    The library allocates packets and commands using malloc onto the heap. While I have yet to use it, I believe the Rust alternative would be to use Box::new(_struct_) anywhere I need to allocate one of these. As long as I have a reference to this, whether on the stack or existing somewhere in a queue, it should still remain allocated and not dropped. Is this the right option?
    .
    Many of these structures also have nested pointers, could these also be boxed up? Quick reference of what I am talking about:
    typedef struct _ENetChannel
    {
       . . .
       LinkedList   incomingReliableCommands;
       LinkedList   incomingUnreliableCommands;
    } ENetChannel;

    typedef struct _ENetPeer
       { 
       LinkedList   dispatchList;
       struct _ENetHost * host;
       . . . 
       void *        data;               /**< Application private data, may be freely modified */
       ENetChannel * channels;
       . . . 
       } 

.
2. List manipulation
The library uses several queues (implemented as linked-lists), inserting at the head and tail, as well as in the middle. I used VecDeque before in one of my previous iterations, but I am wondering if this is the best choice. I had some difficulty while iterating this lists during my searching them during insertions due to borrowed references and trying to mutate them in two places.

I would be moving packets and commands around queues for a given peer (I suppose this detail is irrelevant) and I just want to ensure that their remain alive as long as needed.

I haven't yet messed with lifetimes, but would explicitly specifying lifetimes (and probably figuring out how to properly use them first :stuck_out_tongue:) help with this?

I hope my questions make sense. Thank you all.

If you want non blocking networking yes Tokio is the way to go. I haven't used it for UDP, but it should work fine just as it does for TCP. What you want to do is implement a protocol on top of that, probably implement your own codec.

Personally I wouldn't try to stick too much to the original library's implementation details, because I don't know how much sense it would make in Rust. For example I rarely use Box myself, generally the heap allocations are done under the hood by the standard library (Vec, String, VecDeque and others). The rules for deallocation are the same for stack and for heap: when things get out of scope/when they are not owned anymore, they get dropped.

Lifetimes are used when you use references. If you're using Futures (with Tokio) normally you won't need them too much, because the futures API tends to move things around rather than referencing them.

Maybe start with a simple example of a Codec with tokio_core and go from there?

This is not true of Box, or rather, having a reference to a box's contents won't keep the box alive. If the reference outlives the box, you'll get a compile-time error. Rc/Arc are a way to do this kind of thing. (and their contents are kept on the heap)

I think you make a really good point here. Thank you for your suggestions. I definitely have some research to do in using Tokio and lifetimes.