In my last post, I mentioned a few of the fun things that you would soon be able to do with the type-dependent dispatch stuff. It turns out that one of these was easier to implement than I expected. Consider the following line from the class in the last example:
[(B*)a foo: 12];
The object pointed to by
a implements a
-foo: method, but this method takes an
int as the argument, while the one declared in
B takes a
float. This explicit cast means that the compiler will definitely generate the wrong kind of call frame for this method. This is a contrived example, but it's the easiest way of demonstrating this problem.
When I run this line on OS X, I get this:
The value is just whatever nonsense happened to be in the register or stack slot used for passing the third integer argument. Obviously, this is bad. How about libobjc2 with the stuff from my last post?
Calling [A -foo:] with incorrect signature. Method has v12@0:4i8, selector has v12@0:4f8 int: 1094713344
This is a bit better. It still does the wrong thing, but at least it tells you it's doing it. You can add a breakpoint there and find where the problem is. Even without the debugger, you know that something is sending a
-foo: message to an instance of class
A with a floating point value as the first explicit argument instead of an integer.
Now, what happens if you link the program against LanguageKit? Now the results are a lot better:
So what's really going on there? LanguageKit, when it loads, installs a handler for mismatched method invocations. When this is invoked, it constructs a new method that takes the arguments that the selector defined. This method is simple. It either boxes or unboxes the arguments, as required, and then calls the correct method. The handler then does the lookup again and gets the slot for the new method, which it returns.
Any future message send to this class with this type signature will result in the new method being called. This adds some overhead (potentially quite a lot of overhead), but it is probably a lot better than stack corruption.
With this in place, you can do some fun things. For example, suppose we define the following interface:
@protocol IntMap - (void)setObject: (float)a forKey: (int)b; - (float)objectForKey: (int)a; @end
Hopefully you'll recognise these method names as being those used for manipulating a mutable dictionary. The declarations of these methods in the dictionary class take only objects as arguments - dictionaries are maps between objects and objects, not between primitive types. With this fixup enabled, we can do this:
id<IntMap> d = (id<IntMap>)[NSMutableDictionary new]; [d setObject: 42 forKey: 100]; printf("Dictionary contained %f\n", [d objectForKey: 100]);
This creates an
NSMutableDictionary instance and then casts it to this protocol. Now, the compiler will use the types for the methods declared in the protocol, rather than the types declared in the class, when constructing the message send. So, we're intentionally doing the wrong thing, calling these methods with the wrong signature. What happens?
Dictionary contained 42.000000
The runtime, in conjunction with LanguageKit, does what we wanted. If you inspect the classes used, you'll find that the key is being turned into a
BigInt and the value into a
BoxedFloat. These are the two types that LanguageKit uses for auto-boxing integer and floating point types internally.
The end result is that we can (almost) pretend that Objective-C is a pure object-oriented language, just like Pragmatic Smalltalk. In practice, we still need to be explicit about types, but the language is now a lot more forgiving when we make mistakes.