[swift unboxed]

Safely unboxing the Swift language & standard library


@objc and dynamic

Objective-C runtime visibility and the depths of dynamic dispatch in the modern Swift era.

5 December 2017 ∙ Objective-C Interop ∙ written by

The @objc attribute controls visibility of Swift bits from Objective-C. It’s back under the spotlight with Swift 4 and more specifically the changes in SE-0160, Limiting @objc inference.

What does @objc mean? What about dynamic? How do they work? And what’s different in the post-Swift 4 world?

This article will be a bit different — we’ll compile a simple bit of code and inspect it rather than look at something like Objective-C runtime source code.

Semi-Permeable Membranes

Way back in 2015 I gave a talk titled Switching Your Brain to Swift. Swift 2 was in beta and mixed codebases were all around us.

I used the analogy of a semi-permeable membrane — things make it across the language divide from Objective-C to Swift very easily, but not as easily from Swift to Objective-C.

There are two keywords to keep in mind when dealing with interoperability:

  • @objc means you want your Swift code (class, method, property, etc.) to be visible from Objective-C.
  • dynamic means you want to use Objective-C dynamic dispatch.
objc and dynamic

The state of the world circa 2015.

The slightly confusing bit for me was that this scheme conflates the concepts of visibility and dispatch.

In Swift 3 and earlier, dynamic also implied @objc. New in Swift 4, dynamic only means dynamic dispatch and says nothing about Objective-C visibility.

However, there’s no such thing as Swift dynamic dispatch; we only have the Objective-C runtime’s dynamic dispatch. That means you can’t have just dynamic and you must write @objc dynamic. So this is effectively the same situation as before, just made explicit.

That’s really all there is to it. But where’s the fun in that? How does it work under the hood? Can we start to unbox @objc and dynamic? 📦

Three Ways to Decorate

Here’s what I started with to test three ways to decorate a function:

class ToObjcOrNotObjc {
  func performOperation() -> Int {
    return 42
  }

  @objc func performOcOperation() -> Int {
    return 42
  }

  @objc dynamic func performDynamicOperation() -> Int {
    return 42
  }
}
  1. performOperation is the plain undecorated Swift way to do things.
  2. performOcOperation has @objc, which means it should be visible to the Objective-C runtime. The compiler does some extra name transformation when it sees the string “Objc” in a name so I used “Oc” instead.
  3. performDynamicOperation opts into dynamic dispatch. The method will also be visible from Objective-C.

What’s next, compile it down to machine code and run a binary diff? 😓

Under the Hood

“When in doubt, examine the machine code.”
— me

All right, we don’t have to go all the way down to machine code. But let’s go one level down to my favorite part of the Swift compiler layer cake: SIL.

compiler layer cake
Not sure if these labels should be reversed and assembly is the strawberry on top?

Note: If you want all the code in a gist and instructions on how to compile it, there’s a postscript at the end with all the details.

Function Implementations

The first thing to see is that the SIL code for all three functions is exactly the same. Maybe that’s not such a big surprise — each function just returns 42 and the only difference is in how the function might be called.

I’ve de-mangled some of the names to help readability:

sil hidden @performOperation : $@convention(method)  
(@guaranteed ToObjcOrNotObjc) -> Int {
  debug_value %0 : $ToObjcOrNotObjc, let, name "self", argno 1
  // function_ref Int.init(_builtinIntegerLiteral:)
  %2 = function_ref @_i2048_builtinIntegerLiteral : $@convention(method)  
   (Builtin.Int2048, @thin Int.Type) -> Int
  %3 = metatype $@thin Int.Type
  %4 = integer_literal $Builtin.Int2048, 42
  %5 = apply %2(%4, %3) : $@convention(method)  
   (Builtin.Int2048, @thin Int.Type) -> Int
  return %5 : $Int
} // end sil function 'performOperation'

