Hey, swift-evolution. I want to draw attention to one of the oddest parts of the Objective-C importer today: NSUInteger. TLDR: NSUInteger is treated differently based on whether it comes from a system framework or a user-provided header file. I think this is silly and that we should treat it consistently everywhere, but I haven’t had time to go collect data to demonstrate that this is a safe change. Would someone like to take that on?
If so, read on. (Or jump to the last section, and read these “Background” sections later.)
## Background: Importing Integer Types from C
As everyone is familiar, the importer maps certain “known” Objective-C types to the Swift types. This includes some mostly non-controversial mappings:
- Mapping fixed-sized integers: ‘int32_t' to ‘Int32'
- Mapping common C types to fixed-sized integers: ‘unsigned short’ to ‘UInt16’
- Mapping C’s ‘long’ to Swift's ‘Int’.*
- Mapping ‘intptr_t’ and ‘ptrdiff_t’ to ‘Int’ and ‘uintptr_t’ to ‘UInt'
- Mapping ‘NSInteger’ (and ‘CFIndex’) to ‘Int’
* ‘long’ is a pointer-sized integer on all common modern platforms except 64-bit Windows; we’ll have to do something different there. (‘CLong’ will always be the right type.)
And a few controversial ones:
- Both ‘size_t’ and ‘rsize_t’ are mapped to ‘Int’, not ‘UInt’. This is a pragmatic decision based on Swift’s disallowing of mixed-sign arithmetic and comparisons; if size_t and rsize_t really are used to represent sizes or counts in memory, they will almost certainly never be greater than Int.max. It’s definitely a tradeoff, though.
And finally we come to the strangest one, NSUInteger.
## Background: NSUInteger
In (Objective-)C, NSUInteger is defined to be a word-sized unsigned integer without any stated purpose, much like uintptr_t. It conventionally gets used
1. to represent a size or index in a collection
2. as the base type of an enum defined with NS_OPTIONS
3. to store hash-like values
4. to store semantically-nonnegative 32-bit values, casually (as a compiler writer I’d suggest uint32_t instead)
5. to store semantically-nonnegative 64-bit values, casually (definitely not portable, would suggest uint64_t)
6. to store opaque identifiers known to be 32 or 64 bits (like 3, but either wasting space or non-portable)
(1) is actually the problematic case. Foundation fairly consistently uses NSUInteger for its collections, but UIKit and AppKit use NSInteger, with -1 as a common sentinel. In addition, the standard constant NSNotFound is defined to have a value of Int.max, so that it’s consistent whether interpreted as a signed or unsigned value.
For (2), the code really just wants a conveniently-sized unsigned value to use as a bitfield. In this case the importer consistently treats NSUInteger as UInt. We’re not going to talk about this case any more.
(3) is a lot like (2), except we don’t actually care what the sign of the value is. We just want to vary as many bits as possible when constructing a hash value; we don’t usually try to sort them (or add them, or compare them).
(4) is interesting; it’s entirely possible to have 32-bit counters that go past Int32.max. It’s not common, but it’s possible.
(5) seems much less likely than (4). Int64.max is really high, and if you’re already up in that range I’m not sure another bit will do you any good.
(6) is basically the same as (3); we don’t plan on interpreting these bits, and so we don’t really care what sign the type has.
Because of this, and especially because of the Foundation/*Kit differences, in Swift 1 we decided to import NSUInteger as Int, but only in system frameworks (and when not used as the raw type of an enum). In user frameworks, NSUInteger is consistently imported as UInt.
## The Problem
This is inconsistent. User frameworks should not have different rules from system frameworks. I’d like to propose that NSUInteger be imported as Int everywhere (except when used as the raw type of an enum). It’s not a perfect decision, but it is a pragmatic one, given Swift being much stricter about mixing signedness than C. I’d hope the logic above convinces you that it won’t be a disaster, either—it hasn’t been for Apple’s frameworks.
The recommended idiom for “no, really a word-sized unsigned integer” would be ‘uintptr_t’, but unless you are actually trying to store a pointer as an integer it’s likely that uint32_t or uint64_t would be a better C type to use anyway.
For people who would suggest that Swift actually take unsigned integers seriously instead of using ‘Int’ everywhere, I sympathize, but I think that ship has sailed—not with us, but with all the existing UIKit code that uses NSInteger for counters. Consistently importing NSUInteger as UInt would be a massive source-break in Swift 4 that just wouldn’t be worth it. Given that, is it better to more closely model what’s in user headers, or to have consistency between user and system headers?
(All of this would only apply to Swift 4 mode. Swift 3 compatibility mode would continue to do the funny thing Swift has always done.)
## The Request
Consistently importing NSUInteger as Int would be a pretty major change to how we import existing Objective-C code, and has the potential to break all sorts of mixed-source projects, or even just projects with Objective-C dependencies (perhaps longstanding CocoaPods). Because of this, I’ve held off on proposing it for…a long time now. The last piece, I think, is to find out how Objective-C projects are using NSUInteger in their headers:
- Do they have no NSUIntegers at all?
- Are they using NSUInteger because they’re overriding something that used NSUInteger, or implementing a protocol method that used NSUInteger?
- Are they using NSUInteger as an opaque value, where comparisons and arithmetic are uninteresting?
- Are they using NSUInteger as an index or count of something held in memory?
- Are they using NSUInteger as the raw value of an NS_OPTIONS enum?
- Or is it something else? (These are the most interesting cases, which we probably want to write down.)
If the answers all land in one of these buckets, or even 90% in one of these buckets, then I think we’d be safe in proposing this change; if it turns out there are many interesting uses I didn’t account for, then of course we won’t. But I do think we need to do this reaserch.
Is someone willing to go look at modern CocoaPods and sample code, and ask other developers from major Swift-using companies, to find out how they’re using NSUInteger? And then write up their methodology and report back to us at swift-evolution. If you do this, I will be quite grateful.
P.S. For Apple folks, this is rdar://problem/20347922 <rdar://problem/20347922>.