iOS/C: Convert "integer" into four character string

10.2k Views Asked by At

A lot of the constants associated with Audio Session programming are really four-character strings (Audio Session Services Reference). The same applies to the OSStatus code returned from functions like AudioSessionGetProperty.

The problem is that when I try to print these things out of the box, they look like 1919902568. I can plug that into Calculator and turn on ASCII output and it'll tell me "roch", but there must be a programmatic way to do this.

I've had limited success in one of my C functions with the following block:

char str[20];
// see if it appears to be a four-character code
*(UInt32 *) (str + 1) = CFSwapInt32HostToBig(error);
if (isprint(str[1]) && isprint(str[2]) && isprint(str[3]) && isprint(str[4])) {
    str[0] = str[5] = '\'';
    str[6] = '\0';
} else {
    // no, format as integer
    sprintf(str, "%d", (int)error);
}

What I want to do is to abstract this feature out of its current function, in order to use it elsewhere. I tried doing

char * fourCharCode(UInt32 code) {
    // block
}
void someOtherFunction(UInt32 foo){
    printf("%s\n",fourCharCode(foo));
}

but that gives me "à*€/3íT:ê*€/+€/", not "roch". My C fu isn't very strong, but my hunch is that the above code tries to interpret the memory address as a string. Or perhaps there's an encoding issue? Any ideas?

16

There are 16 best solutions below

3
On BEST ANSWER

The type you're talking about is a FourCharCode, defined in CFBase.h. It's equivalent to an OSType. The easiest way to convert between OSType and NSString is using NSFileTypeForHFSTypeCode() and NSHFSTypeCodeFromFileType(). These functions, unfortunately, aren't available on iOS.

For iOS and Cocoa-portable code, I like Joachim Bengtsson's FourCC2Str() from his NCCommon.h (plus a little casting cleanup for easier use):

#include <TargetConditionals.h>
#if TARGET_RT_BIG_ENDIAN
#   define FourCC2Str(fourcc) (const char[]){*((char*)&fourcc), *(((char*)&fourcc)+1), *(((char*)&fourcc)+2), *(((char*)&fourcc)+3),0}
#else
#   define FourCC2Str(fourcc) (const char[]){*(((char*)&fourcc)+3), *(((char*)&fourcc)+2), *(((char*)&fourcc)+1), *(((char*)&fourcc)+0),0}
#endif

