Type Encodings Explained

Sun, 5 Jul 2020 17:23:00 +0200

The @encode directive and how Objective-C encodes type details.

In this article we’re going to look into Objective-C’s type encodings, and then we’ll then build a small convenience class to parse these encodings so we can use them more easily.

Type encodings are compact string codes that describe the details of a type, and are compiled into the metadata of your app. We touched on them in Inspecting Objective-C Properties, where they appeared in the type attribute of each property.

Every type has an encoding, and although they are always represented as C strings, most of them are just a single character. For example, int types are represented by i, and double is d.

The other scalar types are just as straightforward and don’t need much explanation, but you can find the full list in Apple’s Runtime Programming Guide.

Type encodings are also available for Objective-C specific types, such as:

id pointers encode as @

Class encodes as #

SEL (selectors) encode as :

Some encodings are more than just a single character and include extra information.

More complex encodings

Object types

While plain id types are represented as @, an object of a specific class like NSString has the class name appended in quotes: @"NSString".

Even protocols are included in the encoding if the type is declared as such. Something like id<NSCopying> encodes to @"<NSCopying>".

Block types

Blocks are treated like objects, but their type encodings do not include any information about the block parameters or return type. Every block type is represented as @?, which combines the encodings for “object” and “function pointer”.

Getting the type encodings for the parameters of a block is quite a bit more complicated, but it is possible. The internal details of blocks are documented in the Block Implementation compiler spec, and you can find some example code which will extract them for you.

Compound types

Structs and unions are encoded with the types of all of their member fields. For example, CGSize, which is defined as:

struct CGSize {
    CGFloat width;
    CGFloat height;
};

Has a type encoding of {CGSize=dd}.

Note that in 64-bit apps, CGFloat is defined as double (not float) which is why the two fields have a type of d.

This works even for nested structs like CGRect, which is a combination of a CGPoint origin and a CGSize. The type encoding for CGRect is:

{CGRect={CGPoint=dd}{CGSize=dd}}

Depending on where the type encoding comes from, it can sometimes even include the member names as well:

{CGRect="origin"{CGPoint="x"d"y"d}"size"{CGSize="width"d"height"d}}

Other C types

The remaining types in the list like void, C strings, C arrays, bitfields, function pointers and non-object pointers are rarely used in Objective-C, so we won’t go into more detail on them here.

Instead, lets take a look at what encodings are used for.

Where type encodings are used

In the Objective-C runtime, type encodings are returned by several functions for describing the types of:

Properties (property_getAttributes)

Ivars (ivar_getTypeEncoding)

Method parameters and return types (method_getTypeEncoding)

They are also used by some Foundation classes like NSMethodSignature, which wraps the method-related runtime functions. The NSValue class uses an encoding to keep track of the type of value it is wrapping, which is why you need to initialise it with an objCType parameter.

To get the encoding of any type, we use the @encode compiler directive. For example:

NSLog(@"%s", @encode(CGSize));

// Prints: {CGSize=dd}

Note that we need to use a type here, not a variable. To get the encoding of a variable, wrap it in the typeof() operator first.

A small gotcha is that the @encode directive can return slightly different encodings than the runtime does. This is why the encoding of a struct might include the member names: @encode does not add them, but ivar_getTypeEncoding will. Not a big problem, but it can make it more complicated to compare type encodings.

A wrapper for type encodings

Let’s create a convenience class for dealing with type encodings, so we don’t have to parse C strings every time.

Firstly, we can turn the type encodings table into an enum:

