Import C

Now that we have @c, is it time to start effectively necroposting about import C again?

Does it make sense to have a library with defined enough Swift @c annotated surface to have a reasonably opinionated set of Swift libc bindings in the stdlib? I don't know much about the Rust libc crate, but the pattern feels viable for Swift. Note opinionated here doesn't mean swift-system, but things like obviating nullability annotations or smoothing out other inconsistencies from type impedance mismatching with C.

Another crack idea could be -- much in the vein of llvm's libc which is implemented in C++ -- creating a separate project for libc implemented in Swift... but that's a whole separate barrel of monkeys. I only mention this because the surface for import C would be identical to a libc-implemented-in-Swift, just that the implementations need not be identical.

I hope I'm making sense? Maybe just some experimentation is worthwhile first, I don't know...

2 Likes

Just an aside, I like the idea of a “cross platform” import C, but I’m not sure it would actually involve the new @c attribute. Presumably such a library would be a mix of directly re-exporting some imported C declarations, and some wrappers written in Swift to paper over the differences and maybe add some stronger typing or something, but while those wrappers would use POSIX naming conventions and so on, there’s probably no reason for them to be @c.

Re-implementing a platform libc in Swift would certainly be an interesting exercise and possible in theory at least, but it sounds inherently non-portable, unless you scope it down to a subset of features like the string functions and printf() and scanf() and such. It’s kind of the opposite of an import C overlay in a sense.

For example, on some OSes the direct system call interface is not even stable from release to release. (EDIT: I guess it depends on if you define libc as just the functions in the C standard, or everything that a typical Linux libc / macOS libSystem actually exports.)

2 Likes

I wouldn't classify syscalls as libc, but I can certainly see the argument in that direction. What I'd expect from import C would just be the contents of the standard headers. I wouldn't necessarily expect to see the POSIX extensions, but I also wouldn't be surprised if they were there.

The argument against the library being just the platform-specific imports being that you still have to have platform-specific conditionals at the call sites to deal with: for example, whether the pthread mutex type is pthread_mutex_t or pthread_mutex_t?. Yes, you can potentially add API notes, but why stop there?

Ah, true; the ffi in the other direction is not necessary...

Even the standard is non-standard. There are POSIX extensions, e.g. posix_spawn on Linux takes nullptr as a shorthand for { nullptr } for argv and envp which is even deemed a misfeature as per the man pages. This would mean that the API surface needs to be different per OS even for the cross-platform library. You cannot simply APINotes away this API surface difference. This (IMO) basically is a stronger version of @3405691582's argument - there are places where you have to do more than just adjust the API surface, you end up having to change the API surface, at which point, is no longer is a simple helper to import the system C library interfaces.

1 Like

Another thing I'd love to get under control is the mixed use of _GNU_SOURCE throughout various libraries (including Foundation). Passing -Xcc -D_GNU_SOURCE to swiftc is wholly incompatible with explicit module builds because there can only be a single definition of _GNU_SOURCE throughout the whole graph at the point where libc's header module is compiled (unless you go so far as to prune the dependency everywhere you use it). _GNU_SOURCE makes breaking changes to the way some APIs get imported into Swift (__SOCKADDR_ARG becoming a union is the one that immediately comes to mind, which changes its optionality). I've seen situations where I can't do an explicit module build of two targets in the same dependency graph because they need different (un)definitions of _GNU_SOURCE.

So my experience has led me to being mostly against a way to "just make importing the libc headers directly" easier. At best, it's a promise we can't keep; at worst, it's a giant foot-gun.

1 Like

Couldn't we make _GNU_SOURCE a trait?

No. That's essentially what a preprocessor definition already is in a manner of speaking, since that's how they're passed from SwiftPM to the compiler.