Skip to content

Instantly share code, notes, and snippets.

@dzfranklin
Created April 24, 2021 17:34
Show Gist options
  • Save dzfranklin/10d2cf09d5b6d4d93daa8eab971c48af to your computer and use it in GitHub Desktop.
Save dzfranklin/10d2cf09d5b6d4d93daa8eab971c48af to your computer and use it in GitHub Desktop.
Edit: TLDR: If you need to talk to a library that's very hard to write a completely sound abstraction for, consider writing a mostly sound one (with a clear disclaimer) instead of switching to a different language. Obviously this would be stupid if your code operates on untrusted inputs, but plenty of stuff doesn't. I'm writing code that takes input from your kernel.
I've been thinking recently about various articles by people who have given up on Rust projects, such as [the Way Cooler post mortem](http://way-cooler.org/blog/2020/01/09/way-cooler-post-mortem.html). I think one of the problems is that people sometimes feel it's "wrong" to write unsound code in Rust, so they give up or switch to another language, write unsound code in that language, and feel better about it. By unsound I mean unsafe code that is undefined behavior, i.e. violates the memory safety model of rust.
I find it interesting that I'm my experience people newer to writing unsafe Rust tend to see soundness in binary terms, whereas at least some of the more experienced people see it as a gradient. It's very hard to write code that conforms to the Rust Memory Model when it isn't actually totally defined yet. I think a crate can, if it's upfront about it, define unsafe as meaning "here be dragons" and safe as meaning "I haven't found any dragons here yet". In certain contexts that's more useful than making everything unsafe.
I'm currently working on a program to use one laptop as a virtual monitor for another (like Apple Sidecar). I found an awesome kernel module and userspace c library that provides a toolkit for exactly this sort of program. So I did the standard dance of writing bindings: `cargo new --lib evdi-sys`, create [`build.rs`](https://build.rs), `cargo new --lib evdi-rs`, start writing abstractions. After a while, though, I realized I was [shaving a yak](https://americanexpress.io/yak-shaving/). While it would be cool if someone else found evdi-rs useful, practically speaking I should focus on enabling what I wanted to work on.
The next thing I realized was that libevdi is fundamentally unsound. This isn't to say it's not a great library: the authors decided to focus on making it work for the use cases they needed, accepting that not every edge case would work right. Since it's interacting with very complicated bits of the kernel it's also full of logic bugs that practically speaking cause more of the user-visible bugs anyway. I could try to read the source code of libevdi, find every time it did something unsound, and ensure "my users" could never cause it to happen. First off, that sounds like a huge amount of work. Secondly, remember Daniel that "your users" is just you.
I realized that evdi-rs was never going to be a "rusty" library when I started work on error handling. libevdi does most things asynchronously, and asynchronous error handling in c isn't very fun. Libevdi mostly provides functions that return immediately and then log to stdout of anything goes wrong. They provide a way to intercept their log calls, so I forward them all as info-level messages to the Rust log ecosystem (libevdi doesn't use machine-readable levels).
I decided I'd be much happier and get much more work done if I reframed my goals for evdi-rs: using it is like writing c in Rust, but with nice types. I still got caught up in the fun of making nice abstractions in Rust, so it's actually pretty safe to use, but you will find comments in the source code that say things like
// Safety: Works on my machine.
//
// There's no way to use this without sharing between
// threads, so all the c users must be doing it.
//
// It seems from a brief scan of the source that this might
// not always be sound. We only hold on to this for a short
// time though and you probably won't be writing to the handle
// simultaniously.
I'm absolutely not saying people shouldn't make things sound wherever possible. I'm arguing people writing code that builds on top of fundamentally unsound but very useful c libraries might benefit from accepting at the start that it's not worth trying to ensure you never segfault.
Edit: The opening section of my crate's docs is up front that usage of the library can cause serious bugs. It's not that I document invarients that you need to maintain, it's that I don't know what invarients need to be maintained since the library I'm wrapping doesn't document them. I also think users probably care more about the "borks your devices display and you have to force reboot" logic bugs more than the potential memory unsafety.
There are a lot of useful c libraries out there that good bindings don't exist for. For example, multiple people have tried and failed to write good bindings to Ffmpeg. I think it's reasonable to reduce our goal for that sort of library as "as good as the other languages", not "takes total advantage of Rust to avoid unsoundness. I'd rather someone write unsound Rust than give up and pick a totally unsound language. Unsound is a spectrum.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment