Designing interfaces in C ++

I am developing an interface that can be used like dynamic loading. Also it should be compiler independent. So I want to export interfaces. We are faced with the following problems.

Problem 1: Interface functions take some user-defined data types (mostly classes or structures) as In \ Out parameters. I want to initialize the members of these classes with default values ​​using constructors. If I do this, it is impossible. load my library dynamically and it becomes compiler dependent. How to solve it.

Problem 2: Some interfaces return lists (or maps) of an item for the client. I am using std containers for this purpose. But this also again depends on the compiler (and the compiler version is also multiple times).

Thank.

+3


source to share


3 answers


Code compiled differently can only work together if it uses the same Application Binary Interface (ABI) for the set of types used for parameters and return value. ABIs are significant at a much deeper level: names, virtual dispatcher tables, etc., but I believe that if one of your compilers supports the ability to call functions with simple types, you might at least consider hacking some support for more complex types like , for container-specific implementations of standard containers and custom types.

You will need to research what the ABIs your compilers support and point out what you can about what they will continue to provide.



If you want to support other types besides those that conform to the ABI standard, options include:

  • use simpler types to display internal objects of more complex types

    • pass [ const

      ] char*

      and size_t

      , extracted my_std_string.data()

      or &my_std_string[0]

      and my_std_string.size()

      , similarly forstd::vector

    • serializes data and deserializes it using the receiver data structures (can be slow)

  • provides a set of function pointers to simple accessor / mutator functions implemented by the object that created the data type

    • eg. how a classical C function qsort

      takes a pointer to an element comparison function
+3


source


As usual I have multithreaded focus, I will mostly bark about your second problem.

You already figured out that passing container elements over the API seems to be compiler-dependent. This is actually worse: it is a header file and C ++ - depends on the library, so at least for Linux you are already stuck with two different sets: libstC ++ (comes from gcc) and libcxx (comes from clang). Since some of the containers are header files and some are library code, getting ABI-independent information is almost impossible.

My biggest concern is that you were actually thinking about moving the container elements around. This is a huge thread-safety issue: STL containers are not threadsafe - by design.

By skipping links through the interface, you are passing "pointers to encapsulated knowledge" around - your API users can make assumptions about your internal structures and start modifying that data. This is usually very bad in a single thread environment, but degrades in a multithreaded environment.



Second, the pointers you provided might be outdated and not good.

Be sure to return copies of your internal knowledge to prevent the user from changing your structures.

Passing things const

around isn't enough: const can be dropped and you still expose your internals.

So my suggestion is: hide data types, only pass simple types and / or structs that you have full control over (i.e. not STL or boost dependent).

+2


source


Developing an API with the broadest possible ABI compatibility is an extremely difficult subject, especially when C ++ is involved instead of C.

However, there are more theoretical problems that are actually not as bad as they sound in practice. For example, in theory triggering legends and structure padding / alignment sounds like they might be major headaches. In practice, there are not many, and you can even solve such problems at the same time by providing additional building instructions to third parties or by decorating your SDK functions with macros specifying the appropriate calling convention. "Not too bad" here, I mean they might touch you, but they won't make you go back to the drawing board and reverse engineer the entire SDK in return.

The "practical" issues that I want to address are issues that can lead to a revision of the drawing board and a repeat of the entire SDK. My list is also not exhaustive, but some of them I think you should remember first.

You can also think of your SDK as having two parts: a dynamically linked part, which actually exports functions whose implementation is hidden from clients, and a statically (internally) linked convenience library, which adds C ++ wrappers on top. If you view your SDK as having these two separate parts, you are allowed a lot more freedom in a statically linked library to use a lot more C ++ mechanisms.

So let's start with those practical headache inducers:

1. The vtable binary layout is not necessarily consistent across compilers.

This, in my opinion, is one of the biggest mistakes. We usually consider two main ways of accessing functions from one module to another at runtime: function pointers (including those provided by looking up the dylib symbol), and interfaces that contain virtual functions. The latter can be much more convenient in C ++ (for both developers and client using the interface), but unfortunately, using virtual functions in the API, which should be binary compatible with the widest range of compilers, seems like a game minesweeper through the land gotchas.

I would recommend avoiding virtual functions for this purpose, unless your team is made up of minesweeper experts who know all of these mistakes. It's helpful to try to fall in love with C again for these parts of the common interface, and start creating an affinity for these types of interfaces that are made up of function pointers:

struct Interface
{
    void* opaque_private_data;
    void (*func1)(struct Interface* self, ...);
    void (*func2)(struct Interface* self, ...);
    void (*func3)(struct Interface* self, ...);
};

      

They introduce far fewer bugs and are nowhere near as vulnerable to change (for example: you are perfectly allowed to do things like adding additional function pointers at the bottom of the structure without affecting the ABI).

2. Stub libs for dylib symbol lookup are linker-specific (like all static libraries in general).

This may not seem like a big problem when combined with # 1. When you throw virtual functions in order to export interfaces, the next big temptation is to frequently export entire classes or fetch methods via dylib.

Unfortunately, doing this with a manual character lookup very quickly becomes very cumbersome, so the temptation is often to do this automatically by simply binding it to the appropriate stub.

But this too can get cumbersome when your goal is to support as many compilers / linkers as possible. In such a case, you may have to have many compilers, and create and distribute different stubs for each possibility.

