[swift-evolution] [Draft] Target-specific CChar

William Dillon william at housedillon.com
Wed Mar 2 11:56:10 CST 2016

Please see the gist for the most up-to-date drafts.

I appreciate any comments, concerns and questions!

Improve the portability of Swift with differently signed char.

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: C 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:

Swift PR–1103
Swift Foundation PR–265
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.

Proposed solution

I propose that the CChar be aliased to UInt8 on targets where char is unsigned, and Int8 on platforms where char is signed.

Detailed design

In principle this is a very small change to swift/stdlib/public/core/CTypes.swift:

 /// This will be the same as either `CSignedChar` (in the common
 /// case) or `CUnsignedChar`, depending on the platform.
+#if os(OSX) || os(iOS) || os(windows) || arch(i383) || arch(x86_64)
 public typealias CChar = Int8
+public typealias CChar = UInt8
Impact on existing code

Though the change itself is trivial, the impact on other parts of the project including stdlib and foundation cannot be be ignored. To get a handle on the scope of the required changes, I’ve performed this change on the swift project, and I encourage any interested party to invesigate. https://github.com/apple/swift/compare/master…hpux735:char This project fork builds on both signed and unsigned char platforms. There is one test failure on signed char platforms and two test failures on unsigned char platforms resulting from remaining assumptions about the signness of char. They should be trivial to address by someone skilled at lit tests, and will be fixed prior to any pull request.

In general, code that you write will fail to compile if you assume that C APIs will continue to import char as Int8. Your choice is to write code that interfaces with char using CChar or to break it out into seperate cases. Other than one test, which relies on breaking the char assumption for the purposes of generating an error, I have not seen a case that justifies using conditional compilation directives over CChar. There are cases where it is necessary to cast to a concretely-signed type, such as UInt8 or Int8 from CChar, but in those cases it encourages you to consider the impact of assuming the structure of the data that you’re working with. Very often, if you write your code using CChar it will be portable, and compile cleanly on all platforms.

Alternatives considered

The only real alternative is the status quo. Currently, Swift treats all unspecified chars as 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 -unknown-{windows,darwin} ``` and look for “signext” OR “zeroext" in @_char definition

[8]: Windows char is signed by convention.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160302/3e95d39b/attachment.html>

More information about the swift-evolution mailing list