How does rust-nix' passing raw u8 pointers to ioctl work?

I'm confused about a) how the Linux ioctl command seems to be overloaded with either pointers or longs, and how b) rust gets away with just passing raw *mut u8 pointers on, no matter what.

I am trying to understand how rust-i2cdev makes ioctl calls like this one here: https://github.com/rust-embedded/rust-i2cdev/blob/master/src/ffi.rs#L182

As you can see, the last argument is a raw *mut u8, which is the data argument in the macro definition here: https://github.com/nix-rust/nix/blob/60370990485092b4d7cbe625b964001e912dea9a/src/sys/ioctl/platform/linux.rs#L93

I don't really understand how and why this works. On the one hand, ioctl(2) says that:

The third argument is an untyped pointer to memory. It's traditionally char *argp (from the days before void * was valid C), and will be so named for this discussion.

On the other hand, this and some other sources talk about one being able to pass either an unsigned long or a pointer, and this being some kind of hack.

Stranger still, the Linux i2c-dev docs mention that the particular ioctl call above takes a long: https://www.kernel.org/doc/Documentation/i2c/dev-interface

So what is it? Does it take a pointer or a long? And if it takes on or the other, how does rust get away with passing it a raw pointer, no matter what, even if in this case it should get a long?

1 Like

I have not used the ioctl call in Rust, but I can try to explain my understanding of the backstory.

The "hack" in question is because C is weakly typed. Think of the long second argument as "bag of bytes" type, which has no defined properties except a particular length. Most likely, long was selected because (at the time ioctl became a standard) it was (almost) always bigger than the length of a pointer.

Different drivers will read the value differently (again, with C weak type casting) in order to respond to the user's request. Some used it as a pointer to user memory to read or write data, some used it as an enum value, some used it as a struct of bitfield elements that fit into a long! Many "hacks" went on -- and they were all permitted.

So having Rust make their "bag of bytes" be a *mut u8 makes sense to me, at least from a "getting the point across" point of view. Why not an actual libc::c_long? I'm not sure, but it may have to do with other stuff the C library does with ioctl calls -- which is another set of hacks entirely that vary between UNIXes.

EDIT: I suppose it is also possible that if the Rust i2c library knows that the device driver will accept a pointer, then it is offering a pointer API and will do the "encode it into a long trick" internally. But I have not read enough about the driver itself to know that for sure.

3 Likes

You mentioning that C is weakly typed helped a lot. I was thinking about C in terms of Rust and was incredibly confused about what must be going on with the types being passed in.

I read about the size guarantees of long and pointers, and understood that ioctl was delegating to actual driver implementations depending on its second argument (and that the underlying driver ioctl was then casting the pointer to whatever it needed it to be), but I was stumped about the pointer vs long issue.

Thanks!