Simo Virokannas

Writings and ramblings

Numeric Value of (nothing) Is Zero

Background

Part of what I do is software salvage.

I’ve had the joy of working on bringing life back to abandoned codebases, extend them with new features, modernize services and translate software from obsolete programming languages, operating systems and frameworks.

A frequent type of software modernization has risen with the slow tilt of the dinner table from Objective-C to Swift. Swift can now do everything Objective-C did, and more. In both cases, if you need to write something critically fast and closer to the hardware, you’ll do those bits in C or C++, and Swift still provides a way for bridging that gap. Meanwhile, Objective-C is no longer being updated or maintained by Apple.

All of the Apple-provided frameworks are accessible from both.

This makes translation from one to the other extremely easy. Theoretically, you could take each line of Objective-C and write a corresponding line in Swift, and call it a day. But as stated in Transference of Complexity this would mean a larger total sum of complexity, since Swift is a higher-level language.

But what happens when the a developer drops a mistake in logic and unwittingly works around it, resulting in accidentally correct, yet completely undocumented behavior?

(non-) array (non-) subscript

Problem: A feature from the original application, translated directly from a language to another using the same frameworks, behaves completely differently.

The original source code has an array lookup, using a running index from a for loop. This alone seems like a bad idea, but the original logic depends on it.

After directly translating the code, using the same input data:

  • Original application: runs fine, the result from the array is correct
  • Swift translation: Fatal error: Index out of range

Adding a check not to pick a value in either case would sound perfectly sane, but the original logic doesn’t have that. Also, the rest of the code depends on getting a value from this lookup. So figuring this out first would be necessary.

Reading through the logic these two situations quickly become apparent:

  • The array might not have any elements
  • The index may be larger than the length of the array

The documentation for NSArray for Objective-C provides an important fact: accessing an index that doesn’t exist will throw an exception. So surely the index must exist when running the original application.

All these assumptions go out the window, when running this in debug quickly unveils a third fact:

  • The array doesn’t exist

What’s happening?

The following is an oversimplified example of the actual situation in an Objective-C codebase. This code runs, compiles, finishes without any errors:

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
  NSArray *list;
  NSLog(@"%d", (int)[list[47] integerValue]);
  // Output: 0
  return 0;
}

One might say: but why, that’s obviously wrong – there is no 48th element in the array! It’s not even initialized!

Looking again at the NSArray documentation, specifically the one for Objective-C, it does specify what should happen. The subscript shortcut is defined as being the same as calling objectAtIndex, and objectAtIndex states the following:

If index is beyond the end of the array (that is, if index is greater than or equal to the value returned by count), an NSRangeException is raised.

NSArray documentation

But, by design, all NSObjects (such as NSArray) can also be expressed as id. Let’s see what happens if we slightly modify our test:

id list = nil;
NSLog(@"%d", (int)[list[47] integerValue]);
// Output: 0
  • Since the array object isn’t really “there”, the dynamic method call goes just to an empty shell of an NSObject.
  • The subscript operator [] is mapped to objectAtIndexedSubscript.
  • Since there’s no object, there’s no subscriptable elements.
  • The returned object is (null).

So, now we have a null object, an empty id just like the supposed non-list we began with! What really happens then is this:

id value = nil;
NSLog(@"%d", (int)[value integerValue]);
// Output: 0

The (null) NSObject responds to the integerValue! You can even write this as:

NSLog(@"%d", (int)[(id)nil integerValue]);
// Output: 0

However, if you do:

NSObject *value = nil;
NSLog(@"%d", (int)[value integerValue]);
// Won't compile: No visible @interface for 'NSObject' declares the selector 'integerValue'

The underlying mechanism is:

  • id is a special kind of a variable, it’s a pointer to an instance of a class.
  • It contains one variable: isa which is of type Class.
  • This isa pointer is supposed to point to an instance of a class.
  • When the id is assigned as nil (or unassigned altogether), this contained pointer is just a null pointer, 0x0 – the same kind of a (null) that gets returned by the call to an undefined subscript method.

When you call integerValue on a null id, it still compiles, because potentially it could contain either an NSNumber or NSString, both of which are known to the compiler because of the imported Foundation.h at the very top.

Let’s try the same as at the beginning, but without the Foundation.h (or NSArray or NSLog for that matter, since they’re declared through it):

int main(int argc, const char * argv[]) {
  id list;
  return (int)[list[47] integerValue];
  // Error: No known instance method for selector 'objectAtIndexedSubscript:'
  // Error: No known instance method for selector 'integerValue'
}

But what happens if we add a dummy interface at the top that declares these selectors, never to be used?

@interface dummy
-(id) objectAtIndexedSubscript:(int) index;
-(int) integerValue;
@end
int main(int argc, const char * argv[]) {
    id list;
    return [list[47] integerValue];
}
// Returns: 0

Solution: No array? Zero. Index overflow? Zero.

In this particular case, the immediate fix was easy, after writing out the logic just to do sanity checks and using zero as a default value.

Conclusion

Sometimes the logic is hidden in what fails. When going from a language with no Optional comprehensions to one that does, use the null coalescing operator cautiously. This situation was first caused, then masked, in the new implementation by having the array created in a function that always returns a valid array object. While that is fine, in this case returning an optional and the occasional nil would have dropped the problem into the right spot where it would have been quicker to catch.

As an additional lesson, don’t say “It’s fine” if it really isn’t. Objective-C has some really interesting quirks, not least being the ability to be completely happy to compile a call to a non-existent instance method for an object even if it’s not even remotely compatible. That, combined with the return type of id for that imaginary method, can be a setup for a really nice cascading failure.

If you’ve ran into anything similar, please feel free to drop your experiences in comments below.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.