Crash when trying to reload plugin in runtime with Bundle

I've been playing with runtime plugins reloading using Bundles in Swift. And I've encountered internal crash in conformsToProtocol call while using decoders (JsonDecoder, PlistDecoder etc). This only happens when call PlistDecoder.decode then reload plugin using Bundle.unload, cleanup bundle, load with Bundle.load and call decode in new plugin instance again.

As I understand this related to the Swift and Objective-C runtime and the way Bundles/Frameworks are loaded. This doesn't happen if I use NSDictionary to parse plist or json because it doesn't use any runtime magic.

Does Swift supports this kind of reloading and if not why is that even possible in this case? Looks more like a bug to me. Also is there a way to reload plugins in runtime without restarting an application?

Here's code snippets from my test application Plugin Interface:

@objc(PluginInterface) protocol PluginInterface {
    init()
    func printVersion()
}

Plugin:

struct InfoPlist : Codable {
    let CFBundleShortVersionString: String
}

class Plugin : PluginInterface {
    required init() {}

    func printVersion() {
        print("Current version: \(getVersionFromPlistFile() ?? "nil")")
    }

    private func getVersionFromPlistFile() -> String? {
        let infoPlistPath = Bundle.main.bundlePath + "/Contents/Info.plist"
        guard let data = try? Data(contentsOf: URL(fileURLWithPath: infoPlistPath)) else {
            return nil
        }

        let plistData = try?  PropertyListDecoder().decode(InfoPlist.self, from: data)
        return plistData?.CFBundleShortVersionString
    }

}

Plugin loader:

class PluginLoader {
    private var pluginBundle: Bundle?

    deinit {
        unloadDynamicBundle()
    }

    func load(bundleUrl: URL) -> PluginInterface? {
        return loadDynamicBundle(bundleUrl: bundleUrl)
    }

    func unload() {
        unloadDynamicBundle()
    }

    private func loadDynamicBundle(bundleUrl: URL) -> PluginInterface? {
        guard let pluginBundle = Bundle(url: bundleUrl) else {
            return nil
        }

        self.pluginBundle = pluginBundle

        do {
            try pluginBundle.loadAndReturnError()
        } catch {
            print("Loading error: \(error.localizedDescription)")
            return nil
        }

        let typeNamed: AnyClass? = pluginBundle.classNamed("Plugin.Plugin")
        guard let plugin = initPlugin(from: typeNamed as? PluginInterface.Type) else {
            return nil
        }

        return plugin
    }

    private func unloadDynamicBundle() {
        guard let pluginBundle = pluginBundle else {
            return
        }

        if !pluginBundle.unload() {
            return
        }

        self.pluginBundle = nil
    }

    private func initPlugin(from type: PluginInterface.Type?) -> PluginInterface? {
        if let cls = type {
            let plugin = cls.init()
            return plugin
        }

        return nil
    }
}

Here's crash stack trace:

Exception Type:        EXC_BAD_ACCESS (SIGSEGV)
Exception Codes:       KERN_INVALID_ADDRESS at 0x000000008dc005f0
Exception Codes:       0x0000000000000001, 0x000000008dc005f0
Exception Note:        EXC_CORPSE_NOTIFY

Termination Reason:    Namespace SIGNAL, Code 11 Segmentation fault: 11