So, this might nudge you into a corner where export class definitions are no longer very practical. For now, you can just export standalone functions with a C link (to avoid changing the C ++ name, which is another potential source of headaches).

One of the things that should be obvious is that we are pushing ourselves more and more to use the C or C-like API if our goal is universal binary compatibility without opening too many cans of worms.

3. Different modules have "different heaps".

If you allocate memory in one module and try to free it in another, then you are trying to free memory from the mismatched heap and will cause undefined behavior.

Even in plain old C it's easy to forget this rule and malloc

in one exported function only to return a pointer to it with the expectation that a client accessing memory from another module will be free

when it's done.This calls undefined behavior once again, and we must export the second function to indirectly free memory from the same module that allocated it.

This can get much larger in C ++, where we often have class templates that have an internal linkage that does memory management implicitly. For example, even if we roll our own A- std::vector

like sequence such as List<T>

, we can run a script when the client creates a list, passes it to our API by reference, where we use functions that can allocate / free memory (for example, push_back

or insert

) and butt heads with this inappropriate heap / free storage problem. So even this manual container has to ensure that it allocates and frees memory from the same centralized location if it will be passed across all modules, and placing a new one will be your friend when implementing such containers.

4. Passing / returning standard C ++ objects is not ABI compliant.

This includes the standard C ++ containers, you guessed it. There is no really practical way to ensure that one compiler will use a compatible representation of something like std::vector

when included <vector>

as another. Thus, passing / returning such standard objects, the representation of which is out of your control, is out of the question at all if you are aiming for wide binary compatibility.

They don't even necessarily have compatible views in two projects built by the same compiler, as their views may differ in incompatible ways based on build settings.

This might make you think that you have to manually flip all kinds of containers, but I would suggest the KISS approach here. If you are returning a variable number of elements as a result of a function, then we don't need a wide range of container types. We only need one type of container with a dynamic array, and it doesn't even have to be a persistent sequence, just something with the correct copy, move, and destroy semantics.

This may sound nice and can save you a few loops if you've just returned a set or map in a function that computes it, but I would suggest forgetting about returning those more complex structures and converting to this underlying dynamic view array. Rarely is it a bottleneck that you would think it would be going to / from adjacent views, and if you really bump into an access point as a result of this, which you actually got in a legitimate real world use case profiling session, then you you can always add to your SDK in a more discrete and selective way.

You can also always wrap these more complex containers like map into a C-type function pointer interface, which treats the map handle as opaque, hidden from clients. For larger data structures, such as a binary search tree, the cost of one level of indirection is usually very small (for simpler structures, such as a continuous random access sequence, it is usually not that insignificant, especially if your read operations, such as operator[]

include indirect calls).

Another thing worth noting is that everything I've discussed so far is about the exported, dynamically linked side of your SDK. A convenience static library that is internally linked can accept and return standard objects to make things third party friendly using your library, provided you don't actually pass / return them in the exported interfaces. You can even not collapse your own containers directly and just stick to the C-style for the exported interfaces, returning the original pointers to T*

that should be freed when your convenience library does this automatically and passes the content to std::vector<T>

, for example,

5. Throwing exceptions between module boundaries is undefined.

Usually, we shouldn't have to throw exceptions from one module to get caught in another when we cannot provide compatible build settings in two modules, let alone the same compiler. So throwing exceptions from your API to indicate input errors is out of the question at all.

Instead, we have to catch all possible exceptions at the entry points into our module to avoid leaking them out to the outside world and translate all such exceptions into error codes.

A statically linked convenience library can still call one of your exported functions, check for an error code, and throw an exception on failure. This is fine because this convenience library is internally linked to the third party module using this library, so it effectively throws an exception from the third party module that must be detected by the same third party module.

Conclusion

While this is by no means an exhaustive list, these are some caveats that can, when not heard, cause some of the biggest problems at the broadest level of your API design. These design-level problems can be exponentially more expensive to fix in hindsight than implementation-type problems, so they should be given the highest priority.

If you are new to these subjects, you cannot go too far in favor of C or a very C-like API. You can still use a lot of C ++ by implementing it, and you can also re-create the C ++ convenience library (your clients don't even have to use anything other than the C ++ interfaces provided by this built-in convenience library).

With C, you usually look at a lot of work at a basic level, but perhaps a lot less of those catastrophic design errors. With C ++, you're looking for less basic level work, but much more potentially disastrous surprise scenarios. If you favor the latter route, you usually want to make sure that your team's experience with ABI issues is higher, with a large coding standards document devoting large sections to these potential ABI errors.

For your specific questions:

Problem 1: Interface functions are executed by some user defined data types (mostly classes or structs) as In \ Out parameters. I want to initialize members of these classes with default values ​​using constructors. If I do this, it is not possible to load my library dynamically and it becomes compiler dependent. How to solve it.

This statistically linked convenience library might come in handy here. You can statically link all of this handy code like a class with constructors and still pass your data in a more crude primitive form to the exported interfaces. Another option is to selectively inline or statically link the constructor so that its code isn't exported as with the rest of the class, but you probably don't want to export the classes as above, if your goal is as much binaries as possible, I want too a lot of gotchas.

Problem 2: Some interfaces return lists (or maps) of an item in the client. I am using std containers for this purpose. But this is also once again compiler dependent (and compiler version also several times).

Here we need to ditch these standard feeds, at least at the exported API level. You can still use them at the convenience library level, which is internally linked.

+2


source







All Articles