mercredi 26 février 2020

Mysterious EXC_BAD_ACCESS error on a strongly referenced variable

TL;DR

I have a class with no public initializers or instances that passes an instance of itself to a closure in another class. It does this through a mirror of the other class. When I go to access that instance from within the closure, I'm getting a EXC_BAD_ACCESS error, but other things passed to the function are clearly accessible and do not result in a bad access error. I have no idea why.

Detailed Explanation

I've been trying to figure out a way to implement class-specific access control, where multiple specific classes have sole access to another class containing variables and functions to be shared between them. All other classes would not have such access. Kind of like a static class, or a Singleton pattern, but with specific, class-named access control.

I thought I had something that would actually work in pure swift, (which is nice for me since I don't know Objective-C, and only started on swift about 16 months ago.) It's done in an almost anti-swift manner, so just bear with me - my goal is to start with something functional and move it towards elegance and beauty from there.

Even though I'm reasonably confident it should all work, I'm encountering a EXC_BAD_ACCESS error in a very unexpected place.

The "class-specific private" class that you are not allowed to access an instance of unless you are on its "okay" list, we can call the Restricted class.

The class(es) that is(are) allowed access to the Restricted class we can call the Accessor class(es).

The programmer must tell the Restricted class to call a function from the Accessor, and "drop in" an instance of the Restricted class by passing it as a parameter to that function. You do this by passing in the name of the function to be called, an instance of the Accessor class on which to call said function, and any parameters that the function would need in addition to the Restricted class instance.

I could make an enormous switch in the Restricted class, each case of which properly calls each function indicated on each of the Accessor classes...but to get around that excessive overhead/setup, I have the name of the function to be called on the Accessor classes passed in as a string, and accessed through a mirror. Since mirrors only reflect properties and not functions, the function must be a property with an assigned closure, instead of a traditional function.

We can call these closures DropClosures, since their purpose is to have the shared, Restricted class dropped into them. In fact we could call this whole pattern the "DropClosure Pattern". (Or maybe anti-pattern, I know it's kind of gruesome as-is.)

The properties of the "shared" instance of the Restricted class are stored internally as a private static dict (as json, basically). To generate an actual instance of itself, the Restricted class uses a private initializer that accepts that dict as a parameter. After a DropClosure runs with said initialized instance, the Restricted class uses a Mirror of that instance to store any changes back in the "shared" dict, and the instance will go out of scope unless a reference is made to it. So after each DropClosure completes its run, the instance passed to it is more or less useless as a representation of the "shared" aspect of the class, intentionally so.

I only do this because there is no way to require that all references to a certain weak reference also be weak. I don't want a class with access to the weak reference to assign a strong reference to the same instance and keep it in memory, that would defeat the access control goal by allowing the instance to be shared outside of its access scope. Since I can't force the instance to expire once the closure has completed, the next best thing is to remove the motivation for doing so by making the object no longer connected to the shared source after the closure completes.

This all theoretically works, and will compile, and will not throw any swift exceptions when run.

The Accessor (or any class that has an instance of an Accessor) calls RestrictedClass.run(), the run code validates the Accessor instance, finds the DropClosure in that instance, and passes in an instance of the Restricted class to that closure.

However, whenever I try to access that instance from within the DropClosure, it gives me the aforementioned bad access error, seemingly on a C or Objective-C level.

As far as I can tell, the instance should be accessible at this point, and none of the variables being used should be dropping out of scope yet.

At this point I'm totally spitballing - is it possible that there is something in the background that prevents a class with no public initializers from being passed through a mirror? Does it have to do with passing it into a closure called from that mirror? Is there some kind of hidden weak reference that's making the instance get ARC'd?

Code:

import Foundation

typealias DropClosureVoid<T: AnyObject & AccessRestricted> = (_ weaklyConnectedInterface: WeaklyConnectedInterface<T>, _ usingParameters: Any?)->Void
typealias DropClosureAny<T: AnyObject & AccessRestricted> = (_ weaklyConnectedInterface: WeaklyConnectedInterface<T>, _ usingParameters: Any?)->Any?

enum AccessError : Error {
    case InvalidFunction
    case InvalidAccessClass
}
protocol AccessRestricted {
    static func run<T:AnyObject>(_ closureName:String, in classObject: T, with parameters:Any?) throws
    static func runAndReturn<T:AnyObject>(_ closureName:String, in classObject: T, with parameters:Any?) throws -> Any?
}
///This class contains an instance that should be expected to only temporarily represent the original, even if a strong reference is made that keeps the value in scope.
class WeaklyConnectedInterface<T:AnyObject> {
    weak var value:T?
    init(_ value: T) {
        self.value = value
    }
}
class Accessor {

    let restrictedClassPassable:DropClosureVoid<RestrictedAccessClass> = { weaklyConnectedInterface, parameters in
        print(weaklyConnectedInterface) // **EXC_BAD_ACCESS error here**
        //note that the error above happens even if I pass in the instance directly, without the WeaklyConnectedInterface wrapper. 
        //It's clearly an issue that occurs when trying to access the instance, whether the instance is wrapped in a the class that makes a weak reference to it or not, which means that it is inaccessible even when strongly referenced.
        if let parameterDict = parameters as? [String:String] {
            print(parameterDict["paramkey"] ?? "nil")
            print(weaklyConnectedInterface)
            weaklyConnectedInterface.value?.restrictedVariable = "I've changed the restricted variable"
        }
    }

    let anotherRestrictedClassPassable:DropClosureAny<RestrictedAccessClass> = { weaklyConnectedInterface, parameters in
        if let parameterDict = parameters as? [String:String] {
            print(parameterDict["paramkey"] ?? "nil")
            print(weaklyConnectedInterface.value?.restrictedVariable as Any)
            return weaklyConnectedInterface.value?.restrictedVariable
        }
        return nil
    }

    func runRestrictedClassPassable() throws {
        let functionName = "restrictedClassPassable"
        print("trying validateClosureName(functionName)")
        try validateClosureName(functionName)//this is in case you refactor/change the function name and the "constant" above is no longer valid
        print("trying RestrictedAccessClass.run")
        try RestrictedAccessClass.run(functionName, in: self, with: ["paramkey":"paramvalue"])
        let returningFunctionName = "anotherRestrictedClassPassable"
        print("trying validateClosureName(returningFunctionName)")
        try validateClosureName(returningFunctionName)
        print("trying RestrictedAccessClass.runAndReturn")
        let result = (try RestrictedAccessClass.runAndReturn(returningFunctionName, in: self, with: ["paramkey":"ParamValueChanged"]) as! String?) ?? "NIL, something went wrong"
        print("result is \(result)")
    }

    func validateClosureName(_ name:String) throws {
        let mirror = Mirror(reflecting: self)
        var functionNameIsPresent = false
        for child in mirror.children {
            if child.label != nil && child.label! == name {
                functionNameIsPresent = true
                break
            }
        }
        guard functionNameIsPresent else {
            print("invalid function")
            throw AccessError.InvalidFunction
        }
    }
}
extension Mirror {
    func getChildrenDict() -> [String:Any]
    {
        var dict = [String:Any]()
        for child in children
        {
            if let name = child.label
            {
                dict[name] = child.value
            }
        }
        return dict
    }
}


class RestrictedAccessClass:AccessRestricted {

    private static var shared:[String:Any] = [
        "restrictedVariable" : "You can't access me!"
    ]
    private static func validateType<T>(of classObject:T) throws {
        switch classObject {
        case is Accessor:
            return
        default:
            print("Invalid access class")
            throw AccessError.InvalidAccessClass
        }
    }
    var restrictedVariable:String
    private init() {
        restrictedVariable = "You can't access me!"
    }
    private init(from json:[String:Any]) {
        restrictedVariable = json["restrictedVariable"] as! String
    }
    static func run<T:AnyObject>(_ closureName:String, in classObject: T, with parameters:Any?) throws {
        print("trying validateType(of: classObject) in run")
        try validateType(of: classObject)
        for child in Mirror(reflecting: classObject).children {
            if let childName = child.label {
                if childName == closureName {
                    let dropClosure = child.value as! DropClosureVoid<RestrictedAccessClass>
                    let selfInstance = RestrictedAccessClass(from:shared)
                    let interface = WeaklyConnectedInterface(selfInstance)
                    dropClosure(interface, parameters)
                    runCleanup(on: selfInstance)//parses any data changed by the end of the drop closure back into the dict for use in future instances. This means you mustn't try using the instance in an async closure. The correct way to do this would be to call run inside of an async closure, rather than putting an anync closure inside of the drop closure.
                    _ = interface.value
                    return
                }
            }
        }
    }
    static func runAndReturn<T:AnyObject>(_ closureName:String, in classObject: T, with parameters:Any?) throws -> Any? {
        print("trying validateType(of: classObject) in runAndReturn")
        try validateType(of: classObject)
        for child in Mirror(reflecting: classObject).children {
            if let childName = child.label {
                if childName == closureName {
                    let dropClosure = child.value as! DropClosureAny<RestrictedAccessClass>
                    let selfInstance = RestrictedAccessClass(from:shared)
                    let interface = WeaklyConnectedInterface(selfInstance)
                    let result = dropClosure(interface, parameters)
                    runCleanup(on: selfInstance)//parses any data changed by the end of the drop closure back into the dict for use in future instances. This means you mustn't try using the instance in an async closure. The correct way to do this would be to call run inside of an async closure, rather than putting an anync closure inside of the drop closure.
                    _ = interface.value
                    return result
                }
            }
        }
        return nil
    }
    private static func runCleanup(on instance:RestrictedAccessClass) {
        shared = Mirror(reflecting:instance).getChildrenDict()
        //once this function goes out of scope(or shortly thereafter), the instance passed will become useless as a shared resource
    }


}

Code to encounter error:

I just put this in a new project's AppDelegate.application(didFinishLaunching). You can put all of the code above and below, in order, in a playground and it will break in the same spot, but not as clearly.

let accessor = Accessor()
do {
    try accessor.runRestrictedClassPassable()
}
catch {
    print(error.localizedDescription)
}




Aucun commentaire:

Enregistrer un commentaire