Space Vatican

Ramblings of a curious coder

Rickrolling With Ruby and Objective-C

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
module Objc
  extend FFI::Library
  ffi_lib 'objc', '/System/Library/Frameworks/Foundation.framework/Foundation', '/System/Library/Frameworks/Appkit.framework/Appkit'

  typedef :pointer, :method
  typedef :pointer, :selector
  typedef :pointer, :id
end

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
module Objc
  attach_function :NSLog, [:pointer], :void
  attach_function :objc_getClass, [:string], :pointer
  attach_function :sel_registerName, [:string], :selector
  attach_function :objc_msgSend, [:id, :selector, :varargs], :id
  if FFI::Platform::ARCH =~ /i386/
    attach_function :objc_msgSend_f, :objc_msgSend_fpret, [:id, :selector, :varargs], :float
    attach_function :objc_msgSend_d, :objc_msgSend_fpret, [:id, :selector, :varargs], :double
  else
    attach_function :objc_msgSend_f, :objc_msgSend, [:id, :selector, :varargs], :float
    attach_function :objc_msgSend_d, :objc_msgSend, [:id, :selector, :varargs], :double
  end

  attach_function :method_getNumberOfArguments, [:method], :uint
  attach_function :class_getInstanceMethod, [:pointer, :selector], :method
  attach_function :class_respondsToSelector, [:pointer, :selector], :uchar
  attach_function :method_getReturnType, [:method, :pointer, :size_t], :void
  attach_function :method_getArgumentType, [:method, :uint, :pointer, :size_t], :void
  attach_function :object_getClass, [:pointer], :pointer
end

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
module Objc
  class Object < FFI::Pointer
    def objc_send(args)
      arguments, method_name = extract_arguments_and_method_name(args)
      selector = Objc.sel_registerName(method_name)

      if Objc.class_respondsToSelector(klass, selector).nonzero?
        invoke_selector(selector, arguments)
      else
        raise NoMethodError, "#{self} does not respond to #{method_name}"
      end
    end
  end
end

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
objc_send doThisWith: foo, and: bar

Or for a method with no arguments (eg [string length])

1
objc_send :length

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
module Stdlib
  extend FFI::Library
  ffi_lib 'c'
  attach_function 'printf', [:string, :varargs], :int
end

Stdlib.printf "Hello %s, the time is %d:%d\n",
              :string, 'world', :int, Time.now.hour, :int, Time.now.min

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
def invoke_selector(selector, arguments)
  return_type_signature, argument_types = signature_for_selector(selector)
  args_with_types = argument_types.zip(arguments).flatten
  case return_type_signature
  when 'f' then Objc.objc_msgSend_f(self, selector, *args_with_types)
  when 'd' then Objc.objc_msgSend_d(self, selector, *args_with_types)
  else
    pointer = Objc.objc_msgSend(self, selector, *args_with_types)
    pointer && !pointer.null? ? coerce_return_value(pointer, return_type_signature) : nil
  end
end

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
path = Objc::NSString.stringWithCString "/Users/fred/Desktop/sound.mp3", encoding: 4
sound = Objc::NSSound.alloc.initWithContentsOfFile path, byReference: 1
sound.play

The steps that happen are

  1. referencing Objc::NSString causes the constant missing hook to fire and creates a wrapper for the NSString class.
  2. method_missing is called on this class. This is converted to a call to objc_send(:stringWithCString => "/Users/fred/Desktop/sound.mp3", :encoding => 4), 4 being the value of NSUTF8Encoding
  3. A wrapper object is created around the returned NSString
  4. A wrapper is created for the NSSound class.
  5. alloc is called on the wrapper, triggering method missing, and then initWithContentsOfFile is called on the object returned by alloc
  6. the returned NSSound is wrapped and stored in sound
  7. play is called on NSSound. This takes no arguments so is mapped onto a call to objc_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 like is_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.