Tech News
← Back to articles

“Bypassing” specialization in Rust

read original related products more articles

"Bypassing" specialization in Rust or How I Learned to Stop Worrying and Love Function Pointers

I've spent nearly a year developing and refining my own FAT driver in Rust. For much of the last six months, I had to put the project on hold due to school commitments. However, I'm back now, especially since this project has become my most-starred repository on GitHub. During that journey, I (almost) learned how FAT and filesystems in general work behind-the-scenes and in my attempts to navigate the constraints imposed by the Rust programming language, I encountered what I thought was an immovable obstacle: specialization

A quick word about specialization

Specialization as a concept was introduced with RFC 1210 all the way back to 2015, long before I even had a serious passion about computers and programming. For our use case, specialization allows us to essentially override trait and struct impl s. Let me demonstrate with an example:

trait A { } trait B { } struct MyStruct < S > ( S ) where S : A ; impl < S > MyStruct < S > where S : A { fn do_something ( & self ) { } } impl < S > MyStruct < S > where S : A + B { fn do_something ( & self ) { } }

In Rust, the above code wouldn't compile, since we define the function do_something twice. With specialization, the compiler would realize that the second impl is more "specialized" than the first one and the above code would compile just fine. Then, if we constructed the struct MyStruct with its only field containing an object that implements trait A but not trait B , then we did the same but with an object that now implements both A and B , and called the do_something method on each one struct, the first do_something implementation and second do_something implementation would be run respectively. Cool, so why do the above code not compile? Well, I am not an expert in Rust soundness and safety, but according to the tracking issue (#31844) and some Zulip chats and Rust user forum threads I've found, there's a lifetime-related issue with the whole feature that no one knows how to easily solve. There's another feature, min-specialization which seems to not suffer from that problem to the same extent, but its still unstable, and since I don't wanna deal with safety issues and I also would like my project to be on the stable toolchain, that won't suit us.

Why do we need specialization anyways?

That's a good question. It all comes down to how I've come to choose to communicate with the storage device. FAT filesystems are subdivided to sectors, where each sector stores a fixed amount of data, which can be either 512, 1024, 2048 or 4096 bytes (there are also clusters, which are subdivisions of file data and whose size is an integer multiple of the sector size, but we don't care about clusters right now because of what you are gonna read below). Each time we want to read or modify a certain piece of data, we load that sector to a sector buffer, which is just an array, we read our data from the buffer or directly modify them there, and when we want to load another sector, we first sync the buffer back to the filesystem if there are any changes. The issue might not be apparent right now, but it will be in a bit.

To implement a FileSystem struct that can be either Read-Only or Read/Write, we should define read-related methods in impl blocks that require the storage to support Read and Seek . Write -related methods should be defined in blocks that require Read , Write , and Seek . Let's take a look into our hypothetical function to loads the corresponding sector into the sector buffer.

impl < S > FileSystem < S > where S : Read + Seek { fn load_nth_sector ( & mut self ) -> Result < ( ) , S :: Error > { } }

... continue reading