there are many missing APIs in the swift-system package, so i often find myself filling in gaps (e.g, mkfifo, statvfs, etc.) with private extensions. one issue i’ve run into is that swift-system does not expose access to Errno.current, which makes it challenging to handle errors from bare system calls.
As the TODO above that decl mentions, we need a way to make a release (i.e. a deinit) barrier around access to Errno.current, as deinits could overwrite it.
@Joe_Groff , I remember talking about this concept a while back, do we have any means to do so?
This won't work in general case (e.g. for it to work with read we'd need to return a tuple:
func read(...) -> (result: size_t, errno: Int)
but for the mentioned mkfifo & statvfs (which I believe is a frequent case) it could work nicely and avoid us the trouble of solving the mess errno introduces.
[†] - How many calls like these in Darwin do we have to deal with? If it's a thousand or less we could probably do something like this by hand.
To cure the disease not the symptom: could we fix a Posix subset used via Swift, or is it out of question because Posix is sacred and this is too bold ambition?
// a bike-shed name here. use this from Swift
int real_statvfs(const char *restrict path, struct statvfs *restrict buf) {
// actual code that returns an error or 0
return ....;
}
// don't use this from Swift (or prohibit).
int statvfs(const char *restrict path, struct statvfs *restrict buf) {
int err = real_statvfs(path, buf);
errno = err;
return err == 0 ? 0 : -1;
}
Quinn suggested an excellent idea elsewhere which I think is worth exploring. I'm refactoring it a bit below leaving the gist of the idea intact.
How about having this on the C side:
typedef struct {
int result;
int error;
} IntErrno;
// we'd also need "PointerErrno" and possibly a few others
IntErrno intErrno(int (^execute)(void));
If it looks like an opaque function call, then it should be treated as a deinit barrier by default. But that's not sufficient since any malloc call could also clobber errno before you read it.
We have the @_noLocks experimental feature now, which raises an error if any code in an annotated function would take a lock (or by extension, try to allocate heap memory). So you could do something like
struct PosixError { var errno: Int32 }
@_noLocks
func mkFifo(path: UnsafePointer<Int8>, mode: mode_t) throws(PosixError) -> Int32 {
// mkfifo probably locks, but that's fine for our purposes
let fd = unsafePerformance { mkfifo(path, mode) }
if fd == 0 {
throw PosixError(errno: errno)
}
return fd
}
I don't know the guaranteed semantics of ARC with Obj-C but I suspect that the block execute could be deallocated between result.result = execute(); and result.error = errno if arc_intErrno gets inlined by the caller. Is that likely/possible in the current implementation of ARC for Obj-C: I don't know.
Regarding manual retain counting: Yes, that can be made safe because you, the programmer controls where -release is called.
Aside, we don't have Obj-C on anything but Darwin and Swift should be able to use errno and similar thread locals correctly.
@_noLocks
func foo() -> Int32 {
errno // 🛑 Called function is not available in this module and can have unpredictable performance
}
Forgive my ignorance, we have C there, right? In my own projects I'd identify all cases where I need to reach out for errno, and for those API's create C wrappers that use other means to return it, e.g.:
// old code:
let result = mkinfo(path, mode)
let error = errno
// new code:
let r = mkinfo_wrapped(path, mode) // pure C wrapper
let (result, error) = (r.result, r.error)
Crucially you don't have to do this for all API's that use errno, only for those cases where you care about errno, so it could well be a couple or a couple of dozens, not thousands of API's. Not ideal, sure.
often, we are interested in errno when things do not take the happy path. in fact, i’d say that’s when reading the correct value from errno is most important. i’ve found a lot of code written for client-side use cases makes two assumptions:
memory allocation will always succeed
there will always be more disk space on the host
when you try to run such applications on the server, everything goes off the rails because the code assumes resource allocation can never fail.
swift_dealloc is unlikely to mess with errno, but that’s not a promise. The bigger problem is that deinits run arbitrary code in the most general case.