Why @objc deinit uses ObjC messages to call super deinit?

Consider the following example:

import Foundation

func qux() { print("Q_U_X") }
func zab() { print("Z_A_B") }

class Foo: NSObject {
  deinit {
    qux()    
  }
}

class Bar: Foo {
  deinit {
    zab()
  }
}

Code generated for Bar.deinit() uses objc_msgSendSuper2 to call [super dealloc]. Call finds $s3foo3FooCfDTo (aka @objc foo.Foo.__deallocating_deinit), which has an extra selector argument. It ignores that argument and then calls $s3foo3FooCfD (aka foo.Foo.__deallocating_deinit).

Why not just bypass ObjC machinery altogether and call $s3foo3FooCfD directly?

For async deinit, FooCfD is a thunk that creates task and runs asynchronous FooCfZ (aka foo.Foo.__isolated_deallocating_deinit) in that task.

So far, I've ended up with implementation where asynchronous $s3foo3BarCfZ (already running inside a task) calls [super dealloc] using objc_msgSendSuper2. This calls $s3foo3FooCfDTo, which in turn calls $s3foo3FooCfD, which creates a new task and runs $s3foo3FooCfZ there.

That sounds very inefficient. I want to call async $s3foo3FooCfZ directly from async $s3foo3BarCfZ, which is not possible (or at least hacky) using ObjC machinery.

Are there any reasons why Swift deinit need to use ObjC machinery if base class is known to be a Swift class?

Not an expert in this area but since Foo is inheriting from NSObject I think it's still valid to, say, dynamically swap out the implementation for -[Foo dealloc] at runtime keyed by selector and you'd expect the substituted implementation to be called even if dealloc is invoked by a Swift subclass.

4 Likes