NSLogv C strings encoding

Viewed 287

I wrote a small logging wrapper using NSLogv:

void MyLog(const char* format, ...) {
    va_list vargs;
    va_start(vargs, format);
    NSString* formatStr = [NSString stringWithUTF8String:format];
    NSLogv(formatStr, vargs);
    va_end(vargs);
}

which I can use like that:

MyLog("%d - %s", 123, "ABCD");

The problem I have is when I use characters outside the standard ASCII range:

MyLog("%d - %s", 123, "АБВГ");

NSLogv fails to properly encode those characters:

2019-10-01 11:10:30.890346+0300 TestApp[86349:7051788] 123 - –ê–ë–í–ì

What would be the proper way to encode such characters while maintaining the variadic signature of my helper method?

P.S. Tried on both X86_64 simulator and ARM64 device

If I convert the C strings to UTF16, then it works as expected:

std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
std::u16string value16 = convert.from_bytes("АБВГ");
MyLog("%d - %S", 123, value16.c_str());
2 Answers

You have discovered a curious "feature" of formatting in Objective-C and should visit Apple's Feedback Assistant and report it.

So what is going on? Well it is nothing to do with variadic functions at all, and nothing to do with NSLogv per se. Rather it is to do with the underlying Objective-C formatting code which NSLog, NSLogv, stringWithFormat:, et al use and the type of the format string itself...

Here is a simple demonstration of the "feature":

- (void)demo
{
   char *sample = "АБВГ"; // This will be UTF-8 encoded

   // use %p to show address, %s to show string, \n as printf doesn't add one
   char *cFormat = "%p - %s\n";
   NSString *nsFormat = @"%p - %s\n"; // produces an __NSCFConstantString
   NSString *convertedFormat = [NSString stringWithUTF8String:cFormat]; // produces an __NSCFString

   printf(cFormat, sample, sample); // works
   NSLog(convertedFormat, sample, sample); // fails with __NSCFString
   NSLog(nsFormat, sample, sample); // works with __NSCFConstantString

   NSLog(@"formats equal: %s", [convertedFormat isEqualToString:nsFormat] ? "yes" : "no"); // __NSCFString & __NSCFConstantString are equal
}

Run that and the console will show something like:

0x1000013f8 - АБВГ
2019-10-01 10:25:48.222537+0100 demo[8435:1431874] 0x1000013f8 - –ê–ë–í–ì

2019-10-01 10:25:48.222560+0100 demo[8435:1431874] 0x1000013f8 - АБВГ
2019-10-01 10:25:48.222582+0100 demo[8435:1431874] formats equal: yes

So the C library printf works, NSLog with a constant NSString format works, but NSLog with NSString format converted from a char * does not, and yet the latter two formats compare equal... Notice also that in the failing case NSLog adds an extra newline.

The bad output, –ê–ë–í–ì, is the same interpretation of the argument string that Xcode shows for the string's memory bytes. So the underlying type of the format string is determining how the underlying bytes of the argument string are interpreted...

Such a curious "feature" makes one wonder whether it was designed for some reason, or whether we're missing the obvious... Maybe someone else can enlighten us, but unless they do let's call this a (curious) bug!

Workaround

As the above demo shows using the C library format functions work so if you are happy with losing NSLog's preamble on each outline you can just use one of them in your function:

void MyLog(const char *format, ...)
{
   va_list vargs;
   va_start(vargs, format);
   vprintf(format, vargs);
   va_end(vargs);
}

If you wish to keep to NSLog for the output you can use one of the C library's equivalents of stringWithFormat:, here is a version of your function which dynamically allocates the required space for the formatted C string and then free's it (ARC won't do that for you!):

void MyLog(const char *format, ...)
{
   va_list vargs;
   va_start(vargs, format);
   char *output;
   vasprintf(&output, format, vargs);
   NSLog(@"%s", output);
   free(output);
   va_end(vargs);
}

The "feature" still exists with the latest Xcode 11 and macOS Catalina Beta so please do go report it at Apple's Feedback Assistant.

HTH

Look's like bug in NSLogv function, because NSLog, printf, vprintf works fine. I can suggest use macro instead:

#define MyLog(arg, ...) NSLog(@ arg, __VA_ARGS__)
Related