I'm curious, what difference does it make if it was written in, say, Rust, or C++. Wouldn't you still be able to talk to it in a similar way? What's so special about C in this context?
The thing that's always special about C, it's basically the default goto programming language for calling from higher level code, most languages that support FFI do so by being able to communicate with C apis and having tools written around C.
C also lacks more complex things like classes, it's much easier to build up a complex piece of software from C building blocks bound to your language. Think of like a struct that has a set of functions that operate on it. You can map that basically directly to a class definition in most higher level languages and then wrap up the c implementation details like memory management behind the scenes so you can produce a nice clean api in your chosen language to use.
This practice is fairly common in python when you're dealing with AI (I'm sure there's other examples, but this is the one that sticks out to me.) SDKs like TensorFlow are written in a lower level language (C++ I think for TF) and then FFI called from python, so you can end up with a function like initTensorFlow() and calling it is enough to bring TF online and ready to start processing other data.
And you may be thinking "Well, I can do all that in rust or go or <insert prefered language here>" and of course you can, ultimately it's all just a linker and compiler deciding where to put code and data in an executable, but C is the defacto standard and more tools from more languages are going to work well with it than say, rust or go. Which, now that I think about it, is probably why Rust supports defining a c api for your code, so it's easier to call from other platforms. Rust specifically tries to play nice with being embedded in existing projects because it makes it easier to adopt rust incrementally.
C constructs normally map 1:1 in most languages. It's rare for a modern language to not be able to do all the things that C does. And, even if it's a little weird, it's not too hard to give out an escape hatch because the features aren't that advanced.
C++ constructs--not so much.
Does your language allow overloaded functions? If not, okay, you have to map that somehow (probably name mangling).
Oh, your language has a different notion of inheritance than C++, well, now you need to map vtables somehow.
And what does your system do about exceptions? Constructors/Destructors? Does it agree with the standard library about what a String is? What about memory allocation? etc.
Even if the C++ ABI was perfectly described and standardized, mapping to it is always going to be error prone and clunky relative to the C ABI (which is just a lot simpler).
If you want a good example, look at what it takes to use Vulkan via the C API/ABI--which is an exemplary implementation of an API, in my opinion. Look at the hoops you go through with loaders, structs that have type fields in them, extension fields so that you can add functionality without breaking compatibility, etc. All of that extra "gunk" is the kind of thing that C++ hides normally that gets terribly exposed when you have to cross an ABI boundary.
> It's rare for a modern language to not be able to do all the things that C does.
I agree with the general gist of this, but:
• Java has no unions
• Java has no rectangular multidimensional arrays, although of course C arrays decay to pointers when passed anyway
• Java has no unsigned integer types, I imagine this is generally easily handled though
• Java has no const modifier, you'd need to keep track of that manually
• Java has no preprocessor, although of course this happens at the API level not the ABI level
• Java lacks bitfields but they're generally avoided in C APIs anyway, for good reason
• Probably worst of all, Java references are importantly different from C pointers
You're absolutely right though that the C ABI is a pretty good lingua franca ABI largely due to C being a pretty minimal language. No overloading, templates, garbage collector, or object model to worry about.
I think, as a generalization, most communication between languages goes through C. For example Rust or Switft to C++ would go Rust -> C -> C++. Or Switft -> C -> Rust as examples. I think Swift is trying to make C++ bindings, but in general most languages can talk to/from C.
Of course there are other mechanisms, but for direct bindings I think C bindings are by far the most common.
Actually for Swift <-> C++ you tend to go via Objective C++, which as easy as renaming your .cpp file to .mm, and calling in to Swift via a bridging header
The important part is "C API", the actual implementation language under the API isn't all that important, but C is usually a good fit as implementation language for a C API ;)
And because it is in C, you can talk to it from Nim, Python, Rust, etc.
And scripting is a very big part of RTS games.