Thread 1 Crashed::  Dispatch queue: com.apple.root.default-qos
0   libswiftCore.dylib            	    0x7ff8239aa710 swift_conformsToProtocolMaybeInstantiateSuperclasses(swift::TargetMetadata<swift::InProcess> const*, swift::TargetProtocolDescriptor<swift::InProcess> const*, bool) + 2464
1   libswiftCore.dylib            	    0x7ff8239a9b4e swift_conformsToProtocol + 78
2   libswiftCore.dylib            	    0x7ff823968fed swift::_conformsToProtocol(swift::OpaqueValue const*, swift::TargetMetadata<swift::InProcess> const*, swift::TargetProtocolDescriptorRef<swift::InProcess>, swift::TargetWitnessTable<swift::InProcess> const**) + 45
3   libswiftCore.dylib            	    0x7ff8239a91a5 swift::_checkGenericRequirements(__swift::__runtime::llvm::ArrayRef<swift::TargetGenericRequirementDescriptor<swift::InProcess> >, __swift::__runtime::llvm::SmallVectorImpl<void const*>&, std::__1::function<swift::TargetMetadata<swift::InProcess> const* (unsigned int, unsigned int)>, std::__1::function<swift::TargetWitnessTable<swift::InProcess> const* (swift::TargetMetadata<swift::InProcess> const*, unsigned int)>) + 1813
4   libswiftCore.dylib            	    0x7ff8239a3e19 _gatherGenericParameters(swift::TargetContextDescriptor<swift::InProcess> const*, __swift::__runtime::llvm::ArrayRef<swift::TargetMetadata<swift::InProcess> const*>, swift::TargetMetadata<swift::InProcess> const*, __swift::__runtime::llvm::SmallVectorImpl<unsigned int>&, __swift::__runtime::llvm::SmallVectorImpl<void const*>&, swift::Demangle::__runtime::Demangler&) + 1033
5   libswiftCore.dylib            	    0x7ff8239a2b03 (anonymous namespace)::DecodedMetadataBuilder::createBoundGenericType(swift::TargetContextDescriptor<swift::InProcess> const*, __swift::__runtime::llvm::ArrayRef<swift::TargetMetadata<swift::InProcess> const*>, swift::TargetMetadata<swift::InProcess> const*) const + 131
6   libswiftCore.dylib            	    0x7ff8239a119b swift::Demangle::__runtime::TypeDecoder<(anonymous namespace)::DecodedMetadataBuilder>::decodeMangledType(swift::Demangle::__runtime::Node*, unsigned int, bool) + 21499
7   libswiftCore.dylib            	    0x7ff82399b29d swift_getTypeByMangledNodeImpl(swift::MetadataRequest, swift::Demangle::__runtime::Demangler&, swift::Demangle::__runtime::Node*, void const* const*, std::__1::function<swift::TargetMetadata<swift::InProcess> const* (unsigned int, unsigned int)>, std::__1::function<swift::TargetWitnessTable<swift::InProcess> const* (swift::TargetMetadata<swift::InProcess> const*, unsigned int)>) + 493
8   libswiftCore.dylib            	    0x7ff82399b06d swift_getTypeByMangledNode + 477
9   libswiftCore.dylib            	    0x7ff82399b78a swift_getTypeByMangledNameImpl(swift::MetadataRequest, __swift::__runtime::llvm::StringRef, void const* const*, std::__1::function<swift::TargetMetadata<swift::InProcess> const* (unsigned int, unsigned int)>, std::__1::function<swift::TargetWitnessTable<swift::InProcess> const* (swift::TargetMetadata<swift::InProcess> const*, unsigned int)>) + 1002
10  libswiftCore.dylib            	    0x7ff823998cbd swift_getTypeByMangledName + 477
11  libswiftCore.dylib            	    0x7ff823998eeb swift_getTypeByMangledNameInContext + 171
12  Plugin                        	       0x10bc74cf9 __swift_instantiateConcreteTypeFromMangledName + 89
13  Plugin                        	       0x10bc75033 protocol witness for Decodable.init(from:) in conformance InfoPlist + 19
14  libswiftCore.dylib            	    0x7ff82393a5e7 dispatch thunk of Decodable.init(from:) + 7
15  libswiftFoundation.dylib      	    0x7ff827a61cd8 __PlistDecoder.unbox<A>(_:as:) + 328
Answered by DTS Engineer in 725128022

Does Swift supports this kind of reloading … ?

No. And neither does Objective-C for that matter. I’m not super familiar with the Swift side of this but I don’t think that matters in this case because your plug-in mechanism is based on the Objective-C runtime.

You can’t safely unload Objective-C code. The problem is that, when the Objective-C code loads, it registers with the Objective-C runtime and, due to the architecture of that runtime, it’s not possible to unregister.

IIRC the dynamic linker actually prevents such an unload. So the Bunlde.unload() method works but the underlying Mach-O image is not removed from memory. If you then load another copy… well… things aren’t going to end well.

If you want to support something like this, you need to load the bundle in a separate process, so that you can clean up by killing the entire process.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Accepted Answer

Does Swift supports this kind of reloading … ?

No. And neither does Objective-C for that matter. I’m not super familiar with the Swift side of this but I don’t think that matters in this case because your plug-in mechanism is based on the Objective-C runtime.

You can’t safely unload Objective-C code. The problem is that, when the Objective-C code loads, it registers with the Objective-C runtime and, due to the architecture of that runtime, it’s not possible to unregister.

IIRC the dynamic linker actually prevents such an unload. So the Bunlde.unload() method works but the underlying Mach-O image is not removed from memory. If you then load another copy… well… things aren’t going to end well.

If you want to support something like this, you need to load the bundle in a separate process, so that you can clean up by killing the entire process.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Crash when trying to reload plugin in runtime with Bundle
 
 
Q