NSExtension & PlugInKit — A brief intro.
Recently I’ve been doing a lot of reverse engineering in the PrivateFrameworks department, simply out of curiosity. One thing that really always bothered me was how to insert a view hierarchy across processes — and I was interested in seeing how Apple had done it. A few frameworks of note: ViewBridge.framework, PlugInKit.framework, LaunchServices.framework, and finally, libextension.dylib. The last one is a bit weird because it’s actually a high level Objective-C framework that’s tied into AppKit — but for some reason has been relegated to /usr/lib (but is now located in Foundation.framework in macOS Sierra). All of the frameworks above use a common transmission medium: the NSXPCConnection API, which internally wraps the libxpc.dylib library, found in /usr/lib/system.
With the introductions out of the way, I took a quick look into ViewBridge.framework, and realized it was the wrong place to start. I had to start off a little bit lower, down in PlugInKit.framework and libextension.dylib. In my understanding, the PlugInKit serves as generic plugin subsystem hub framework which an app can hook into with a custom subsystem and set of protocols of some sort and start using. The FxPlug API for writing Final Cut Pro plugins seems to be based atop PlugInKit, along with many others, but modern macOS (Yosemite and beyond) extensions use the newer NSExtension API, which is a subsystem to the former framework. In addition, ViewBridge framework is also a subsystem to the former framework by tying in NSViewControllers. An NSViewController can be discovered as an extension and then vend its view through the ViewBridge.framework into a host application.
PlugInKit and LaunchServices
PlugInKit’s life begins with plugin discovery — if you take a look at Console and search for “pkd”, you’ll see a lot of interesting things going on related to extension installation. This is really just LaunchServices’ private API with a little sprinkles on top. LSApplicationWorkspace seems to be NSWorkspace’s older and more mature cousin, able to query for applications anywhere on disk with certain properties (like settings bundles on iOS, audio components, VPN plugins, user activity handlers, and URL schemes). In addition, it also allows registering for notifications (LSApplicationWorkspaceObserver) when an application is installed (which I assume really means copied and then opened at least once). It allows un/installing applications, opening URLs, and most importantly grabbing plugins from applications. The LSBundleProxy and its subclasses provide detailed information (mostly through Info.plist hunting) about bundles, applications, resources, plugins, and more. In addition, there is a pluginkit tool that allows system-level control of plugins.
Essentially, when an application is installed, pkd picks it up and detects all sorts of metadata within it, including any plugins or extensions it may have. From there, things begin to diverge: PlugInKit relies on a conventional model of providing explicit protocol information and formal communication with a plugin; NSExtension, the new bees’ knees, relies on an informal “generic item” based communication model (as seen in NSExtensionRequestHandling for Safari Link extensions) and builds on top of PlugInKit. This allows the application to be really flexible with extension protocols.
The NSExtension Host
The NSExtension framework doesn’t have its own plugin discovery system; it uses PlugInKit for that. +[NSExtension beginMatchingExtensionsWithAttributes:completion:] internally calls through to -[[PKHost defaultHost] continuouslyDiscoverPlugInsForAttributes:flags:found:] with a discovery block. This block enumerates each PKPlugIn discovered and transforms them into NSExtensions by grabbing necessary properties, and holding a strong reference to them, as seen in -[NSExtension _initWithPKPlugin:]. When +[NSExtension endMatchingExtensions:] is invoked, it calls through to -[[PKHost defaultHost] cancelPlugInDiscovery:]. There’re a couple features I don’t fully understand here yet: -[NSExtension attemptOptIn:], -[NSExtension attemptOptOut:], and -[NSExtension optedIn]. All of these call into -[PKPlugIn userElection], of which I’m not sure of the purpose.
So once you have all these extensions, how do you begin using them? -[NSExtension beginExtensionRequestWithInputItems:completion:]. What this really does is prepare an internal call on the “GlobalStateQueue” to -[NSExtension _reallyBeginExtensionRequestWithInputItems:listenerEndpoint:completion:], I know, very imaginative name. In the process of this call as well as the matching one to end using the plugin, -[NSExtension _safelyBeginUsing:] and -[NSExtension _safelyEndUsing:] both call into -[PKPlugIn beginUsing:] and -[PKPlugIn endUsing:] to prepare anything needed.
Back to the main hero here: _reallyBeginExtensionRequest…: First, it sets up things where necessary (nil checks for extension contexts or service connection dictionaries), and then ensures to load “NSExtensionContextHostClass” if it exists in the extension’s Bundle’s Info.plist. It then invokes -[NSExtensionContext initWithInputItems:] with the inputItems passed in. It stores the extension data by the context’s UUID in the NSExtension’s dictionaries. After configuring the invalidation and interruption handlers on its NSXPCConnection, it invokes -resume and grabs the connection’s remoteObjectProxy. If it was able to obtain the object proxied (a _NSExtensionContextVendor object), it then sets it as the NSExtensionContext’s extensionVendorProxy (by invoking -[NSExtensionContext _setExtensionVendorProxy:]). If the listenerEndpoint passed in was nil, it assigns it as the value of -[NSExtensionContext _auxiliaryListener]’s endpoint.
And then we go down the rabbit hole of XPC by invoking -[_NSExtensionContextVendor _beginRequestWithExtensionItems:listenerEndpoint:withContextUUID:completion:]. It passes the context UUID to match with an endRequest call, so the NSExtension knows which NSExtensionContext was completed or invalidated. Of course, it also provides the listener endpoint, and the input items. If an error occurs at any stage, if the host cancels the request, or if the request completes successfully, everything is torn down by invoking __NSExtensionTearDownRequestWithIdentifier.
The Rabbit Hole that is PlugInKit
Let’s take a step back and take a look at things from the extension vendor’s point of view first. If you begin a new app extension project, you might notice the lack of a main.c or NSApplicationMain. That’s because Xcode performs some magic linker voodoo (flag -e) to set the executable EntryPoint to _NSExtensionMain. When the extension host begins a connection to the extension, launchd actually executes the extension, where this function begins. Ideally, if you’ve worked with XPC helpers, what happens is the application invokes -resume on the NSXPCListener, preferably after setting its delegate to to accept any incoming connections. NSExtensionMain calls through to +[PKService _defaultRun:arguments:] which then calls through to -[PKService run] on the +[PKService defaultService]. What follows is pretty interesting: it reveals that PlugInKit was originally designed to service multiple SDKs in the Mavericks era, possibly initially servicing FxPlug only — the PKService instance then sets the NSXPCListener’s delegate and calls an interesting method -[PKService discoverSubsystems]. What it does is enumerate over -[PKService configuredSubsystemsList] which currently checks to see if the bundle’s Info.plist has an “NSExtension” or “PlugInKit” array — within -[PKService mergeSubsystems:from:], it looks for the key “NSExtensionPointIdentifier”, and if it doesn’t exist, “SDK” (a Legacy term) and then invokes +[PKPlugInCore readSDKDictionary:] to retrieve the entire dictionary information.
Interestingly enough, there’s a function _xpc_copy_extension_sdk_entry that’s invoked here and there are references to looking for an “NSExtensionSDK” key somewhere. Upon taking a peek into libxpc.dylib, it’s easy to see that there are a couple provisional functions: _xpc_connection_is_extension, _launch_extension_check_in_live_4UIKit, and some more internal _launch_extension* functions. It’s a little unnerving to see that there’s so much framework bleed, to the point where it enters launchd, xpc, and two whole plugin oriented frameworks. Also, random sidenote: XPC has a number of painful functions to create an XPC object from a Plist file descriptor… which ultimately ends up parsing the binary format. :( Anyway, after a lot of processing of the SDK dictionaries, PKService checks for any Subsystems involved. One of interest being “NSViewService_PKSubsystem”, which pops up in -[PKService _processDefaultSubsystemName:]. Once the whole subsystem discovery ends, PlugInKit knows exactly what SDKs and subsystems it’ll be vending and I assume it knows their definitions for XPC communication as well. After this, it looks for a PKServiceDelegate (with the key “Delegate”) set in the Info.plist, and attaches it if it can. Once ALL of this is done, the NSXPCListener is resumed.
When a new connection attempts to connect, if it’s from the correct listener, it creates what is called an “PKServicePersonality” with that connection which configures the connection. It exports the PKCorePlugInProtocol protocol, and expects the remote interface to be a PKCoreHostProtocol (which is basically empty…). The idea is that a PKService may either have a number of personalities or a solePersonality (either the highest priority or the only personality). These objects encompass the plugin SDK and communications channel, and the service keys them according to the host processIdentifier (pid) of the NSXPCConnection. To register or unregister a PKServicePersonality, -[PKService registerPersonality:] and -[PKService unregisterPersonality:] both enqueue operations on the internal _sync DispatchQueue. PKService is also a little bit nifty in that it can be scheduled to terminate the XPC transaction using an internal dispatch_source_timer terminationTimer, which can also be cancelled later. PKService has a number of other useful functions too, such as getting a personality’s connection, defaults, pluginPrincipal, hostPrincipal, and embeddedPrincipal; all of these functions call-through to the PKServicePersonality with the appropriate name. In addition, it can also launch the containing application, if on the host’s side of things. Both PKServicePersonality and PKHostPlugIn are subclasses of PKPlugInCore, where most of the heavy lifting seems to happen.
When PKServicePersonality receives a -[PKCorePlugInProtocol prepareUsing:reply:] call, it caches the identifier, hostProtocol, version, and uuid. Of particular importance is the hostProtocol — once the personality invokes -[PKServicePersonality setupWithIdentifier:], it tries to find the protocol if there is one using _pkFindProtocol(). Within that setup method, the personality first invokes resolveSDK, then registerPersonality with the PKService’s defaultService. In addition, it prepares a PKServiceDefaults for itself and calls -[PKServicePersonality checkEnvironment:]. There’s another “prepareUsing” call, -[PKServicePersonality prepareUsingPlugIn:hostProtocol:reply:], but that appears to just call into the former method, and is designated as legacy. This is where things get interesting: the personality enumerates over all subsystems and invokes -[PKModularService beginUsing:withBundle:]. This essentially implies that all subsystems conform to PKModularService… and this checks out, as both NSExtension and NSViewService_PKSubsystem and do. When the PKServicePersonality receives a -[PKCorePlugInProtocol shutdownPlugIn] call, it basically does the opposite: it unregisters itself, invokes -[PKModularService endUsing:] on all of the service’s subsystems, and finally calls -[PKService scheduleTermination:] on the service. Side note: PKCorePlugInProtocol has two preferences methods which simply call down to the PKServiceDefaults for the personality.
Well that was fun. — No one ever.
Where are we really going with this?
Ultimately, the paragraphs above were just an interactive disassembly of what the system is really doing with extensions. We’ve learned a bit about how tightly integrated they are with the system, to the point where launchd and XPC are getting slightly involved (does no one know the meaning of separation of tasks?) and how PlugInKit uses LaunchServices through pkd to auto-magically register and unregister plugin bundles. From there, we went through the bootstrap procedure of PlugInKit on the vendor (plugin) side, and how PlugInKit is designed around modular services called subsystems. In fact, grab a copy of the system’s runtime headers and grep for PKModularService — there’s plenty of them. We also covered how NSExtension requests can be started and how they snake down the PlugInKit lifeline to reach the vendor. There’s so much more. In fact, this is quite literally the tip of the iceberg; I know there could be facts I may have gotten wrong about this whole shpeel, but it was a fun trip.
A fun side project would be to use the extension framework to allow your app to interface with plugins, or even create your own PlugInKit modular subsystem (which actually doesn’t seem to be that much of a challenge)! Or perhaps, use the ideas here on a smaller scale (NSWorkspace instead of LaunchServices, perhaps?) to create your very own PlugInKit! In reality, a clone might just be Info.plist checking and some XPC magic.