wickwirew / Runtime

A Swift Runtime library for viewing type info, and the dynamic getting and setting of properties.
MIT License
1.08k stars 94 forks source link

withValuePointer() sees object not as a class instance but an existential instance #81

Open NSExceptional opened 3 years ago

NSExceptional commented 3 years ago

I'm not sure what's going on, but whatever it is I think I might be doing something wrong.

Here is a project that demonstrates the issue: https://github.com/FLEXTool/SwiftEX

Select the SwiftEXTests scheme and run the testMirror() test. It fails. My testing shows that for some reason, Value.self inside the call to withValuePointer() is seen as an existential instead of a class type. It appears to be correct, too, which tells me I'm the one doing something wrong.

The gist of what I'm trying to do is to wrap your APIs and make it work like a readwrite Mirror, so that I don't have to fumble with .property(named:) and property.set(value:on:) or property.get(on:) every time I want to do something.

Any help is appreciated!

NSExceptional commented 3 years ago

What's really weird to me is if I break right here on the kind declaration and do po type(of: value), I get the class I expect. But when I po type inside Kind(type:) I get Any

func withValuePointer<Value, Result>(of value: inout Value, _ body: ...) throws -> Result {
--> let kind = Kind(type: Value.self) // po type(of: value) → SwiftEXTests.SwiftEXTests
    let obj = value as AnyObject
    let cls: AnyClass = object_getClass(obj)!
    let same = cls === cls

    switch kind {
    case .struct:
        return try withUnsafePointer(to: &value) { try body($0.mutable.raw) }
    case .class:
        return try withClassValuePointer(of: &value, body)
    case .existential:
        return try withClassValuePointer(of: &value, body)
    default:
        throw RuntimeError.couldNotGetPointer(type: Value.self, value: value)
    }
}

(Ignore the code I added for myself for testing)

public enum Kind {
    ...

    init(type: Any.Type) {
    --> let pointer = metadataPointer(type: type) // po type(of: value) → Any
        self.init(flag: pointer.pointee)
    }
}
NSExceptional commented 3 years ago

Update: even if I force it to return Kind.class, it looks like you're not accounting for inheritance… the reported offset of this particular ivar is 56 and the actual offset reported by ivar_getOffset is 272

wickwirew commented 3 years ago

So Value.self in that context is actually an Any so existential is correct. The Property.get takes an Any and so the original type would be boxed in the existential container.

As for inheritance, it may be an objc thing but I am not too sure at the moment, Ill keep looking into it though. The offset stored on the field descriptor should already take that into account.

I wrote a quick unit test in your project to verify:

class Foo {
    let a: Int = 1
}

class Bar: Foo {
    let b = Baz()
}

class Baz {}

let bar = Bar()

let info = try typeInfo(of: Bar.self)

let aProp = try info.property(named: "a")
let a: Int = try aProp.get(from: bar) as! Int

let bProp = try info.property(named: "b")
let b = try bProp.get(from: bar) as! Baz

XCTAssert(a == 1)
XCTAssert(b === bar.b)
NSExceptional commented 3 years ago

I think the rules are different across module boundaries or when subclassing an objc class

Relevant Twitter thread

NSExceptional commented 3 years ago

So Value.self in that context is actually an Any so existential is correct. The Property.get takes an Any and so the original type would be boxed in the existential container.

Shouldn't Value.self be equivalent to type(of: value) in that context? The debugger showed me what I expected. If it's always an existential, would it still be able to find and access fields? o_O

wickwirew commented 3 years ago

In a generic context things get a bit tricky, type of type(of: value) will return Value.self which is Any. To get the actual under lying type you'd have to do type(of: value as Any).

If it's always an existential, would it still be able to find and access fields? o_O

It manually unboxes the value. Its done like this to simplify things. Having it always boxed made it a bit more predicable.

Check out the docs on it https://developer.apple.com/documentation/swift/2885064-type . See the "Finding the Dynamic Type in a Generic Context" section.

Also here's an interesting example to help illustrate:

struct Foo {}

func holdMyBeer<T>(val: T) {
    print(T.self)
    print(type(of: val))
    print(type(of: val as Any))
}

let val = 1
holdMyBeer(val: val) // Int, Int, Int
holdMyBeer(val: val as Any) // Any, Any, Int

Sorry didn't mean to close it

NSExceptional commented 3 years ago

That's crazy. Well then I guess the only problem is the ivar offset thing, which I've been working on.

Tell me, is one of the members in ClassMetadataLayout equivalent to startOfImmediateMembers? I think that's the flag Joe was referring to (or something like it anyway) but I'm not sure, and I can't tell if your struct has it defined already under a slightly different name or not