In the session I presented at railsclub I finished off by showing how to use FFI to call into Objective-C (If you weren’t there on then my slides are online - I would recommend browsing through the section on FFI). I didn’t go into any detail because it was the end of what I think was a pretty technically intense session.
This definitely comes under the category of “doing something to prove it can be done” rather than anything more serious, but if you’d like to know how it works then read on!
The bridge
The idea of this bridge is that we wrap instances of NSObject
with a ruby object that forward messages to the underlying object. I’m going to explain how the bridge works - you might want have a copy of the file open for reference.
1 2 3 4 5 6 7 8 |
|
This section is straightforward: it loads the Objective-C runtime library (libobjc) and the Foundation and Appkit framework. These frameworks are only there because the demo I did happens to use those frameworks. If you wanted to call other frameworks then you’d need to add them to the list. Them some typedefs are setup to make the rest of the code more readable.
Now it’s time to attach some functions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
In order these are:
NSLog
Logs a format string to the console - for debugging only.
objc_getClass
Looks up a class by name, returning a instance of Class
.
sel_registerName
Converts a string into a selector (the runtime APIs use selectors to identify methods).
objc_msgSend
This is the core objective-C dispatch function: [some_object doThisWith:foo and:bar]
turns into a call to objc_msgSend(
some_object, @selector(dothisWith:and:),foo,bar)
. This is basically the same as ruby’s send
. The :varargs
in the argument list tells FFI that this function (and thus the corresponding ruby method) takes a variable number of arguments.
This first version is used for anything that returns an integer or pointer to an object. Mike Ash describes objc_msgSend
in detail and has another post showing you how to build it yourself.
objc_msgSend_f
, objc_msgSend_d
These are used for methods that return float/doubles. On i386 this requires calling objc_msgSend_fpret
instead of objc_msgSend
. On x64 we call the same function in all cases, but we need separate FFI bindings so that the return value is picked correctly.
The next set of functions are how we get method information out of the Objective-C runtime:
class_respondsToSelector
Corresponds to respond_to?
. This returns a uchar because the FFI :bool
type corresponds to the bool
type (from C99) whereas the Objective-C BOOL
type is an unsigned char. The pitfall this creates is that you need to check the value for zero-ness rather than truthyness.
class_getInstanceMethod
Returns a pointer to the Method
structure for the method.
method_getNumberOfArguments
Returns the number of arguments the method expects (ie the number of :
in the selector).
method_getReturnType
Retrieves the return type (more about types later).
method_getArgumentType
Retrieves the type of the n-th argument.
object_getClass
Retrieves the class for a given object.
The wrapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
This is our proxy object class. It is a subclass of FFI::Pointer
and the pointer value will be the address of the object. This means that when pass self
to a function expecting a pointer argument we are passing though object’s address.
The first step is extracting the selector and arguments from my abuse of ruby’s hash notation. I’m expecting [some_object doThisWith:foo and:bar]
to result in objc_send
being invoked like so
1
|
|
Or for a method with no arguments (eg [string length]
)
1
|
|
extract_arguments_and_method_name
unpacks this string/hash into an array of argument values and the selector name (i.e. doThisWith:and:
in the first example).
Then we convert this string into the actual selector object that the runtime APIs expect. Once class_respondsToSelector
has confirmed that the method actually exists we call invoke_selector(selector, arguments)
.
Varargs
Before we peek inside invoke_selector
, a brief digression on how FFI’s support for varargs work. When you have declared a function as taking varargs then each of the variable arguments must be preceded by its ffi type (:pointer, :string, :int and so on). For example you could call the libc printf
like so
1 2 3 4 5 6 7 8 |
|
So in order to call objc_msgSend
we need to find the ffi type for each argument. We also need to workout the return type so we know which version of objc_msgSend
to call.
Objective-C type signatures
This is where method_getNumberOfArguments
, method_getReturnType
and method_getArgumentType
come into play. method_getNumberOfArguments
tells us how many arguments to expect and method_getArgumentType
allows you to grab the signature for a particular argument.
A signature is a string describing a type. The <objc/runtime.h>
header defines what each of the symbols mean, for example @
means object and ’d’ means double. Some of these can be combined with r
, which means const
. The objc_signature_to_ffi_type
method is just a big switch statement mapping these various types onto ffi types (it’s incomplete - I build what I needed).
Invoke selector
1 2 3 4 5 6 7 8 9 10 11 |
|
The first step is to get the ffi types we need - that is what signature_for_selector
does (using the previously mentioned runtime functions and objc_signature_to_ffi_type
). At the end of this return_type_signature
will be something like @
(if the method returns an object) and argument types will be something like [:pointer, :pointer, :int]
We then zip the argument types with the argument values and use the return type to call the appropriate variant of objc_msgSend
. In the float/double case we do no further work (since we have told ffi the appropriate return value) but in the default case we may need to convert the return value from the pointer returned to a string, integer, char etc. as appropriate (coerce_return_value
does this).
Joining the dots
Lastly a little magic is done with const_missing
and method_missing
. If you try to access Objc::NSString
then const_missing
will use objc_getClass
to get the Class
(which is itself an object), wrap it in an Objc::Object
and set the constant for future use. If you then hit method_missing
on that wrapper then it will call objc_send
.
The code I ran during the session was
1 2 3 |
|
The steps that happen are
- referencing
Objc::NSString
causes the constant missing hook to fire and creates a wrapper for theNSString
class. method_missing
is called on this class. This is converted to a call toobjc_send(:stringWithCString => "/Users/fred/Desktop/sound.mp3", :encoding => 4)
, 4 being the value of NSUTF8Encoding- A wrapper object is created around the returned NSString
- A wrapper is created for the
NSSound
class. alloc
is called on the wrapper, triggering method missing, and theninitWithContentsOfFile
is called on the object returned byalloc
- the returned
NSSound
is wrapped and stored insound
play
is called onNSSound
. This takes no arguments so is mapped onto a call toobjc_send :play
What’s missing
There’s an awful lot this code doesn’t do:
- considering solely the task of method invocation it doesn’t deal with things like variable arguments or structures as arguments/return values (You need to use
objc_msgSend_stret
to return large structs). - it doesn’t deal with the Objective-C equivalent of
method_missing
(I think!) - the object hierarchy is confusing: everything appears to be an instance of
Objc::Object
whereas it would probably less surprising if they appeared to be instances of the appropriate classes. This would require redefining methods likeis_a?
,class
and so on. Equally this might introduce an amount of complication that is unwarranted. - if you were to use this in a more long running situation you would need to handle memory management, in particular autorelease pool creation/draining.
Thinking more widely this snippet doesn’t allow you to define or extend classes. This should be a matter of calling the right runtime functions to create the classes and then calling class_addMethod
to add methods. Method bodies are specified as function pointers, so this would require the use of FFI callbacks. I suspect it would be best to work out what to do with the object hierarchy before embarking on this.