Search code examples
ruststructuredirectory-structurerust-crates

Why should/shoudn't I use lib.rs in binary crates?


When I search the internet for design examples how to structure my code, I frequently find setups with main.rs only containing the main function and usages of lib, while lib.rs contains all the actual code and mods.

There are also cases where there is just main.rs without lib.rs, but I found enough main+lib examples that I never really thought about this point and just always created a lib.rs for binary crates. Now I stumbled upon the issue that I was refactoring my code, but many of my functions and structs are pub* and I don't get unused-warnings for them because of that.

I see the point of using main+lib when I want to build a library and add a binary to try if it is working. I also see the point of it when I have multiple binaries that share same lib code. However, most of these examples contained just one lib and one main and were meant to be executables. So, both points don't apply.

Is it a best practice to usually create lib.rs in binary crates? What are the pros and cons?


*many functions are pub: I am aware that in this setup I should only expose things that are necessary for the main (or for public API), but my main was too large. Hence the refactoring. Also, I could search and replace all pub with pub(crate), then make everything pub again what the new main really needs and now I would get unused warnings for everything else again. But this is an active step I need to remember to do every now and then, while the compiler would remind me automatically.


Solution

  • The most important distinction between having a lib.rs + main.rs, rather than just main.rs, is that in the former case, you are producing two targets, a library, and an executable, whereas in the latter you just have an executable. So, basically, the question is why would you move part of your code into a library rather than have it all in an executable.

    Here is a list of reasons I could think of. Note that I can't think of a single reason not to do so, except if you are writing a super small application, and none of these reasons outweigh the cost of having one additional file.

    • Libraries compose well, executables don't. Imagine you want to programmatically perform some operations on a git repository. You could use the git command, but that would be very painful.

      • You'd have to write very verbose code to execute a system command, which would be even more verbose if you tried to do so in a cross-platform way, even if the git operation itself is entirely platform agnostic.
      • It becomes hard to recover structured errors, because you need to parse the output of the command. You have to manually patch together Rust's error handling idioms with how system commands work. You have a lot more sources of possible errors, which can either be ignored (which is not idiomatic), or pollute your error types in the rest of your application.
      • You don't have language support, might it be with its type system; with Rust's compiler hints, which know nothing of external commands; your editor cannot complete external commands.

      On the other hand, libraries compose well. You just add it as a dependency, and get none of the problems I mentioned.

    • Executables and libraries have different idioms. For instance, it is rather common to handle errors with dynamic dispatch in executables, but with ADT in libraries (see for instance a short comparison between anyhow and thiserror). Splitting your code between an executable and a library means that parts that should be written with library idioms will be idiomatically written so.

    • Separating executable code and library code helps you structure your program. When you design large code bases, having different architectures can have huge impacts on how easy it is to maintain it. Furthermore, it's not always easy to see how to split a complex problem than an application is supposed to solve into relatively lowly coupled parts, where to put abstraction boundaries, etc., especially if you don't have much experience with large code bases. Thinking of your code as something that should either be on the library side, or on the executable side, is IMO always a good heuristic to start organizing your code.

    • Libraries are easier to test than executables. This is very much related to the first point: if you want to test the features of your application in a black-box way (ie. not with unit tests, but with integration tests), but without resorting to mocking a user interfacing with your UI (which you might want to test separately), life is going to be much simpler if you have your code separated between library and executable.

    My personal feeling about this is that I don't have the same mindset when writing an executable, or a library, and I tend to accumulate less technical debt when writing a library, because it forces me to think about all possible use cases of the interface I offer, which is more general than that of the executable. Hence, I usually write better code if it's a library.