Designing a C interface for complex types
Update Nov 2022: After doing 4-5 interfaces in this fashion, if I had it to do over, I would probably use SWIG. I passed on it initially because it had only one maintainer on GitHub. However, given the time I ended up sinking into writing & debugging custom interfaces, it may have been worth the risk.
Let’s say we are designing an interface to pass a complex type between C++ modules. If there is any possibility of the two modules being built by different compilers, different versions of the same compiler, or for different platforms, the C++ ABI could be different, making updates potentially very brittle. The unfortunate result is we’ll need to use C functions & structures for the interface. The same is true of C++ to another language.
As an example, let’s say our module houses a lookup we want to share:
If we wanted to make this lookup available to another module, due to reasons previously stated, we can’t pass our nice C++ types. So, we would need something like this:
So, the implementation of the external-facing function could look something like this:
The code in the caller would look something like this
I don’t love this approach. To be honest, I’ve never liked returning values via parameter. Especially, in functions with several parameters, I think using return values is much clearer to the reader.
In this case, it also puts a number of responsibilities on the caller. First, if they get a ski resort, there is nothing about that type that makes it obvious how to get more information about it (a run list by difficulty, for example). It requires that they know of the existence of ExternalInterface_SkiResort_GetRunsByDifficulty(). Sure, in our example, it is declared close by and named decently, but that is not always the case. Second, it makes the caller responsible for declaring all of the necessary pointers, then cleaning up the memory manually when finished. We could add a cleanup function in the library that the caller can use, but again, the caller has to know about the function's existence.
So, how might we design this to avoid these two issues? By taking over these responsibilities in the library:
In this example, ExternalInterface_GetSkiResort() is the only thing the caller needs to know about. We could even put it in a separate file to set it apart. Use becomes more straightforward as well:
The caller no longer has to worry about set up, the operations possible on a given return structure are included in the structure, and the cleanup details are abstracted away. The complexity has been moved inside the library:
This is a lot of boilerplate. Is it worth it for the reduced complexity for the caller? I'm not sure. I've been thinking around objective reasons the latter might be better, but every performance or memory management situation I have thought of can be handled with either approach. It seems to boil down to ease of use for the caller.
Comments
Post a Comment