Let’s focus on a few lines to warm up our SIL reading skills:

  • %2 — as the comment above this line suggests, this is the Int initializer that takes an integer literal.
  • %5 — apply %2 (that’s Int.init) with parameters %4 and %3, which are the literal 42 and the Int type.
  • return the newly created Int.

That seems like a lot of work to return 42 but there it is, in full SIL glory.

Again, performOcOperation and performDynamicOperation have the exact same implementation. So what’s different about them? 🔎

Different Ways to Dispatch

If you have an instance and want to call an instance method, how would you do it?

Calling a plain old function seems like a simple matter: find its address and jump to it. Instance methods in Swift can be just as simple. Similar to C++, Swift keeps a virtual call table or vtable for each named type.

At the end of the generated SIL, you’ll find a vtable for the ToObjcOrNotObjc class:

sil_vtable ToObjcOrNotObjc {
  #ToObjcOrNotObjc.performOcOperation!1: (ToObjcOrNotObjc) -> () -> Int : _ToObjcOrNot_performOcOperationSiyF
  #ToObjcOrNotObjc.performOperation!1: (ToObjcOrNotObjc) -> () -> Int : _ToObjcOrNot_performOperationSiyF
  #ToObjcOrNotObjc.init!initializer.1: (ToObjcOrNotObjc.Type) -> () -> ToObjcOrNotObjc : _ToObjcOrNotC0CACycfc
  #ToObjcOrNotObjc.deinit!deallocator: _ToObjcOrNotC0CfD    // ToObjcOrNotObjc.__deallocating_deinit
}

You’ll see four instance methods here in the vtable:

  • performOcOperation
  • performOperation
  • init
  • deinit

That gives us a hint on how those methods will be called. But notice performDynamicOperation is nowhere to be seen! 🔬

Standard Dispatch

We’ve looked at the method implementations and the vtable. How do things look at the call site?

Consider this bit of Swift:

let q = ToObjcOrNotObjc()

q.performOperation()
q.performOcOperation()
q.performDynamicOperation()

If we compile that down to SIL, we can see side-by-side what the three calls look like:

// call to performOperation()
%5 = class_method %4 : $ToObjcOrNotObjc, #ToObjcOrNotObjc.performOperation!1 :  
     (ToObjcOrNotObjc) -> () -> Int,  
     $@convention(method) (@guaranteed ToObjcOrNotObjc) -> Int
%6 = apply %5(%4) : $@convention(method) (@guaranteed ToObjcOrNotObjc) -> Int

// call to performOcOperation()
%9 = class_method %8 : $ToObjcOrNotObjc, #ToObjcOrNotObjc.performOcOperation!1 :  
     (ToObjcOrNotObjc) -> () -> Int,  
     $@convention(method) (@guaranteed ToObjcOrNotObjc) -> Int
%10 = apply %9(%8) : $@convention(method) (@guaranteed ToObjcOrNotObjc) -> Int

There’s a similar pattern here: the first line gets a reference to the functions (%5 and %9), then the following line uses apply to make the call.

Notice the symbols like #ToObjcOrNotObjc.performOperation!1 match up with what you saw in the vtable earlier. Also, the calling convention is standard Swift $@convention(method).

Dynamic Dispatch

Next, what does the call to performDynamicOperation look like?

// call to performDynamicOperation()
%13 = class_method [volatile] %12 :  
      $ToObjcOrNotObjc, #ToObjcOrNotObjc.performDynamicOperation!1.foreign :  
      (ToObjcOrNotObjc) -> () -> Int,  
      $@convention(objc_method) (ToObjcOrNotObjc) -> Int
%14 = apply %13(%12) : $@convention(objc_method) (ToObjcOrNotObjc) -> Int

Notice the addition of .foreign to the method lookup as we’re now outside the native Swift world. The calling convention has also changed from plain old “method” to $@convention(objc_method).

In short:

  • dynamic methods don’t appear in the vtable.
  • At the call site, things go through the foreign-to-Swift Objective-C system.

Objective-C Visibility

At the SIL declaration level, all our functions performOperation, performOcOperation, and performDynamicOperation were declared with $@convention(method).

