Wednesday, July 15, 2026

Unicode, circa 1994

Public announcement of the day: on the Objective C++ compiler than ships with Xcode 26.6 (and maybe before, who knows), the option -fshort-wchar is broken, unless you also specify -fno-builtin-wcslen, and it only manifests in release builds. The evidence is overwhelming.

My iOS app is a port from Windows [CE], where wchar_t has been a synonym for unsigned short since days immemorial, and the C/C++ run-time library provides the wide-character versions for all the string functions - wcslen, wcschr, wcsspn, etc. The calls to those were all over the codebase. When porting to iOS, many years ago, I had to specify -fshort-wchar to keep the binary compatibility. Naturally, all the widechar RTL calls broke, since the Apple's RTL was assuming 4 byte wchar_t, and the linker was smart enough to scream on that discrepancy. So I did the lazy thing: provided naive but workable implementations for all widechar RTL functions used, and #defined them in. E. g:

size_t w2_wcslen(LPCWSTR s)
{
size_t n = 0;
while(*s++)
n++;
return n;
}

// And then globally:
#define wcslen w2_wcslen

And life was good.

Then in some version of LLVM, they have introduced the optimization where it detects a strlen-like algorithm and forwards the call to the built-in strlen or - crucially - wcslen. And that optimization, at least in Xcode 26.6, is not, apparently, looking at the wchar_t size override option. After my app was turned down by the App Store over crashes, after numerous attempts to reproduce the crash in the debug build, after an utter failure of my trusty crash reporter to produce a workable report, I've figured out how to debug a release build on a device, and saw with my own eyes that w2_wcslen was returning bogus values. Looking inside the disassembly of the release build, the function had a body, but it was calling into RTL's wcslen.

The debug build would execute as written.

Tracking the manifestation of this to the root cause was quite a journey. In my case, it manifested as an out of memory condition, caused by a parsing loop becoming essentially infinite over wrong boundary, with memory allocation on each step. That, by the way, is why the crash reporter didn't report on it - OOM is delivered to an app as neither a POSIX signal nor an NSException.

You don't find a compiler bug every day.


For the record, in order to debug a release build of an app on an iOS device without changing the signing options under project settings, do this:

  • Archive the app
  • In the Organizer, select the archive, click Distribute
  • Select the "Debugging" option, choose a path
  • In the "Devices and simulators" window, connect to the device
  • Install the freshly exported IPA of the app with the + button underneath the list of apps on the device
  • Run the app
  • In Xcode, select the device as the build/run target
  • In the menu, select Debug/Attach to process by name or ID
  • Type the app name, OK



No comments:

Post a Comment