-
Notifications
You must be signed in to change notification settings - Fork 18.8k
Description
This proposal is coming out of the discussion on https://go-review.googlesource.com/c/sys/+/150321 .
Problem statement
I'm implementing a PPP client in Go, which involves talking to Linux's PPP and PPPoE kernel drivers. Both are driven through ioctls on fds. This has led to me having to make a couple of changes to x/sys/unix, to add more ioctl request codes as well as new Ioctl* getters and setters.
The current API for ioctls in x/sys/unix is:
- An unexported "raw" ioctl function that takes a uintptr value:
func ioctl(fd int, req uint, value uintptr) error - Exported type-safe implementations that require specific argument types, and cast to uintptr under the hood, e.g.
func IoctlSetInt(fd int, req uint, value int) errororfund IoctlSetTermios(fd int, req uint, value *Termios).
The overall aim of this API is to make ioctl invocation type safe, or at least as type safe as possible. The current incarnation only partially succeeds: the ioctl request code is still exposed to the user, meaning they are free to invoke an ioctl using the wrong "type helper", e.g. passing an int into an ioctl that expects a *Termios. It's still a benefit over the fully raw ioctl call, in that you have to explicitly type the wrong parameter name into your editor (and hopefully realize the discrepancy at that point), but it's not ideal.
Proposal
I propose defining an ioctl API surface where one function maps to a single ioctl request code, and thus the functions can be completely type-safe. With the public API, it would become impossible to invoke an ioctl with the wrong parameters, or invoke a write ioctl using a read idiom.
The obvious problem with this approach is that it's a lot of busywork to define all those boilerplate functions. The obvious answer is to autogenerate them.
At least on Linux, this is somewhat straightforward: the kernel headers uses a set of macros to facilitate the definition of ioctl request codes. These macros carry the information needed to generate a strongly typed wrapper for every ioctl in the kernel API surface.
As a randomly selected example, include/uapi/linux/sed-opal.h has the following definition:
#define IOC_OPAL_SET_PW _IOW('p', 224, struct opal_new_pw)
When evaluated through the C preprocessor, the macro evaluates to the ioctl request number. mkerrors.sh already demonstrates how to pull those definitions out into Go definitions.
When not evaluated, this macro invocation tells us a few more useful things: _IOW means the ioctl is a write ioctl (userspace-to-kernel), and that its parameter is a struct opal_new_pw. With that additional information, we can autogenerate:
func IoctlOpalSetPw(fd int, value *OpalNewPw) error {
return ioctl(fd, 0x12345678, uintptr(unsafe.Pointer(value)))
}
Pros/Cons
The pro for me is that I can do a little bit more work once, and not have to send another CL to x/sys/unix each time I discover a new strange ioctl.
It would make ioctl invocation completely type-safe for users of x/sys/unix on linux.
In opposition, I found #14873 , in which r objects to making ioctl nice. The existing API is already going against his wishes, so I'm assuming there was another unrecorded discussion which shifted things.
This will also bloat the x/sys/unix implementation quite a bit. If we autogenerate all read and write ioctls, that adds >500 functions. We could follow current standard practice for x/sys, and only add new ioctl definitions when people want to use a particular subsystem, but that's still a large chunk of the x/sys/unix API surface. Another option might be to define a subpackage for ioctl, which is entirely autogenerated. That way the main x/sys API surface isn't polluted by ioctl, and invocations look like ioctl.OpalSetPw(...), which is still nice. That may get a bit tricky because this hypothetical new package would have to define all the structs and whatnots that the ioctl calls want, or reference their definitions in the main unix package.
It makes the linux implementation of x/sys/unix diverge from that of other unix derivatives. Currently the ioctl API is relatively uniform across unix variants, but this change would pull linux away. Maybe this is fine since x/sys/unix is completely OS dependent anyway. Maybe there's similar tricks we can play with the BSDs and OpenSolaris family tree, but I don't know their API conventions well enough to know.