Crash when dynamic dispatch method For Class Extension Method


(Tanner Jin) #1

Language Version: Swift4.2
Xcode Version: Xcode10.0


Error: segmentation fault
As you see, after I exchange Foo and Poo's extension method, I call Foo's instance method foo, it crash.
I know the method 'foo.foo()' called by objc_msgSend dynamic dispatch.

And if Foo is not subClass of NSObject, it fine.


I know Foo is subClass of SwiftObject(same as NSObject) after compiled and method 'foo.foo()' is also called by objc_msgSend dynamic dispatch.

But I don't know why NSObject's subClass will Crash in this case.


(Jordan Rose) #2

This is a low-level implementation detail, but subclasses of NSObject use a different implementation of reference counting from pure Swift classes. That's why you get a crash in the Swift runtime: it's trying to retain a subclass of NSObject using the logic that makes sense for pure Swift objects.

(Strictly speaking, maybe this retain isn't necessary, but I still wouldn't bet on this working in general.)

As an aside, if you're going to use the Objective-C runtime to change method implementations, you should always mark those methods as dynamic. Otherwise, the compiler might try to inline calls to those methods, and you won't get the implementation you're expecting.


(Tanner Jin) #3

Thanks for your answer and another problem make me confused recently.

protocol FooProtocol {
    func foo()
}

protocol PooProtocol {
    func poo()
}

class Foo: FooProtocol {
    let name = "Foo"
    
    func foo() {
        print("foo")
    }
}

struct Poo: PooProtocol {
    let name = "Poo"
    
    func poo() {
        print("poo")
    }
}

func getPointer<T: Any>(value: inout T) -> UnsafeMutableRawPointer {
    let pointer = withUnsafeMutablePointer(to: &value) { (pointer) -> UnsafeMutablePointer<T> in
        return pointer
    }
    return UnsafeMutableRawPointer(pointer)
}

// 32: witeness_table pointer
var foo: FooProtocol = Foo()
let fooPointer = getPointer(value: &foo)
let foo_witness_table_pointer = fooPointer.advanced(by: 32).assumingMemoryBound(to: UnsafeMutableRawPointer.self)

var poo: PooProtocol = Poo()
let pooPointer = getPointer(value: &poo)
let poo_witness_table_pointer = pooPointer.advanced(by: 32).assumingMemoryBound(to: UnsafeMutableRawPointer.self)

// relpace witness_table' pointer
foo_witness_table_pointer.initialize(to: poo_witness_table_pointer.pointee)

/ 8: first method pointer
//let method_foo_pointer = foo_witness_table_pointer.pointee.advanced(by: 8).assumingMemoryBound(to: UnsafeMutableRawPointer.self)
//let method_poo_pointer = poo_witness_table_pointer.pointee.advanced(by: 8).assumingMemoryBound(to: UnsafeMutableRawPointer.self)

// replace witness_table's method' pointer
//method_foo_pointer.initialize(to: method_poo_pointer.pointee)

foo.foo()  // print poo

This case can replace method 'foo' with 'poo' successful.

but if change class Foo and struct Poo like below

struct Foo: FooProtocol {
    let name = "Foo"
    
    func foo() {
        print("foo")
    }
}

class Poo: PooProtocol {
    let name = "Poo"
    
    func poo() {
        print("poo")
    }
}

other's don't change. This case will crash.

0x50 should be Foo or Poo's vtable' first method pointer offset in struct objc_class.

So, first case's method dispatch is witness_table, but why the second case will crash? Whether the second method dispatch is vtable?


(Jordan Rose) #4

It's never correct to return a pointer out of withUnsafeMutablePointer; the pointer you get within the closure is only guaranteed to be be valid within the body of that closure. It's also not correct to swap implementations in a witness table or vtable ever, since methods can have different calling conventions for different types, and on platforms with pointer authentication.

That said, it's fine to play around with this for fun. Just don't ship it in anything.


My guess is that you're correct: the protocol witness implementation for the class is trying to do a vtable dispatch, but structs don't have vtables (because they don't have subclasses).


(Tanner Jin) #5

but if I read the memory that fooPointer point to, it can show foo's really memory. like below


(Tanner Jin) #6

I see, I test on Mac(intel cpu), not on iPhone(arm cpu) and has Pointer Authentication.
Thanks a lot


(Brent Royal-Gordon) #7

There's a difference between what the compiler happens to do today, and what it promises it will do in all versions. inout parameters may have the memory address of the original value, or they may have the memory address of a temporary value which will be assigned back to the original after the call. (That's how you can pass a computed property inout.) The compiler doesn't make any promises about which one will choose in any particular situation, so you should always assume the pointer will not be valid after the call.

What you're seeing here is that, in this particular code sample with this particular compiler version, the compiler chooses to pass the original address. But it can change that decision for any reason at any time, so you shouldn't write code that only works if it passes the original address.

(Having said that, if you're just experimenting to learn how Swift works, this is fine. Just don't expect it to keep working.)


(Tanner Jin) #8

Thank you very much. yep, I'm testing how swift method dispatch and memory layout