Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
NSExceptional opened this issue Jan 28, 2021 · 7 comments

Comments

@NSExceptional
Copy link
Contributor

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
Copy link
Contributor Author

NSExceptional commented Jan 28, 2021

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
Copy link
Contributor Author

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
Copy link
Owner

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
Copy link
Contributor Author

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

Relevant Twitter thread

@NSExceptional
Copy link
Contributor Author

NSExceptional commented Jan 28, 2021

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
Copy link
Owner

wickwirew commented Jan 28, 2021

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

@wickwirew wickwirew reopened this Jan 28, 2021
@NSExceptional
Copy link
Contributor Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants