How Swift Playgrounds Work, Part II

In my last post I explained how I became interested in how Swift playgrounds were implemented, especially playgrounds with live views.

It’s this case that I needed to investigate more, because it was less obvious how they work than a simpler playground that logs values. My first thought was that, like the simple case, the playgrounds were being compiled and run on a simulator. In this case they’d need to be compiled into an app bundle though, so that the screen could show the live view. The first thing I tried was to put print(Bundle.main.executablePath) in a playground. That printed:

NSBundle </Users/brandon/Library/Developer/XCPGDevices/38BBF06C-3549-4DA2-BD08-EB862F7186A1/data/Containers/Bundle/Application/9E08E9F6-D0A7-4E83-9936-C71A0DD04B17/asdfasfasf-25250-2.app> (loaded)
// The name of the playground was asdfasfasf.playground 😬

Interesting! There’s a few things this indicates:

  1. Yep, looking at the last path component, it’s an app bundle.
  2. What is /Users/brandon/Library/Developer/XCPGDevices/?

Let’s start with the app bundle, and look inside. The contents should look familiar if you’ve done some iOS development before.

In short, this is a code-signed app bundle with very little inside it. There’s the executable, an Info.plist and a Pkginfo. I didn’t know what the purpose of that last file was, and the docs aren’t very helpful, so if you have some more details about this I’d love to know. This looks like it could be the bare minimum for an iOS app.

That means that there’s not a lot of clues here, at least to the naked eye. My next thought was to look at the executable, but that’s not something I’d ever done before. Thankfully in the course of making iOS and Mac apps I’ve heard about tools that can help with this. Two notable ones are class-dump and Hopper. class-dump prints Objective-C runtime information in a header-like format. This is a portion of what class-dump prints when I run it with the path to the executable I showed above:

__attribute__((visibility("hidden")))
@interface XCPAppDelegate : UIResponder <UIApplicationDelegate>
{
    _Bool _needsIndefiniteExecution;
    XCPLiveViewManager *_liveViewManager;
}

- (void).cxx_destruct;
- (void)finishExecutionNotification:(id)arg1;
- (void)liveViewDidChangeNotification:(id)arg1;
- (void)needsIndefiniteExecutionChangedNotification:(id)arg1;
- (void)unregisterNotifications;
- (void)registerForPlaygroundSupportNotifications;
- (void)registerForXCPlaygroundNotifications;
- (void)finishExecution;
- (void)enqueueRunLoopBlock;
- (_Bool)application:(id)arg1 willFinishLaunchingWithOptions:(id)arg2;

// Remaining properties
@property(readonly, copy) NSString *debugDescription;
@property(readonly, copy) NSString *description;
@property(readonly) unsigned long long hash;
@property(readonly) Class superclass;
@property(retain, nonatomic) UIWindow *window;

@end

class-dump won’t turn a program back into source code, but it can be really useful to get an overview of the different pieces that make it up.

Hopper is a decompiler and disassembler that has the neat feature of generating Objective-C pseudo-code that is almost like reading the original source. This is what it looks like when I use it to look at that same executable:

Here I was able to find the application:willFinishLaunchingWithOptions: app delegate method, which would be the “entry point” to the app, and from there Hopper lets you double click to navigate to other methods that are being invoked.

Between the two tools I could see that, just like the bundle, there’s not a lot inside the executable itself. There’s an XCPAppDelegate and XCPLiveViewManager, and a lot of the methods look like what we’d expect to see in an app that was used to run a playground in a simulator.

But, the important part of a playground is the code that was entered in Xcode, so where’s that? One method that stood out in Hopper was _executePlayground. Strangely, Hopper displayed it as a no-op. Turning off some of Hopper’s pseudo-code clean-up features didn’t reveal much more in the form of assembly code.

void _executePlayground() {
    return;
}
// 🤔

How is it that the one method that should do the most important part looks like it doesn’t do anything? In fact, searching in Hopper showed that none of my playground code was showing up in this binary. Even going out of the way to add static strings that couldn’t be optimized away during compilation wouldn’t put them in the app. But they’d be there at runtime!

It was at this point that I spent more time going down a lot of different paths trying to find out how this could be. I knew of different techniques that could apply here, like “injecting code” into the running app (whatever that means in practice), or some kind of IPC between Xcode and the simulator, but nothing seemed like an answer. It was a bit of chance that I ended up finding the place in Xcode that makes the magic happen, but I don’t want to reveal it just yet. Now that I know the answer, it seems both clever and obvious in hindsight, so I think it’s worth considering:

Knowing that there’s this “empty” app running in a simulator, how would you get playground code to execute?