FourCharCode code = 'APPL';
NSLog(@"%s", FourCC2Str(code));
NSLog(@"%@", @(FourCC2Str(code));

You could of course throw the @() into the macro for even easier use.

0
On

Here's the Swift 3 version of j.s.com's code, cleaned up to eliminate repetition:

func debugPrintFourCcCode(_ value: Int) {
  var res = ""

  if value <= 0x28 {
    res = "\(value)"
  } else {
    func add(_ pos: Int) {
      res.append(String(UnicodeScalar((value >> pos) & 255)!))
    }

    add(24)
    add(16)
    add(8)
    add(0)
  }

  NSLog("Code: \(res)")
}
2
On
char str[5];
str[4] = '\0';
long *code = (long *)str;
*code = 1919902568;
printf("%s\n", str);
5
On

Integer = 4 bytes = 4 chars. So to convert from integer to char * you can simply write:

char st[5] = {0};
st[0] = yourInt & 0xff;
st[1] = (yourInt >> 8) & 0xff;
st[2] = (yourInt >> 16) & 0xff;
st[3] = (yourInt >> 24) & 0xff;

To convert it back:

yourInt = st[0] | (st[1] << 8) | (st[2] << 16) | (st[3] << 24);
2
On

In Swift you would use this function:

func str4 (n: Int) -> String
{
    var s: String = ""
    var i: Int = n

    for var j: Int = 0; j < 4; ++j
    {
        s = String(UnicodeScalar(i & 255)) + s
        i = i / 256
    }

    return (s)
}

This func will do the same like above in a third of the time:

func str4 (n: Int) -> String
{
    var s: String = String (UnicodeScalar((n >> 24) & 255))
    s.append(UnicodeScalar((n >> 16) & 255))
    s.append(UnicodeScalar((n >> 8) & 255))
    s.append(UnicodeScalar(n & 255))
    return (s)
}

The reverse way will be:

func val4 (s: String) -> Int
{
    var n: Int = 0
    var r: String = ""
    if (countElements(s) > 4)
    {
        r = s.substringToIndex(advance(s.startIndex, 4))
    }
    else
    {
        r = s + "    "
        r = r.substringToIndex(advance(r.startIndex, 4))
    }
    for UniCodeChar in r.unicodeScalars
    {
        n = (n << 8) + (Int(UniCodeChar.value) & 255)
    }

    return (n)
}
0
On

A version for the Mac, but not iOS:

extension String {

    init(_ fourCharCode: FourCharCode) { // or `OSType`, or `UInt32`
        self = NSFileTypeForHFSTypeCode(fourCharCode).trimmingCharacters(in: CharacterSet(charactersIn: "'"))
    }

}
0
On

I suggest using a function like this:

static NSString * NSStringFromCode(UInt32 code)
{
    UInt8 chars[4];
    *(UInt32 *)chars = code;
    for(UInt32 i = 0; i < 4; ++i)
    {
        if(!isprint(chars[i]))
        {
            return [NSString stringWithFormat:@"%u", code];
        }
    }
    return [NSString stringWithFormat:@"%c%c%c%c", chars[3], chars[2], chars[1], chars[0]];
}

This will ensure that you don't end up with some random results for some FourCharCodes that are actual numbers like kCMPixelFormat_32ARGB = 32.

0
On

This is a variation of my preferred answer above (thanks pawneebill) I save the creation and deallocation of an NSData intermediate object by using a different NSString initializer.

#import <Cocoa/Cocoa.h>
#import <CoreServices/CoreServices.h> // for EndianU32_NtoB
@implementation NSString (FourCC)

+ (NSString *) stringFromFourCC: (OSType) cccc
{
    NSString * result = nil;
    cccc = EndianU32_NtoB(cccc); // convert to network byte order if needed
    result = [[NSString alloc] initWithBytes: &cccc length: sizeof(cccc) encoding: NSMacOSRomanStringEncoding]; // lossless 8-bit encoding
    return result;
}

Or, more compact but less explained...

+ (NSString *) stringFromFourCC: (OSType) cccc
{
    cccc = EndianU32_NtoB(cccc); // convert to network byte order if needed
    return [[NSString alloc] initWithBytes: &cccc length: sizeof(cccc) encoding: NSMacOSRomanStringEncoding]; // lossless 8-bit encoding
}
2
On
  • Handles different endian architectures
  • iOS and MacOS(et al)
  • Easy to understand
  • Resulting string (should be) identical to OSType even with 8-bit characters
#import <Cocoa/Cocoa.h>
#import <CoreServices/CoreServices.h> // for EndianU32_NtoB
@implementation NSString (FourCC)

+ (NSString *) stringFromFourCC: (OSType) cccc
{
    NSString * result = nil;
    cccc = EndianU32_NtoB(cccc); // convert to network byte order, if needed
    NSData * data = [NSData dataWithBytes: &cccc length: sizeof(OSType)];
    result = [[NSString alloc] initWithData: data encoding: NSWindowsCP1252StringEncoding]; // lossless 8-bit encoding, could also use NSMacOSRomanStringEncoding
    return result;
}
0
On

Tricksy Swift version:

func convert(fileType: OSType) -> String {
    let chars = [24, 16, 8, 0].map { Character(UnicodeScalar(UInt8(fileType >> $0 & 0xFF)))}
    return String(chars)
}

I've tested on Swift 5, but should work on previous Swift versions.

0
On
public extension UInt32 {
    var string: String {
        String([
            Character(Unicode.Scalar(self >> 24 & 0xFF) ?? "?"),
            Character(Unicode.Scalar(self >> 16 & 0xFF) ?? "?"),
            Character(Unicode.Scalar(self >> 8 & 0xFF) ?? "?"),
            Character(Unicode.Scalar(self & 0xFF) ?? "?")
        ])
    }
}
5
On

In case you're developing for macOS rather than iOS, there's already a built-in function for this in Launch Services. To get the 4cc as NSString:

(__bridge_transfer NSString *)UTCreateStringForOSType(fourccInt)
0
On

Here are my helper functions I use within test targets:

Swift 5:

extension FourCharCode {
    private static let bytesSize = MemoryLayout<Self>.size
    var codeString: String {
        get {
            withUnsafePointer(to: bigEndian) { pointer in
                pointer.withMemoryRebound(to: UInt8.self, capacity: Self.bytesSize) { bytes in
                    String(bytes: UnsafeBufferPointer(start: bytes,
                                                      count: Self.bytesSize),
                           encoding: .macOSRoman)!
                }
            }
        }
    }
}

extension OSStatus {
    var codeString: String {
        FourCharCode(bitPattern: self).codeString
    }
}

private func fourChars(_ string: String) -> String? {
    string.count == MemoryLayout<FourCharCode>.size ? string : nil
}
private func fourBytes(_ string: String) -> Data? {
    fourChars(string)?.data(using: .macOSRoman, allowLossyConversion: false)
}
func stringCode(_ string: String) -> FourCharCode {
    fourBytes(string)?.withUnsafeBytes { $0.load(as: FourCharCode.self).byteSwapped } ?? 0
}

It works for me both on macOS and iOS and closely matches the behaviour of built-in macOS NSFileTypeForHFSTypeCode and NSHFSTypeCodeFromFileType

A couple of gotchas:

  1. use bigEndian and byteSwapped
  2. use macOSRoman encoding
  3. handle long strings by returning 0, as NSHFSTypeCodeFromFileType

The difference of above implementation from standard library methods:

  1. NSFileTypeForHFSTypeCode adds extra single quotes around the sting: NSFileTypeForHFSTypeCode(OSType(kAudioCodecBadDataError))) == "'bada'". I've opted out to just return bada
  2. it fails to produce strings with \0 in the middle, like in NSFileTypeForHFSTypeCode(kAudioFormatMicrosoftGSM) should be ms\01 but returns 'ms
