Wednesday, December 9, 2020

Wrapping C APIs with C++

If you write in C++ it is nice not to have to switch mental gears back to C, but sometimes it is inevitable because many of the libraries and interfaces we use are written in C. There are many good reasons for this that you can find elsewhere.

I'm currently writing a GUI library targeting the Raspberry Pi frame buffer (no desktop GUI interface loaded) with touchscreen or mouse as the gesture interface. I am building this on top of SDL2, which uses a C interface style, so that's where my examples come from.

The first thing that normally comes up when talking about wrapping C APIs in C++ is how to deal with bear pointers. Of course the thing to do is use a smart pointer, either a std::unique_ptr or a std::shared_ptr depending on the context. Usually for a pointer that the calling program is responsible for deleting a std::unique_ptr is best, but give it some thought. One such bare pointer in the SDL2 libarary is the SDL_Renderer which is created by SDL_CreateRenderer and destroyed by SDL_DestroyRenderer. I usually create a function class to destroy the pointer, and declare a type alias for the std::unique_ptr:

/**
* @brief A functor to destroy an SDL_Renderer
*/
class RendererDestroy {
public:
void operator()(SDL_Renderer *sdlRenderer) {
SDL_DestroyRenderer(sdlRenderer);
}
};
///< An SDL_Renderer unique pointer
using Renderer = std::unique_ptr<SDL_Renderer, RendererDestroy>;

Once this is done you can use these in code and know that when the Renderer goes out of scope, the pointer will be properly destroyed and can't be used again. When you need to pass the actual C style bare pointer to an API call, simply use the get() method of the std::unique_ptr.

Renderer renderer = SDL_CreateRenderer(mSdlWindow.get(), -1,
SDL_RENDERER_ACCELERATED | SDL_RENDERER_TARGETTEXTURE);
if (renderer) {
SDL_SetRenderDrawBlendMode(renderer.get(), SDL_BLENDMODE_BLEND);
} else {
mErrorCode = RoseErrorCode::SDL_RENDERER_CREATE;
std::cerr << "Could not create renderer: " << SDL_GetError() << '\n';
}

But that isn't all you can do to wrap up the API. Sometimes you wish to set a property on a pointer, use it within a scope and then remove it restoring the original state. This can involve a bunch of tedious temporary variables and ensuring calls with the correct variables are made in the correct sequence. Take this guard class for an SDL Clipping Rectangle for example:

/**
* @class ClipRectangleGuard
* @brief Store the current clip rectangle replacing it with a new clip rectangle. When the object is
* destroyed (by going out of scope) the old clip rectangle is set.
*/
class ClipRectangleGuard {
protected:
Renderer &mRenderer; ///< The renderer to which the clip rectangles are set.
SDL_Rect mOldClip{}; ///< The old clip rectangle

public:
ClipRectangleGuard() = delete;
ClipRectangleGuard(ClipRectangleGuard&) = delete;

/**
* @brief Set the old clip rectangle back on the renderer when destroyed.
*/
~ClipRectangleGuard() {
SDL_RenderSetClipRect(mRenderer.get(), &mOldClip);
}

/**
* @brief Constructor. Store the current clip rectangle and set the new one.
* @param renderer The renderer to set the clip rectangles on.
* @param clip The new clip rectangle.
*/
ClipRectangleGuard(Renderer& renderer, const SDL_Rect &clip) : mRenderer(renderer) {
SDL_RenderGetClipRect(mRenderer.get(), &mOldClip);
SDL_RenderSetClipRect(mRenderer.get(), &clip);
}

/**
* @brief Constructor. Store the current clip rectangle and set the new one.
* @param renderer The renderer to set the clip rectangles on.
* @param x X co-ordinate of the new clip rectangle.
* @param y Y co-ordinate of the new clip rectangle.
* @param w Width of the new clip rectangle.
* @param h Height of the new clip rectangle.
*/
ClipRectangleGuard(Renderer& renderer, int x, int y, int w, int h)
: ClipRectangleGuard(renderer, SDL_Rect{x,y,w,h}) {}
};

This is used to set a new Clipping Rectangle, while saving the current value, for the remainder of the scope. The original value is restored as soon as the guard is destroyed.

void ScrollArea::draw(Renderer &renderer, SizeInt size, PositionInt position) {
auto screenPos = getScreenPosition();
ClipRectangleGuard clipRectangleGuard(renderer, screenPos.x(), screenPos.y(), mSize.width(), mSize.height());
Container::draw(renderer, SizeInt{}, mScrollOffset);
}

These are two steps that can help to make calling C style APIs much less error prone by getting the details correct once, and then using C++ idiom from that point on.

No comments:

Post a Comment