Errno.current

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.

can Errno.current be made public API?

6 Likes

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?

2 Likes

Just in case we want existing bugs: The "can't read errno" safely/correctly in Swift is [SR-5732] no built-in guaranteed way to read errno safely · Issue #48302 · apple/swift · GitHub / rdar://problem/28473807 .

1 Like

Is it not better to fix this at C level, in other words — as early as possible?

We could teach the importer a new trick [†], and similar to how it translates:

- (BOOL)foo:(NSError **)error;

into:

func foo() throws

it could treat specially annotated calls like:

int mkfifo(const char *path, mode_t mode) SOME_ANNOTATION;

as:

int mkfifo(const char *path, mode_t mode) {
    int err = original_mkfifo(path, mode);
    return err != 0 ? errno : 0;
}

for further Swift consumption.

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.

1 Like

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;
}

I proposed some of these things way back when, so there might be some useful content there: [Pitch] Make `errno`-setting functions more usable from Swift

5 Likes

Great writeup, thanks for the link.

As another alternative we could return a struct [†]:

typedef struct {
    int value;
    int error;
} IntWithError;

IntWithError better_statvfs(const char *restrict path, struct statvfs *restrict buf);

[†] However, 8 years passed since then... Looks like there is no enough demand to do anything about it.

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));
IntErrno intErrno(int (^execute)(void)) {
    IntErrno v = {};
    v.result = execute();
    v.error = errno;
    return v;
}

and call it from Swift:

let r = intErrno { statvfs(.....) }
print(r.result)
print(r.error)

Or could errno still be messed up by ARC here?

If that's Objective-C with ARC enabled, then yes.

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
}
3 Likes

Are you sure?

This test passes alright...
// ---------
// test.swift
var r = arc_intErrno { testCall(-2, 123) }
precondition(r.result == -2 && r.error == 123)
r = arc_intErrno { testCall(0, 0) }
precondition(r.result == 0 && r.error == 0)
r = mrc_intErrno { testCall(-2, 123) }
precondition(r.result == -2 && r.error == 123)
r = mrc_intErrno { testCall(0, 0) }
precondition(r.result == 0 && r.error == 0)

// ---------
// Header.h
typedef struct {
    int result;
    int error;
} IntErrno;
// we'd also need "PointerErrno" and possibly a few others here

IntErrno arc_intErrno(int (^execute)(void));
IntErrno mrc_intErrno(int (^execute)(void));

// ---------
// Arc.m
#include "Header.h"
#import <errno.h>

IntErrno arc_intErrno(int (^execute)(void)) {
    IntErrno result = {};
    result.result = execute();
    result.error = errno;
    return result;
}

// ---------
// Mrc.m, compile with -fno-objc-arc
#include "Header.h"
#import <errno.h>

IntErrno mrc_intErrno(int (^execute)(void)) {
    IntErrno result = {};
    result.result = execute();
    result.error = errno;
    return result;
}

// ---------
// TestCall.h
int testCall(int result, int error);

// ---------
// TestCall.c
#include "TestCall.h"
#include <errno.h>

int testCall(int result, int error) {
    errno = error;
    return result;
}

// ---------
// Bridging-Header.h
#import "Header.h"
#import "testCall.h"

If it's just by luck it works with ARC, the question becomes — is it guarantee to not mess errno with ARC disabled? (see mrc_intErrno in the code).

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.

3 Likes

That didn't compile. Neither does this:

@_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.

Is ARC messing with errno a real or imaginary thing? Neither allocations nor deallocations change errno on their "happy" path:

class C {
    deinit { print("deinit") }
}
errno = 12345
print(errno)        // 12345
var c: C? = C()
print(errno)        // 12345
print(c)            // Optional<C>
c = nil             // "deinit"
print(errno)        // 12345
let v = malloc(123)
print(v)            // Optional<...>
print(errno)        // 12345
free(v)
print(errno)        // 12345
free(nil)
print(errno)        // 12345
let r = malloc(-1)
print(r)            // nil
print(errno)        // 12

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:

  1. memory allocation will always succeed
  2. 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.

1 Like

I was referring to the previously raised concerns that this somehow could happen:

darwinCall() // set's errno
<invisible ARC traffic here that resets errno>
let err = errno // oops, cleared

Presumably ARC traffic exercises happy path here (and thus the above could not happen), no?

1 Like

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.

6 Likes

Very good point.

Is the following a safe sequence IRT proper errno handling?

    @inline(never)
    func readBytes(buffer: UnsafeMutableRawBufferPointer) throws -> Int {
        let size = Darwin.read(file, buffer.baseAddress, buffer.count)
        // *****************
        guard size >= 0 else {
            throw SomeError.errno(errno)
        }
        return size
    }

or is it possible there would some ARC code that could change errno in the marked line between Darwin call and errno reading?

(Just in case I've marked it as inline(never), not sure if that's necessary).