[Pitch] Change CChar32 to UInt32

Hello! I'd like to propose a change to CChar32, Swift's C/C++ type alias for char32_t. char32_t can hold values that are not valid Unicode scalars, so importing it as Unicode.Scalar is unsound.

This is technically a breaking change for Swift code that relies on the current unsound mapping, but I believe there will be few actual breakages in practice.

Summary of changes

Change CChar32, the Swift type for C++ char32_t, from Unicode.Scalar to UInt32.

Motivation

Swift's Unicode.Scalar is always a valid Unicode scalar value (0...0xD7FF or 0xE000...0x10FFFF). C and C++ make no such guarantee for char32_t, which can hold any 32-bit integer value:

// C++
char32_t make_char(uint32_t i) { return i; } // Valid C++

With C++ interoperability enabled, Swift imports char32_t as CChar32 (which resolves to Unicode.Scalar), which makes it possible for C++ to drop invalid Unicode values into Swift:

// Swift
let c: Unicode.Scalar = make_char(0xD800)  // Holding an invalid value

The current import behavior depends on the interoperability mode. In C and Objective-C, char32_t is just a typedef of a 32-bit integer type, so it already imports as UInt32. In C++, char32_t is a built-in type that Swift maps to CChar32 and resolves to Unicode.Scalar. Thus, the same header produces different Swift types depending on whether C++ interoperability is enabled. Fixing the Swift type alias would bring C++ interop's behavior into alignment with (Obj)C interop.

Note that CChar8 is UInt8 and CChar16 is UInt16; CChar32 is the only sized character type alias in this family that doesn't resolve to an integer type. CWideChar is also an alias to Unicode.Scalar but will be dealt with separately (see "Future work").

Proposed solution

-public typealias CChar32 = Unicode.Scalar
+public typealias CChar32 = UInt32

C++ declarations that use char32_t will still import as CChar32 and resolve to UInt32,
consistent with C interop.

I've already opened a PR the contains this change: [stdlib] Change CChar32 from Unicode.Scalar to UInt32 by j-hui · Pull Request #92870 · swiftlang/swift · GitHub

Source compatibility

This change breaks source for code that treats CChar32 or char32_t imported from C++ as a Unicode.Scalar. For projects that build without C++ interop enabled, only code that uses CChar32 directly is affected.

Code Result after this change
let c: CChar32 = "a" error: UInt32 has no scalar literals
CChar32(42)! error: result is not optional
make_char(42).value error: no member value
u32str.push_back(scalar) error: expects UInt32
print(make_char(97)) before: prints a; after: prints 97
CChar32("a") before: a; after: nil

ABI compatibility

UInt32 and Unicode.Scalar have the same size, alignment, and calling convention.

Mangled names will change. A Swift function with CChar32 in its signature gets a new symbol when recompiled. Rebuilding an ABI-stable library that exposes CChar32 in public API changes its symbols, which breaks clients compiled against the old version. Such libraries can keep their existing symbols by writing Unicode.Scalar instead of CChar32 in those declarations.

The standard library's own ABI doesn't change, since CChar32 only ever appears in the type alias itself.

The Objective-C runtime's type encoding for CChar32 in @objc methods changes from int (i) to unsigned int (I).

Alternatives considered

Keep Unicode.Scalar and validate at the language boundary.

Function arguments and results can be checked, but char32_t data often crosses in buffers where no check is possible (std::u32string::data(), UnsafePointer<CChar32>).

Import char32_t as UInt32, but keep CChar32 as Unicode.Scalar.

This keeps the meaning and mangling of code that names CChar32, but CChar32 would no longer be the Swift type for char32_t. Code that uses CChar32 for char32_t values, like let c: CChar32 = make_char(97), would break instead.

Put the change behind an upcoming feature flag or a new language mode.

CChar32 is just one type alias in the standard library, and rarely used. Making its meaning depend on each module's language mode would need special handling in the compiler, and a library and its clients built in different modes would disagree on what CChar32 means.

Future work

CWideChar (wchar_t) is also Unicode.Scalar on non-Windows platforms and has the same problem. wchar_t is used more widely, so changing it would break more code. Dealing with that is left for a separate proposal.

13 Likes

Swift evolution PR: Propose changing CChar32 to UInt32 by j-hui · Pull Request #3549 · swiftlang/swift-evolution · GitHub

1 Like