I think I found a bug in the Objective-C++ compiler, linker, or runtime. Here’s the scenario:
- We have a macOS app written in Swift.
- To control hardware from a 3rd party manufacturer, we use a couple SDKs provided by the manufacturer.
- The SDKs use dynamically loaded libraries and the interface is defined in C++ headers.
- To bridge between our Swift code and the C++ APIs we have a private Cocoapod that wraps the 3rd party interface with Objective-C++ classes.
The two SDKs each provide an interface for discovering attached devices using a callback class that the programmer provides. By accident we named both callback implementations DiscoveryCallback
, but this was not a compiler error because neither class was publicly declared, and each was defined in the .mm file where it was used.
However, the problem we’re seeing is this:
- We want to discover Videohub devices, so we register a new instance of
DiscoveryCallback
(defined in the same .mm file as this code) with the Videohub SDK. - A Videohub device is connected and the SDK calls a method on our callback.
- Surprise! The callback we registered in step 1 was actually the one intended for Decklink devices, defined in a completely different .mm file.
- This violates all sorts of assumptions and our app quickly crashes.
The funny thing is, the two implementations of DiscoveryCallback
have completely different method names. The Videohub SDK is supposed to be calling NewVideohubDevice,
yet somehow it successfully calls DeckLinkDeviceArrived
on an instance of a class it shouldn’t even know about.
So the compiler has checked that our intended DiscoveryCallback
matches the protocol that the SDK expects, but at runtime the compiled code instantiates a completely different implementation of DiscoveryCallback
and somehow doesn’t immediately fail; we still call a method on it that doesn’t even share a name with the intended target. I imagine at this point the method names are long forgotten and are just pointers in a table.
I don’t know if this is a bug in the compiler, the Objective-C++ runtime, or if this is just “working as designed” undefined behavior that I should have avoided by not giving two private classes the same name. I know it’s possible to use a private API simply by redeclaring it in my own code, and this seems related to that, but I feel like the compiler or linker should have warned me that I had two implementations of the same class, or if that is not an error, then the runtime should have instantiated the class that was privately defined in the same source file where it was used.
Obviously I can’t share our entire project; I’d like to provide some sample code that replicates the issue, but I don’t have time to do that right now. I’m posting this to see if other developers have had a similar experience.
This doesn’t sound like a bug, but rather a consequence of how Obj-C has always worked. (You’re using Obj-C++, but I assume the names you give are Obj-C classes.)
In Obj-C, class names are global to your app. They are not implicitly qualified by a module name, and they are not local to one target.
In particular, wherever a class name is declared in header files, it represents the same class app-wide, no matter where the declaring header file is. If classes of that name have implementations in more than one target, these are conflicting implementations of the same class. IIRC, the linker will warn you if you provide 2 implementations of the same class in the same target, but I don’t think it has any reasonable way of checking this across targets.
This is the reason why an app’s Obj-C classes have traditionally been given a 2- or 3-letter prefix to make them unique, and it sounds like this is the pattern you should follow here, at least for the class names you control.
Depending on what you're doing, another approach might be to use Swift's ability to interop directly with C++.