For APIs that tackle a lot of disparate concerns from interop to performance to versioning for backwards binary compatibility and so forth, I find it substantially less painful in the long run (and have the scars to show for it) to omit such convenience and utility functions from the "raw", exported API itself in favor of a smaller legacy to maintain.
The last thing I want to do is find that I need to extend the versioning and ABI struggles to functions exported for the sole purpose of making the library more convenient to use. It's difficult enough to maintain such library interface designs for decades without trying to make them as easy to use as possible in their "native/raw" form by adding (and exporting) all kinds of additional functions to those interfaces for the sake of convenience. Instead I favor minimalism there and maximum breathing room to minimize the probability of deprecation and all sorts of extra hurdles imposed on backwards compatibility at the cost of convenience to users of said library in its "raw" form.
Wrapper on Top
That said, I'm not suggesting to make the library easier to implement/maintain at the cost of actually being more difficult to use. What I suggest in such cases is to create a wrapper sort of library on top which tackles convenient usage in a target language as its fundamental concern. In many cases where I favor that kind of wrapper approach, I actually discourage using the "raw", say, C API in favor of the wrappers.
And emphasis on target language, because if your API is a C API, for example, as might be required for FFIs or JNIs and things of this sort, and you're using it from other languages like Java, it's never going to be all that convenient, or idiomatic, or safe to use in its native C form no matter how hard you try, while any effort to do so could add serious grief in the long run as far as maintenance of legacy code. So this wrapper library tackling convenient, safe, idiomatic usage is something I'd suggest to write in Java in that case, while keeping your C library as minimalist and as easy to implement/maintain/version as possible so as to minimize the difficulties of, say, versioning interfaces and preserving backwards compatibility.
Maintaining the Wrappers
This might raise the question, wouldn't we still have to maintain the wrappers on top the exported API? Yes we do to some extent, but they don't have backwards ABI-level compatibility concerns since they can be internally linked to the user's binaries. So we have much more breathing room to change those wrappers without breaking stuff built ten years ago against older versions of the wrappers.
With the "raw" API that exports its functions across dylibs/shared libs, unless backwards compatibility is not a concern, you have to keep whatever functions in there in their original form for as long as backwards compatibility is preserved, even that silly function that someone added 12 years ago that is causing so much grief to keep in there because its signature requires that the implementation utilizes global variables. So there I find it much less painful to keep that exported, versioned API as tiny and as simplistic as possible with little to no concern about convenient usage.
Performance
Now specifically when performance concerns are high with an API, I would also suggest, sometimes, to even almost deliberately design very inconvenient-to-use functions in order to minimize their probability of requiring changes, because otherwise the performance concerns could multiply the reasons for change. As a specific example, consider this function in the OpenGL API:
// glVertexAttribPointer — define an array of generic vertex attribute data
void glVertexAttribPointer(GLuint index,
GLint size,
GLenum type,
GLboolean normalized,
GLsizei stride,
const GLvoid* pointer);
Gross! First they've abandoned type safety in favor of just accepting a void pointer to the vertex data with a type
parameter that specifies what type of data/format we're using (ex: GL_UNSIGNED_INT_2_10_10_10_REV
).
Second there's a normalized parameter just for fixed-point fields that specifies whether their values should be normalized when accessed by the shader or not, which isn't even relevant for all types.
There's also a stride parameter which allows people to pass in data that isn't tightly packed and potentially interleaved with other data, but that also throws a wrench into type safety and makes the function just skip over bits and bytes to read to get from one field to the next.
And from the standpoint of general SE, it's arguably a poorly-designed function, and not even convenient to use from a C point of view. But I'd say it's quite well-designed when you consider the performance and backwards compatibility requirements of OpenGL, because it has few reasons to ever need to be changed. If they want to support a new vertex data format, like half-floats, they don't need to change the function signature or introduce new functions. They can merely introduce a new constant, like GL_HALF_FLOAT
. If it is more efficient to normalize such data as required in the function rather than outside, that base is covered with the normalized
parameter. If they find it more efficient in some cases to pass all this vertex data in interleaved, as opposed to a packed format, then the stride
parameter handles that case without requiring changes or new functions to be introduced.
So it's designed in a way so as to minimize the probability of requiring future changes while happily sacrificing convenience for the user, which imposes the fewest obstacles when it comes to maintaining backwards compatibility. So in cases where both performance and backwards compatibility are sufficiently critical requirements (two very tricky ones to adhere to, and especially when combined together), I actually recommend designing functions this way so as to minimize the probability of requiring changes at the cost of convenience, safety, whatever you have to sacrifice to minimize the probability of API changes (which can be enormously costly and constantly tempt us to abandon backwards compatibility), because you can regain whatever you sacrificed in order to achieve this quality by merely wrapping the exported functions, and those wrappers are much cheaper to change than the exported API.