Future proofing library APIs
6th
October 2018
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.