0
On

Working Swift 2.2 version as two extensions:

extension String {
    public var fourCharCode:FourCharCode {
        var string = self
        if unicodeScalars.count < 4 {
            string = self + "    "
        }
        string = string.substringToIndex(string.startIndex.advancedBy(4))

        var res:FourCharCode = 0
        for unicodeScalar in string.unicodeScalars {
            res = (res << 8) + (FourCharCode(unicodeScalar) & 255)
        }

        return res
    }
}

extension FourCharCode {
    public var string:String {
        var res = String (UnicodeScalar((self >> 24) & 255))
        res.append(UnicodeScalar((self >> 16) & 255))
        res.append(UnicodeScalar((self >> 8) & 255))
        res.append(UnicodeScalar(self & 255))
        return res
    }
}
0
On

I wrote this C function for my audio code ... it might be a tad naive, but it does the job for well enough for me:

NSString* fourCharNSStringForFourCharCode(FourCharCode aCode){

char fourChar[5] = {(aCode >> 24) & 0xFF, (aCode >> 16) & 0xFF, (aCode >> 8) & 0xFF, aCode & 0xFF, 0};

NSString *fourCharString = [NSString stringWithCString:fourChar encoding:NSUTF8StringEncoding];

return fourCharString; }
0
On

A more Swifty implementation, working for Swift 4:

extension String {
    init(fourCharCode: FourCharCode) {
        let n = Int(fourCharCode)
        var s: String = ""

        let unicodes = [UnicodeScalar((n >> 24) & 255), UnicodeScalar((n >> 16) & 255), UnicodeScalar((n >> 8) & 255), UnicodeScalar(n & 255)]
        unicodes.flatMap { (unicode) -> String? in
            guard let unicode = unicode else { return nil }
            return String(unicode)
        }.forEach { (unicode) in
            s.append(unicode)
        }

        self = s.trimmingCharacters(in: CharacterSet.whitespaces)
    }
}

Which can be used as followed:

String(fourCharCode: CMFormatDescriptionGetMediaSubType(charCode))