Why does casting type metadata to `AnyObject` later result in `destroy_value` being called on the `AnyObject`?

(This post may seem a little abstract and academic but I assure you it comes from a real-world problem!)

Given the following code:

class A {
}

func b() {
  _ = A.self as AnyObject
}

The compiler generates the following raw SIL code for the function b:

%0 = metatype $@thick A.Type                    // user: %1
%1 = thick_to_objc_metatype %0 : $@thick A.Type to $@objc_metatype A.Type // user: %2
%2 = objc_metatype_to_object %1 : $@objc_metatype A.Type to $AnyObject // user: %3
destroy_value %2 : $AnyObject                   // id: %3
%4 = tuple ()                                   // user: %5
return %4 : $()                                 // id: %5

Why is there a destroy_value at the end of the function? As far as I can see, there does not exist a balancing “retain”?

In Objective-C, metatypes are immortal so retain/release has no effect. However objc_metatype_to_object has to return a +1 value (transfers ownership to the caller) which must be destroyed because at this point it’s just an AnyObject as far as the compiler is concerned. We could eliminate the destroy with a simple analysis in this case but I doubt this pattern comes up in enough hot paths to be worth it.

Thanks for the insight, @Slava_Pestov!

In my application, the value being casted to AnyObject is an Objective-C NSProxy object that acts like an Objective-C Class object. (This is for the EarlGrey UI testing framework, which proxies method calls between the test and app processes.) The proxy object is returned to the Swift code as AnyClass and then I am attempting to cast it to AnyObject.

The reason I wrote this post is because I saw that the proxy object was being overreleased. It seems objc_metatype_to_object does not generate a corresponding “retain”, so the “release” that the compiler generates is unbalanced.

Is that a bug? I’ve been trying to find the objc_metatype_to_object code but I don’t know where it is.

Class objects are generally assumed to be immortal by Swift (and Objective-C ARC), so hiding a proxy object that isn't immortal behind a Class-typed value isn't a good idea. If it doesn't make sense for the proxy to be immortal, you should probably pass it as an id instead. Otherwise, if the proxy is effectively immortal, you should implement -retain and -release on the proxy as no-ops too.

1 Like

Changing the type in the Objective-C code sounds like a good idea. I will do that to avoid this issue. Thanks!

Still, why does the compiler generate a “release” for this when there isn’t a corresponding “retain”?

  1. An AnyObject value is always considered retained by default, so the compiler has to release it.
  2. When you convert a Class to AnyObject, the compiler could retain it, but it chooses not to because it knows Classes are immortal, and so it can save on code size and run time by not doing it.

These two pieces of local reasoning result in the issue you ran into. Doing something smarter for (1) is impossible if the release is sufficiently far away from the creation of the AnyObject value (maybe you assigned a Class to a global variable, and later assigned something else). So the only 100% correct option would be to be more conservative in (2), benefiting this particular uncommon use case at the cost of unnecessary operations for everyone else.

2 Likes

Because we’re releasing the result of the instruction which is just an AnyObject. It doesn’t “know” it’s actually a class. It could elide it in this example by observing the value was defined by that particular SIL instruction, but in general that’s impossible to determine because AnyObject has a dynamic type by definition.

As Joe writes, your code is violating an implicit assumption that retain and release on classes is a no op.

1 Like

Thanks, @Slava_Pestov @Joe_Groff @jrose!