That’s OK for performOperation which is only callable from Swift. We just saw how performDynamicOperation goes through a foreign call system to get dynamic dispatch from Swift.

But performOcOperation and performDynamicOperation are also callable from Objective-C. How does that bridge over?

Thunks

The magic bit of glue here is a thunk. In the Swift to Objective-C world, this is an additional method callable from Objective-C. It’s a thin wrapper and all it needs to do is call through to the native Swift method.

Here’s what the thunk for performOcOperation looks like; I left in a tiny bit of mangled name so you can tell the functions apart — performOcOperationSiyF is the regular function and performOcOperationSiyFTo has the extra “To” at the end meaning @objc:

// @objc ToObjcOrNotObjc.performOcOperation()
sil hidden [thunk] @_ToObjcOrNot_performOcOperationSiyFTo :  
 $@convention(objc_method) (ToObjcOrNotObjc) -> Int {
bb0(%0 : $ToObjcOrNotObjc):
  %1 = copy_value %0 : $ToObjcOrNotObjc
  %2 = begin_borrow %1 : $ToObjcOrNotObjc
  // function_ref ToObjcOrNotObjc.performOcOperation()
  %3 = function_ref @_ToObjcOrNot_performOcOperationSiyF :  
       $@convention(method) (@guaranteed ToObjcOrNotObjc) -> Int
  %4 = apply %3(%2) : $@convention(method) (@guaranteed ToObjcOrNotObjc) -> Int
  end_borrow %2 from %1 : $ToObjcOrNotObjc, $ToObjcOrNotObjc
  destroy_value %1 : $ToObjcOrNotObjc
  return %4 : $Int
} // end sil function '_ToObjcOrNot_performOcOperationSiyFTo'

Some highlights:

  • Notice the [thunk] and $@convention(objc_method) on the first few lines.
  • %3 — grabs a reference to the native Swift method.
  • %4 — calls the native Swift method.
  • Final return statement passes back the result.

If you migrated code from Swift 3 to Swift 4, you probably had to add @objc to several methods since that’s no longer inferred. One benefit to the new system mentioned in SE-0160 is smaller binaries and faster load time since there should be fewer thunks in existence.

Although this thunk is simple, you can imagine more complex ones when the function has arguments that need to be set up — converting Swift strings to NSString instances, etc.

Timing

As a final point of curiosity, I was thinking about performance. Is dynamic dispatch slower? If so, by how much?

I ran a hundred million iterations of calling each of performOperation, performOcOperation, and performDynamicOperation to see what kind of difference there is, and here’s what I saw on my 12" Macbook:

Time for 100000000x performOperation: 0.5983s
Time for 100000000x performOcOperation: 0.5911s
Time for 100000000x performDynamicOperation: 3.3750s

@objc doesn’t have much effect, but dynamic definitely has a cost!

You’ll see different results with compiler optimizations turned on and when you do more work than just return 42; as always your mileage may vary, so take these numbers as trivia and a starting point.

The Closing Brace

Here’s the least you need to remember:

  • @objc makes things visible to Objective-C code. You might need this for setting up target/action on buttons and gesture recognizers.
  • dynamic opts into dynamic dispatch. You might need this for KVO support or if you‘re doing method swizzling.
  • The only way to do dynamic dispatch currently is through the Objective-C runtime, so you must add @objc if you use dynamic.
swift native, objc, and objc dynamic

As for thunks and vtables and SIL: they’re lurking one level down the Swift compiler layer cake, and I always enjoy knowing a bit more about what’s going on there. I’m by no means an expert but I’ll keep digging where I can. ⛏

}

Postscript

You can have a look at the full Swift code listing and SIL output as a gist.

When emitting SIL, the compiler uses the filename as the module name. I set the module name to a random word (“orchard” in this case) so it’s easy to spot and ignore.

Here’s the command I used to generate SIL:

$ swiftc objc-dispatch-demo.swift -emit-silgen -module-name orchard