jjrscott

Future proofing library APIs

In 2017 I wrote a small comment on Yann Collet’s blog post entitled The art of designing advanced API which I thought was worth expanding on.

Here’s my original comment:

Given you’re already using lots of abstraction I’d like to suggest just one more: expand on your generic_function and use inline to lock down the main points in the client binary at compile time:

void ZSTD_msgSend(const char *functionName, unsigned libraryVersion, void *returnValue, ...);

ZSTDLIB_API size_t ZSTD_CCtx_setCompressionLevel(ZSTD_CCtx* cctx, unsigned value);

inline size_t ZSTD_CCtx_setCompressionLevel(ZSTD_CCtx* cctx, unsigned value)
{
	size_t returnValue;
	ZSTD_msgSend(__FUNCTION__, ZSTD_VERSION_NUMBER, &returnValue, cctx, value);
	return returnValue;
}

This has the following features:

  • maintains type safety for the user while giving you all the information you need to handle any call that comes in
  • allows you to generate all the interfaces in your favourite scripting language at library release time to maintain type safety on your end
  • allows easy bridging for any other languages
  • the API only actually export one function, so there can’t be a mismatch at that level

What I’m actually doing here is using inline functions to place some jump code directly into the user’s binary, then using a carefully crafted dispatch function which I can guarentee will always be available.

Here’s the implementation that would go with the header in the comment:

void _ZSTD_msgSend(const char *functionName, unsigned libraryVersion, void *returnValue, va_list argsList);
size_t _ZSTD_CCtx_setCompressionLevel_v1(ZSTD_CCtx* cctx, unsigned value);

void ZSTD_msgSend(const char *functionName, unsigned libraryVersion, void *returnValue, ...)
{
    va_list argList;
    va_start(argList, returnValue);
    _ZSTD_msgSend(functionName, libraryVersion, returnValue, argList);
    va_end(argList);
}

void _ZSTD_msgSend(const char *functionName, unsigned libraryVersion, void *returnValue, va_list argsList)
{
    if (0 == strcmp(functionName, "ZSTD_CCtx_setCompressionLevel"))
    {
        ZSTD_CCtx* cctx = va_arg(argsList, ZSTD_CCtx*);
        unsigned value = va_arg(argsList, unsigned);
        size_t *result = returnValue;
        *result = _ZSTD_CCtx_setCompressionLevel_v1(cctx, value);
    }
    else
    {
        printf("%s %u", functionName, libraryVersion);
    }
}

size_t _ZSTD_CCtx_setCompressionLevel_v1(ZSTD_CCtx* cctx, unsigned value)
{
    printf("ZSTD_CCtx_setCompressionLevel(1): %p %i", cctx, value);
    return 0;
}

A few things to note if you’re looking to implement this yourself:

  • This only works in languages that have header files with inline functions or have some other way to inject code into the user’s binary.
  • Because we’re performing runtime dispatch there with inevitably be a performance hit.
  • The example avoids one of the common issues with dispatch functions - return values - by passing it by reference to the dispatch function.