Yup but OSX has a tonne of C (and unfortunately C++) APIs.
But yeah having most of the system apis being in a language that has built in dynamic resolution makes a world of difference in the difficulty of maintaining abi compat
Publicly exposed? Most of CoreFoundation and the other C APIs vend objects that are entirely opaque, and there are very few actual C++ APIs that I'm aware of–though many are written in (Objective-)C++ internally.
There are plenty of APIs that have structs in their interface, although they are always passed by reference so that the structs can increase in size, while still retaining compatibility with old software, e.g.
struct APIFoo {
int a_field;
}
void do_something(struct APIFoo* argument);
later on we add a new field, and that becomes:
struct APIFoo {
int a_field;
int new_field;
}
For API compatibility you just need the source level field names to remain, and to remain the same type. But for ABI compatibility - e.g a existing compiled program - the fields must remain in the same location, so you can only add members to the end, and you can't change the alignment or padding rules.
What remains is how the API implementation knows if the argument is the size of the old api, or the new one.
The Microsoft API idiom is to have a size field that you initialize to sizeof(APIFoo). This means when you recompile you code against a more recent version of the API you'll get a modern sized struct. My feeling has always been that this approach is more fragile, as you can unintentionally increase the struct without initializing the new members. Apple APIs that need it tend to use an explicit version number, so you have to explicitly say that you know which fields you want to update - as an example you can look up JSClassDefinition (IIRC) in JavaScriptCore.
The other thing that Apple APIs will do is use a linked-on-or-after check: basically the macOS and iOS include metadata about the system version that a library or application was compiled to target. This is generally used to control legacy behaviour to handle cases where some internal logic leaked across an encapsulation boundary but the behaviour that was leaked is simply too awful to retain in general, and in the worst of worst cases code may include explicit tests for what the current application is (you can see a bunch of these in the WebKit codebase). Microsoft accomplishes similar tasks using (I think they're called) compatibility shims.
You are right however that the actual objects you get returned in macOS and iOS APIs are basically all opaque objects - looking at the JSC example, the actual API objects are opaque: so JSClassMake() takes a pointer to a JSClassDefinition, and returns an opaque JSClassRef that provides the actual API object you use to create objects.
As for C++: I would suggest you look at the nightmare that is IOKit. That's right, the kernel driver APIs are in C++. Because NEXT wrote that code in the 90s, when C++ was considered the perfect solution to all problems (e.g. before people really understood how hard C++ makes ABI compatibility)
Well aware of how C++ structures have issues with ABI compatibility, as well as the various linked-on-or-after checks ;) Note that DriverKit on NeXTSTEP, on which IOKit is based, was written in Objective-C. The switch to embedded C++ for Mac OS X was done to attract developers comfortable in the language, AFAIK.
But yeah having most of the system apis being in a language that has built in dynamic resolution makes a world of difference in the difficulty of maintaining abi compat