3

Say you're designing a C API, and one of your big concerns is ABI stability (it's going to be deployed as a shared library, or whatever).

You have a function exported

int foo_bar(int a, int b, int* c);

And you feel that someday, you may need to add one or more parameters to this function, but you don't know which ones yet.

You come up with the following solution

int foo_bar(int a, int b, int* c, int ver,...) ;
#define FOO_BAR_V1 0xA9520F00

And document that ver is reserved, and must always be set to FOO_BAR_V1, and additional parameters were ignored.

You then plan that, when you nail down the additional parameter needed, say, an output buffer, you'll do the following

int foo_bar(int a, int b, int* c, int ver,/* char* buff, int buffSize, int* outSize*/...) ;
#define FOO_BAR_V2 (FOO_BAR_V1 + 1)

After typing this all out, it seems like there's not much point to this over just having two exported functions (is there? Other than not having to come up with a new name), so I guess I'll also ask if anyone knows of any libraries that do this, or something similar?

  • The standard stdio.h functions along with a number of the unistd.h functions make use of variadic input. (printf, fprintf, sscanf, etc.. and execl, et.al., the ulimit function`...) There is nothing wrong, or uncommon about it, as long as you have some yet to be determined number of parameters of the same type. There is a standard for handling variadic input and it isn't much to become familiar with. – David C. Rankin Aug 6 '16 at 5:31
  • 8
    This is exactly what variadic functions are not designed for. – user22815 Aug 6 '16 at 5:44
  • 2
    sqlite.org/capi3ref.html - sqlite "versions" some of their APIs. If the docs are good enough, works well. Please don't abuse variadic functions like that. – Mat Aug 6 '16 at 6:11
  • 1
    That approach kills quite a bit of type checking support and is IMHO a result of sloppy checks for feature completeness. – tofro Aug 20 '16 at 9:10
4

Perhaps the safest and most flexible way to preserve backward compatibility in an API is to put the parameters into a struct and pass either the plain struct by value, or a pointer to the struct (by reference). The latter (pass a pointer) is necessary for use in a shared library for backwards compatibility at the ABI level (as is the necessity of preserving the ordering and types of the existing struct members), but the former will maybe be slightly cleaner and safer for an API that requires re-compilation for every change.

In future evolutions of the API (and maybe ABI) it is then possible to add new, but optional, parameters.

However if new parameters are somehow not optional then I think the only safe and sane solution is to define a new function (i.e. with a new name) which provides the new interface.

In some cases it can be possible to use the pre-processor, perhaps along with weak references (sometimes called "weak linking") in the linker, to rename calls to use the new function and its new API, and at the same time preserve ABI backward compatibility (e.g. when the type of a parameter changes). NetBSD has used this to evolve standard library and system call interfaces such that old binaries can continue to work with new kernels and new libraries, but within their original limitations, while new binaries can exploit the full capabilities of a new API. E.g. the new underlying type for time_t is wider than it once was, so the new libc has to offer both the old 32-bit time() and a new 64-bit one, and yet NetBSD only publishes one API for time_t time(time_t *). See the NetBSD libc README for an in-depth discussion (under the heading Old versions of library routines).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.