nix
nix copied to clipboard
Figure out the future of nix
tl;dr Should nix be split up into multiple crates named nix-*
and what should the API style be?
The situation
Nix originated as a posix binding dumping ground for various crates that I had been working on. I tended to need the same bindings over and over again, so I just moved them into Nix. I thought this would be useful for other people, so I accepted PRs for various system APIs as they have been submitted.
Everything has been fine so far, but a few issues have arisen.
As the API coverage has increased, whenever a breaking change is made, the semver must be incremented. So, if the ioctl
module changes an API as it figures out the best way to expose functionality, the version must be bumped and downstream crates must change their dependencies. For example, Mio depends on nix, but not on any ioctl
features, so bumping the dependency is pretty annoying.
Secondly, the API style has been somewhat ad hoc, especially with more complicated system APIs like fcntl
.
Crate structure
There is value in having a single place (nix) to find system APIs that have a coherent API style and a single documentation site. I think that each specific API silo, like sockets, ioctl, mqueue, signals, etc... could be in their own crate named nix_*
. Common features like errors, NixPath
, etc... would live in nix_core
. The nix
crate would be a facade depending on all the sub crates with liberal dependencies (probably "*"
) which would allow the consumer to optionally be able to pin each sub crate to whatever.
Libraries like Mio could then depend only on the features that are needed.
Also, it would allow delegating responsibility better. I, for one, know nothing about ioctl
. I would like to stop being the blocker.
API style
This is still something that I haven't figured out. I do know that the goal of nix is *comprehensive, safe, zero cost bindings to system APIs on nix OSes.
So, what does that mean for nix? As of now, there are currently a variety of styles. For example, most APIs are 1-1 bindings to the equivalent OS api. This leads to APIs like fcntl
which uses a convoluted FcntlArg
strategy. ioctl
on the other hand uses a (fairly impressive) macro...
The question is, would it be better to expand beyond the 1-1 rule. For example, in the fnctl
case, the API would most likely be much cleaner and more "rusty" if each "fcntl arg" was moved to an individual function. There could, for example, be a public fcntl
mod with a dupfd
function. Using it would be:
let res = fcntl::dup_fd(fd);
Then, should it go further? Should there be an Fd
type that wraps std::os::unix::RawFd
and implement functions like dup
directly on that type?
These are all open questions that I pose to the users of nix
.
cc @posborne @cmr @geofft @utkarshkukreti @MarkusJais and whoever else :)
cc @alexcrichton (possibly relevant for liblibc plans?)
I’m not sure what the best approach is here but here are few thoughts I had:
Also, it would allow delegating responsibility better. I, for one, know nothing about ioctl. I would like to stop being the blocker.
This is possible within a single crate if you wanted to give others ability to merge pull requests. Multiple crates would give finer control e.g. someone can have permission to merge ioctl changes but not fcntl changes.
I do know that the goal of nix is comprehensive, safe, zero cost bindings to system APIs on *nix OSes.
Awesome.
As of now, there are currently a variety of styles
This is something to discuss in terms of developing nix but I think single crate or multiple crates this still imposes multiple styles on users of the crate.
@joekain My proposal would be to keep all the code in the same git repository, but to release individual crates to crates.io
. This would only be to allow the versions to progress independently. There should be a consistent style across all the individual crates.
@alexcrichton also mentioned that he had a goal of generating all the constants / unsafe FFI definitions needed for nix in the the libc
crate, which would allow nix to purge all the unsafe / const bindings in favor of focusing entirely on the Rust API.
Ah and to clarify, I'm not sure we'll be generating the libc
crate, but we're going to start verifying it much more rigorously, so it should be much easier to add to but will still require additions.
Thanks for bringing this up @carllerche.
I am in favor of your proposal to retain a single git repository for the "official" nix libraries in order to provide consistency but breaking out subsystems into discrete crates that are largely independent (with the exception of nix_core).
The level of abstraction question is one that I have been thinking about more as changes have been made with the ioctl
system. With that change, I feel that it may have been a better (in hindsight) to expose the functionality in layers:
- Layer 1: The raw
ioctl
call exposed by libc/liblibc - Layer 2: Something similar to (the old
ioctl
API)[https://github.com/carllerche/nix-rust/blob/c4a9598ec16a01e11e66bea1eb34cea9b4646326/src/sys/ioctl.rs] I wrote (Unsafe) - Layer 3: Higher level, macros (API present today)https://github.com/carllerche/nix-rust/blob/master/src/sys/ioctl/mod.rs
- Layer 4: Safe APIs for specific drivers based on top of layer 2/3. Examples of that would be crates like https://github.com/posborne/rust-spidev and https://github.com/posborne/rust-i2cdev
I think all layers up to 3 could have a good spot within nix, especially with discrete crates but that designing in terms of layers and exposing each layer to users probably makes sense in most cases as well. The higher level the abstraction, the more likely it is that a use case might be prevented.
I believe Nix should aim to expose the highest level (most safe) API that presents an unlimited (a user can do everything needed), zero-cost abstraction as a start. To benefit users, nix crates may also expose APIs that sacrifice flexibility in favor of safety for select use cases.
My vote (as a user who is only starting to use nix) would be to keep nix as a monolithic crate, and just let users bump their version dependencies. They shouldn't be forced to bump the version that frequently, right? i.e. it should be safe for them to continue using an older version for some time.
I also think that developing a rusty (and complete) API would be ideal. e.g. the ptrace signature is currently unsafe (although not marked so?!), and could productively be replaced with multiple helper functions that are actually safe, since the types of the later arguments depend on the flag.
FYI I filed https://github.com/carllerche/nix-rust/issues/214 about exporting all functions / types at the crate root as is now done in the libc crate. It's probably relevant to this discussion.
@droundy the main driver around splitting up the crate is that a breaking change in a less thought out part of the API isn't a breaking change in other parts...
@kamalmarhubi Yep, thanks for filing it.
In general, this issue is on hold until libc stabilizes a bit and pulls in the required symbols.
I think the most important thing is to have a consistent style for APIs.
Putting the low-level bindings in libc is a good idea and then nix could just wrap them so no unsafe stuff is necessary for the user of nix. And we could add convenience functions on top of the low-level bindings (e.g. the mq_set_nonblock/mq_remove_nonblock I've added to mqueue.rs) that makes using the APIs easier (but avoiding any serious overhead!)
I am happy to remove all the low-level mqueue stuff once it is in libc.
I like the idea of moving as much of the current ffi
module stuff to libc as possible. The main things attracting me to nix are:
- no unsafe necessary in user code
- Rustified error handling
I don't have enough experience in any of the areas where nix has fancier bindings, so I can't comment on that at all. I can say I do like things mapping directly to man pages for what I'm trying to do at the moment.
I mostly agree with @posborne's four layers, although I do not quite see the need for the distinction between two and three (or rather the existance of level two). That leaves me with three levels:
- libc: essentially 1:1 ffi, raw and unsafe.
- nix: close to zero cost abstractions that provide an idomatic API using
Result
,Optional
, structs with methods, bitflags, etc., 100% functionality available to the user, no unsafe API - See Level 4 of @posborne.
In commit 142045f in my dev branch, I changed the signals module toward that goal.
- I replaced almost all constants with libc constants.
- I replaced all ffi function declarations with their libc equivalents.
- I replaced the low level structs with their corresponding libc structs. This is the first breaking change, because I could not use the word "equivalents" in the previous sentence.
- I removed the export of the raw low level structs. This is also a breaking change. I would rather support the idea of exporting the full libc under
nix::libc
. Currently, there is no way to retrieve the raw structs from their wrapping counterparts. I would not mind to include this usingAsRef
implementations.
In addition to these changes I would also liked to have removed the type alias SigNum
, but then I would have had to touch the wait module as well. I do not see much value in types aliases for primitive types from libc.
The commit would not bring signals all the way to where it should be according to 2. above. We do not have 100% functionality, e.g. the results do not distinguish error types. There is almost no documentation.
I don't have a proper understanding of how multiple versions of a crate play together. According to #226, tying nix's types to libc could result in downstream users having weird effects due to multiple libc versions being around. @fiveop do you have any insight, since this seems to go against what you're suggesting / working towards with signals.
In general, this issue is on hold until libc stabilizes a bit and pulls in the required symbols.
We can start this discussion up again, as I think libc is pretty stable now. There have been 8 releases in the 0.2 series: https://crates.io/crates/libc/versions
Goals of nix
@MarkusJais said:
Putting the low-level bindings in libc is a good idea and then nix could just wrap them so no unsafe stuff is necessary for the user of nix.
I like this as a lowest level aim for nix. Use system APIs without unsafe
. I filed #264 a while ago to track removing our own definitions, migrating to libc as much as possible.
@fiveop said:
- libc: essentially 1:1 ffi, raw and unsafe.
- nix: close to zero cost abstractions that provide an idomatic API using Result, Optional, structs with methods, bitflags, etc., 100% functionality available to the user, no unsafe API
- See Level 4 of @posborne: Safe APIs for specific drivers based on top of layer 2/3. Examples of that would be crates like https://github.com/posborne/rust-spidev and https://github.com/posborne/rust-i2cdev
(I copied in @posborne's level 4.)
I agree with @posborne that "all layers up to 3 could have a good spot within nix", ie that levels 1-2 in @fiveop's summary work well in Nix.
Crate / repo structure
I'm in favour of single repo / multiple crates. I think this pattern has been used pretty well in a few crates in the ecosystem.
Single repo keeps the development cohesive and in one place. Multiple crates gets rid of unnecessary version coupling. There are APIs where the best way to handle them in nix is pretty clear. We've got the unistd
basics down pretty well. But the ioctl
, fcntl
, and sockopt
areas seem a bit more in flux, and are more likely to have breaking changes. It would be annoying to me as a downstream user to have to bring in a version with potential incompatibilities just because I want a fix to one of the more solid areas.
A comment regarding usage of libc
types where possible (continuing the discussion) -- I think that we should go this way as much as possible, and don't hesitate to wrap them with our own for the end user if it seems a good idea. For example, fd_set
from libc
is pretty inconvenient (unsafe FD_*
functions), but we can wrap it in our own FdSet
and make it much more idiomatic.
For the record:
I wrote
- nix: [i...] no unsafe API
I am now certain that there are some system calls that nix cannot provide a safe interface for. For example, we as a library can never ensure that a handler installed using the function sigaction
does not make unsafe calls (on the operating system level). No library, that does not also provide the installable handler functions, can provide a safe interface.
@fiveop
handler installed using the function sigaction does not make unsafe calls (on the operating system level)
Could you elaborate a little, or give an example? I haven't used the signal stuff, but this sounds important to understand.
@abbradar
I think that we should go this way as much as possible, and don't hesitate to wrap them with our own for the end user if it seems a good idea
Yeah I think this makes sense. It fits in with @fiveop's 2. above, which includes "structs with methods" under providing an idiomatic API.
One more thing I'd love to see is reorganizing modules/subcrates more similarly to std
. I get the appeal of mirroring libc but after a few years of not working with it I have trouble to remember in which module particular function/type is defined.
Having both ways, one being facade of the other is also fine.
Maybe also eventually getting some well-tested things to std
?