Wrapping C Libraries With Smart Pointers
Published on January 31st 2019

In this post, we will look at how smart pointers can be used to implement a RAII interface to C libraries. This is the basis for my SDL2 wrapper. Using this method, we don't have to write wrapper classes.

SDL uses the common C design pattern of opaque types, in which the implementation details of a type are hidden through use of forward declaration. Consider SDL_Renderer, which is declared in SDL_render.h like so:

struct SDL_Renderer;
typedef struct SDL_Renderer SDL_Renderer;

The actual definition of struct SDL_Renderer is omitted from the header files. Alongside the above are a number of function declarations, most notably the "constructor" and "destructor":

extern DECLSPEC SDL_Renderer * SDLCALL
	SDL_CreateRenderer(SDL_Window * window, int index, Uint32 flags);
extern DECLSPEC void SDLCALL SDL_DestroyRenderer(SDL_Renderer * renderer);

This gives C libraries an "OOP feel", as seen here:

SDL_Renderer r = SDL_CreateRenderer(w, -1, SDL_RENDERER_ACCELERATED);
SDL_SetRenderDrawColor(r, 255, 0, 0, 255);
SDL_RenderClear(r);
SDL_SetRenderDrawColor(r, 0, 0, 255, 255);
SDL_Rect rect(0, 0, 100, 50);
SDL_RenderFillRect(r, &rect);
SDL_RenderPresent(r);

The example above is incomplete, as the programmer forgot to call SDL_DestroyRenderer, resulting in a resource leak. Thankfully, in C++ land we have language support for dealing with such pitfalls. Enter smart pointers:

This can be generalized using templates, like so:

template<typename Creator, typename Destructor, typename... Arguments>
auto Create(Creator c, Destructor d, Arguments&&... args) {
	auto r = c(::std::forward<Arguments>(args)...);
	if (!r) {
		throw ::std::system_error(
			errno, ::std::generic_category(), SDL_GetError());
	}
	return ::std::unique_ptr<::std::decay_t<decltype(*r)>, decltype(d)>(r, d);
}

using Renderer =
	std::unique_ptr<SDL_Renderer, decltype(&SDL_DestroyRenderer)>;

inline Renderer CreateRenderer(SDL_Window* window, int index, Uint32 flags) {
	return Create(
		SDL_CreateRenderer, SDL_DestroyRenderer, window, index, flags);
}

and the above example rewritten like so:

auto r = CreateRenderer(w, -1, SDL_RENDERER_ACCELERATED);
SDL_SetRenderDrawColor(r.get(), 255, 0, 0, 255);
SDL_RenderClear(r.get());
SDL_SetRenderDrawColor(r.get(), 0, 0, 255, 255);
SDL_Rect rect(0, 0, 100, 50);
SDL_RenderFillRect(r.get(), &rect);
SDL_RenderPresent(r.get());

We no longer have to worry about calling SDL_DestroyRenderer, as it is called automatically when r goes out of scope.

A word on unique_ptr

As unique_ptr models singular ownership, it does not implement a copy constructor. In other words, it cannot be passed by value. If you need to model multiple ownership, use the reference counting shared_ptr instead. It can easily and efficiently be constructed from a std::unique_ptr like so:

auto sharedPtr = make_shared_from(std::move(uniquePtr));

The reverse is not possible. It is generally a bad idea to design your API to require a specific pointer implementation, and specifically std::unique_ptr unless ownership transfer is implied.

Enjoyed this post? Got feedback? Consider following @Flygsand for the latest updates.

This post, excluding sample code, is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License. See the example directory for code licensing.

© Martin Häger, 2018 - 2023.

v1.5.17 (changelog). Built with GraphQL, React and Semantic UI.