Singleton double instantiation

I have a project that is a mixture of Obj-C and Swift. A singleton instance is instantiated twice: once in the test target and once in the App target under test. I’m starting an investigation into this. It seems combinations of file target membership and dynamic linking change the outcome. I’ll come back with more information after I experiment further.

The following is bridged to swift.

@implementation Client

+(instancetype)sharedInstance {
    static Client *client = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        client = [[Client alloc] init];
        NSLog(@"[DEBUG] Memory address of %@", client);
    });
    return client;
}

Has anyone seen this problem before?

It seems withUnsafePointer generates a new pointer each time it’s called, whereas Unmanaged.passUnretained(instance).toOpaque() does not.

Where? You'll have to show us the Swift code that's calling this

withUnsafePointer(to:) used on a class instance is almost always going to do the wrong thing. It'll give you a pointer to the memory on the stack that contains the reference to the class instance, not to the class instance itself. When that stack frame is lost, so is the pointer. Unmanaged is the correct way to pass pointers to classes in Swift.

8 Likes
import XCTest

@testable import HelloWorldSwift

final class HelloWorldSwiftTests: XCTestCase {
    
    func testExample() throws {
        let algorithm = Algorithm()
        XCTAssertEqual(algorithm.calculate(), 10)
        let client = Client.shared
        withUnsafePointer(to: client) { pointer in
            print("[TEST] Memory address of \(client): \(pointer)")
        }
        withUnsafePointer(to: client) { pointer in
            print("[TEST] Memory address of \(client): \(pointer)")
        }
        XCTAssertEqual(algorithm.perform(client: client), 7)
    }
}

in ios app target

import Foundation

final class Client {
    
    static let shared = Client()
    
    private init() {
        withUnsafePointer(to: self) { pointer in
            print("[DEBUG] Memory address of \(self): \(pointer)")
        }
    }
    
    func establish() -> Int {
        return 4
    }
}

import Foundation

final class Algorithm {
    
    func calculate() -> Int {
        perform(client: Client.shared) + 3
    }
    
    func perform(client: Client) -> Int {
        client.establish() + 3
    }
}
1 Like

Just posted the code to @lukasa in this thread :saluting_face:

Yep, you have the exact issue that @lukasa pointed out.

Using Unmanaged.passUnretained(instance).toOpaque() will work correctly, because it's looking at the address of the object itself (on the heap), not the address of the client variable (on the stack).

Also, you might be interested in using ObjectIdentifier(client), which is great for debug info like this.

5 Likes

I'm trying to understand if I do in fact have two instances of a singleton running when executing my tests. My test project is only a handful of lines of code.

[DEBUG] Within client init: 0x0000600000008270
[DEBUG] AppDelegate: 0x0000600000008270
Test Suite 'HelloWorldSwiftTests' started at 2024-07-24 05:53:49.152.
Test Case '-[HelloWorldSwiftTests.HelloWorldSwiftTests testExample]' started.
[DEBUG] Within client init: 0x0000600000008350
[DEBUG] Algorithm, perform: 0x0000600000008350
[TEST] client: 0x0000600000008350
[DEBUG] Algorithm, perform: 0x0000600000008350
Test Case '-[HelloWorldSwiftTests.HelloWorldSwiftTests testExample]' passed (0.001 seconds).
Test Suite 'HelloWorldSwiftTests' passed at 2024-07-24 05:53:49.154.
	 Executed 1 test, with 0 failures (0 unexpected) in 0.001 (0.002) seconds

Agove is the output from the following code.

import Foundation

final class Algorithm {
    
    func calculate() -> Int {
        let client = Client.shared
        return perform(client: client) + 3
    }
    
    func perform(client: Client) -> Int {
        assert(Client.shared === client)
        let pointer = Unmanaged.passUnretained(client).toOpaque()
        print("[DEBUG] Algorithm, perform: \(pointer)")
        return client.establish() + 3
    }
}

final class Client {
    
    static let shared = Client()
    
    private init() {
        let pointer = Unmanaged.passUnretained(self).toOpaque()
        print("[DEBUG] Within client init: \(pointer)")
    }
    
    func establish() -> Int {
        return 4
    }
}

And in the test

import XCTest
import HelloWorldSwift

final class HelloWorldSwiftTests: XCTestCase {
    
    func testExample() throws {
        let algorithm = Algorithm()
        XCTAssertEqual(algorithm.calculate(), 10)
        let client = Client.shared
        let pointer = Unmanaged.passUnretained(client).toOpaque()
        print("[TEST] client: \(pointer)")
        XCTAssertEqual(algorithm.perform(client: client), 7)
    }
}

Note: I am not using testable import. Instead, both client.swift and algorithm.swift are target members of all targets.

Pausing execution of the running tests reveals some runtime confusion. My understanding here is that the source is compiled in both modules, hence the Module name prefix to help distinguish them.

error: <EXPR>:8:8: error: ambiguous use of 'shared'
Client.shared
       ^
HelloWorldSwiftTests.Client (internal):2:25: note: found this candidate
    internal static let shared: HelloWorldSwiftTests.Client
                        ^
HelloWorldSwift.Client (internal):2:25: note: found this candidate
    internal static let shared: HelloWorldSwift.Client

I need to understand this in terms of

  1. Module boundaries
  2. Pointers
  3. More than one instance by accident?
  4. Dynamic linking
  5. Xcode running the app and test target in same process or not? If so, does dynamic linking ensure only one pointer recognised by each process?

If the same file is in two targets, then yes, you will instantiate two singletons. That's because the types are actually different, so you get one singleton for each of them.

1 Like

Thank you