Performance of installGetClassHook_untrusted() at startup

I see that installGetClassHook_untrusted is run as an initializer and that this function can take 100ms, largely due to dladdr. At other times, it seems to finish instantly and my symbolic breakpoint for it stops working (but it's still listed in DYLD_PRINT_INITIALIZERS as being called). Also, when I set the minimum app version from 12 to 13, it no longer gets called. So, I have a few questions:

  • How can I prevent it from being run, while still leaving the minimum iOS version at 12.0? It seems to be a Swift 5.0 compatibility thing, does that mean I need to remove all code built for Swift 5.0?
  • Why might I see it being called in DYLD_PRINT_INITIALIZERS but have it not take any time at all, nor trigger symbolic breakpoints?

Any answers, guesses, or just additional context would be greatly appreciated.

The Objective-C runtime in iOS 12 and earlier did not have correct support for instantiating Swift classes, so that initializer in the backward compatibility library replaces the ObjC runtime hook for instantiating Swift classes with a correct implementation. This is necessary for code generated with new Swift compilers to run properly on older OSes. You can see the implementation here:

The dladdr check ensures that the constructor is actually running on behalf of the main executable, and not a dylib, because some build systems improperly link the libswiftCompatibility* libraries into dylibs, and we don't want an endless chain of hooks piled on each other. If you are certain that none of the dylibs your executable links against includes this hook, then you could patch it out. Or, you could remove the constructor entirely, as long as you still install the fixed hook manually at some point before NSClassFromString or objc_getClass are ever called in the process.

To follow up, @Mike_Ash and other folks worked out a more efficient way to do the main executable check here; all we need to do is check the Mach header of the current image:

const mach_header* mh = (const mach_header*)__dso_handle;
if (mh->filetype != MH_EXECUTE) {
  return;
} 

So we can modify the hook in the compatibility library to do that instead of the dladdr check.

1 Like

By "patch it out" do you mean interpose dladdr? And the solution of calling it manually is interesting. Could I replace the init call (or interpose installGetClassHook_untrusted) with:

if (OS < 13.0) {
    run initializer
}

By "patch it out", I mean that the libswiftCompatibility50.a library is part of the developer toolchain, not the OS, and it gets statically linked into executables that need to back deploy to older OSes, so you can conceivably replace it with a modified version that meets your needs, as long the behavior modifications it installs still get installed early enough. If you were going to use a modified libswiftCompatibility library that didn't include the static constructor, then yeah, you should be able to run the initializer code first thing in main before any Swift-ObjC interop calls happen.

I see, but would it be OK to gate by OS <13 as I show in the above example? My thought is to use interposing, and from the new version of the function only call the original version if OS <13

Yeah, it only needs to be installed on iOS 12 or earlier. We should probably change the library upstream to check the OS version too.

1 Like

Doesn't look like this change ever got made, did it end up not working out or just forgotten about?

I haven't heard anything about it not working; the change probably just hasn't been made yet. I filed rdar://76538727 to make sure our team at Apple takes a look at this again (if nobody beats us to it).

Here is a PR with the change: Compatibility50: Use __dso_handle for cheaper main executable check. by jckarter · Pull Request #36872 · apple/swift · GitHub

1 Like

Just to be clear, would this mean that all apps compiled with the SDK release including the new implementation would see improved startup times on older OSes?

(Or maybe even that the AppStore’s bitcode-recompiling feature could do it for all apps, even those which don’t get rebuilt?)

Yes, the compatibility libraries are static libraries that are linked into your main executable, so compiling with a newer toolchain will pick up the new implementation. However, that linking happens at the bitcode level, so the App Store recompiling wouldn't really be able to replace the implementation without you resubmitting your app.

1 Like