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!