- Proposal: SE-004x
- Author: William Dillon
- Status: Draft
- Review manager: TBD
In C, the signness of char is undefined. A convention is set by either the platform, such as Windows, or by the architecture ABI specification, as is typical on System-V derived systems. A subset of known platforms germane to this discussion and their char signness is provided below.
| char | ARM | mips | PPC | PPC64 | i386 | x86_64 |
|---|---|---|---|---|---|---|
| Linux/ELF | unsigned 1 | unsigned 2 | unsigned 3 | unsigned 4 | signed 5 | signed 6 |
| Mach-O | signed [7] | N/A | signed [7] | signed [7] | signed [7] | signed [7] |
| Windows | signed [8] | signed [8] | signed [8] | signed [8] | signed [8] | signed [8] |
This is not a great problem in C, and indeed many aren't even aware of the issue. Part of the reason for this is that C will silently cast many types into other similar types as necessary. Notably, even with -Wall clang produces no warnings while casting beteen any pair of char, unsigned char, signed char and int. Swift, in contrast, does not cast types without explicit direction from the programmer. As implemented, char is interpreted by swift as Int8, regardless of whether the underlying platform uses signed or unsigned char. As every Apple platform (seemingly) uses signed char as a convention, it was an appropriate choice. However, now that Swift is being ported to more and more platforms, it is important that we decide how to handle the alternate case.
The problem at hand may be most simply demonstrated by a small example. Consider a C API where a set of functions return values as char:
char charNegFunction(void) { return -1; }
char charBigPosFunction(void) { return 255; }
char charPosFunction(void) { return 1; }Then, if the API is used in C thusly:
char negValue = charNegFunction();
char posValue = charPosFunction();
char bigValue = charBigPosFunction();
printf("From clang: Negative value: %d, positive value: %d, big positive value: %d\n", negValue, posValue, bigValue);You get exactly what you would expect on signed char platforms:
From clang: Negative value: -1, positive value: 1, big positive value: -1
and on unsigned char platforms:
From clang: Negative value: 255, positive value: 1, big positive value: 255
In its current state, swift behaves similarly to C on signed char platforms.
From Swift: Negative value: -1, positive value: 1, big positive value: -1
This code is available here, if you would like to play with it yourself.
The third stated focus area for Swift 3.0 is portability, to quote the evolution document:
- Portability: Make Swift available on other platforms and ensure that one can write portable Swift code that works properly on all of those platforms.
As it stands, Swift's indifference to the signness of char while importing from C can be ignored in many cases. The consequences of inaction, however, leave the door open for extremely subtle and dificult to diagnose bugs any time a C API relies on the use of values greater than 128 on platforms with unsigned char; in this case the current import model certainly violates the Principle of Least Astonishment.
This is not an abstract problem that I want to have solved "just because." This issue has been a recurrent theme, and has come up several times during code review. I’ve included a sampling of these to provide some context to the discussion:
In these discussions we obviously struggle to adequately solve the issues at hand without introducing the changes proposed here. Indeed, this proposal was suggested in Swift Foundation PR-265 by Joe Groff.
These changes should happen during a major release. Considering them for Swift 3 will enable us to move forward efficiently while constraining any source incompatibilities to transitions where users expect them. Code that works properly on each of these platforms is already likely to work properly. Further, the implementation of this proposal will identify cases where a problem exists and the symptoms have not yet been identified.
Thanks to Ben Rimmington for identifying related bugs
Notable comment:
- Chris Lattner: "I don't have a strong opinion on it, I was sort of hoping that all the C* types would go away someday. I agree that in this case, CChar does serve a useful purpose though."
A new type for CChar will be defined. All char types from C will be mapped into this type. The CChar type will be mostly opaque; only a small set of operations will be allowed to act upon it: user will very few choices other than to make an educated decision about what arethmetic type to cast it into.
In general, it will not be possible to perform arithmatic operations on the new CChar. It will be easy, however to cast CChar into UInt8 and Int8. This cast will be implemented by adding init methods to Int8, UInt8, and CChar:
extension Int8 {
init(_ rawByte: RawByte) {
self = unsafeBitCast(rawByte, Int8.self)
}
}
extension UInt8 {
init(_ rawByte: RawByte) {
self = unsafeBitCast(rawByte, UInt8.self)
}
}
extension CChar {
init(_ intVal: Int8) {
_inaccessible = unsafeBitCast(intVal, UInt8.self)
}
init(_ uintVal: UInt8) {
_inaccessible = uintVal
}
}I do think that there are a few arethmetic and comparison operators that may be considered for use with CChar:
- Equivalence
== - Test/set individual bits i.e.:
CChar.setBit(bit: int, to: Bool)CChar.testBit(bit: Int) -> Bool
- Bit-shift operators
<<>>(with either or both zero fill or one fill)
The reasoning for the inclusion of these few operators is because char is often used as a bitfield. In this case, it does not make sense to think of it as being truely signed or unsigned. A small number of operators would allow the continued use of this type in this particular case without inappropriately forcing it to take on a numeric meaning. I'm not wedded to these operators, and the concept in general, but I'd like it to be a part of the discussion.
Though the change itself is relatively minor, the impact on other parts of the project including stdlib and foundation cannot be be ignored. Every example of with C char in the standard library that I encountered can immediately cast to UInt8 without consequence as the vast majority of tests pass on Int8 and UInt8 platforms. As users encounter issues with other libraries and APIs that are less portable across systems with varying char signness, other choices will have to be made. This proposal will, however, provide the tools to perform this work.
-
Another solution (and the one I originally proposed) is that
CCharbe aliased toUInt8on targets wherecharisunsigned, andInt8on platforms wherecharissigned. -
Status quo. Currently, Swift treats all unspecified
charsas signed. This mostly works most of the time, but I think we can do better.
[7]: proof by construction (is it signed by convention?)
$ cat test.c
char _char(char a) { return a; }
signed char _schar(signed char a) { return a; }
unsigned char _uchar(unsigned char a) { return a; }
$ clang -S -emit-llvm -target <arch>-unknown-{windows,darwin}
and look for “signext” OR “zeroext" in @_char definition
[8]: Windows char is signed by convention.
Awesome, thanks for working on this! Overall it looks great! I also had a few notes:
#if os(OSX) || os(iOS) || os(windows) || arch(i383) || arch(x86_64)seems a little inelegant.UnsafeMutablePointer<UInt8>in their function signatures after being transformed to Swift. Does this proposal affect this at all?