typedef NS_ENUM(char, EncodedType) {
    TypeChar              = 'c',
    TypeInt               = 'i',
    TypeShort             = 's',
    TypeLong              = 'l',  // note: long encodes to 'q' on 64 bit
    TypeLongLong          = 'q',
    TypeUnsignedChar      = 'C',
    TypeUnsignedInt       = 'I',
    TypeUnsignedShort     = 'S',
    TypeUnsignedLong      = 'L',
    TypeUnsignedLongLong  = 'Q',
    TypeFloat             = 'f',
    TypeDouble            = 'd',
    TypeBool              = 'B',  // note: BOOL encodes to 'c' on 64 bit
    TypeVoid              = 'v',
    TypeCString           = '*',
    TypeObject            = '@',
    TypeClass             = '#',
    TypeSelector          = ':',
    TypeArray             = '[',
    TypeStruct            = '{',
    TypeUnion             = '(',
    TypeBitField          = 'b',
    TypePointer           = '^',
    TypeUnknown           = '?',
};

We’ll just use the first character of the encoding, since it’s enough to tell us which type it is.

You might think “shouldn’t we use @encode to get these instead of hardcoding them?”. Well yes, technically, but these haven’t changed in decades and are very unlikely to change in the future. Since hardcoding them makes our life easier, I’m prepared to compromise in this particular case 🙂

Now let’s define an interface with some properties of encodings that would be handy to have:

@interface TypeEncoding : NSObject

@property (nonatomic, readonly) EncodedType type;

/// The raw type encoding
@property (nonatomic, readonly, copy) NSString *encoding;

/// If the type is either an object, a Class type, or a block
@property (nonatomic, readonly) BOOL isObjectType;

/// If the type is float or double
@property (nonatomic, readonly) BOOL isFloatType;

/// If the type is a signed or unsigned integer of any size
@property (nonatomic, readonly) BOOL isIntType;

/// The class of the object type. Nil for anything except TypeObject.
@property (nonatomic, readonly, nullable) Class classType;

- (instancetype)initWithEncoding:(NSString *)encoding;

@end

We’ll implement this by extracting the EncodedType straight off the start of the encoding string:

@implementation TypeEncoding

- (instancetype)initWithEncoding:(NSString *)encoding {
    self = [super init];
    if (self) {
        _encoding = [encoding copy];
        _type = [encoding characterAtIndex:0];

        static const unichar TypeConst = 'r';
        if (_type == TypeConst) {
            // const C strings are encoded as "r*", skip the 'r'
            _type = [encoding characterAtIndex:1];
        }
    }
    return self;
}

We could set all the properties right in init, but we’ll extract those into separate getters. The isObjectType and isFloatType methods just test if the EncodedType matches certain values:

- (BOOL)isObjectType {
    return _type == TypeObject || _type == TypeClass;
}

- (BOOL)isFloatType {
    return _type == TypeFloat || _type == TypeDouble;
}

We could explicitly check for each of the eleven integer types (including BOOL), but another way would be to put all the integer types in an array and test if it contains our value. Since EncodedType is a character, the array of types just becomes a C string:

- (BOOL)isIntType {
    static const char *integralTypes = "cislqCISLQB";
    return strchr(integralTypes, _type) != NULL;
}

The classType method needs a little string manipulation to extract the class name (if present) out of the type encoding. So an encoding like @"NSURL" will return NSURL, and id will default to NSObject.

- (Class)classType {
    if (_type == TypeObject
        && [_encoding hasPrefix:@"@\""] && [_encoding hasSuffix:@"\""]) {
        NSRange range = NSMakeRange(2, _encoding.length - 3);
        NSString *classStr = [_encoding substringWithRange:range];
        return NSClassFromString(classStr) ?: NSObject.class;
    }
    return nil;
}

@end

Wrapping up

We could add some more functionality to our TypeEncoding class, such as extracting protocol names or the names and member types of structs, but we can leave that until we need it.

One place this wrapper would already be useful is on ClassProperty from the previous post. The encodeType property can be changed from NSString to TypeEncoding and initialised as:

_encodeType = [[TypeEncoding alloc] initWithEncoding:attribDetail];

TypeEncoding will also come in very handy for future posts, when we need to get and store the values of properties and ivars dynamically.

Useful links

Inspecting Objective-C Properties post

Apple’s Type Encodings reference

Type Encoding implementation in LLVM

Type Encodings on NSHipster