Wrapping C API calls, improving the “fit RAII”.

Introduction

In previous articles I’ve described several idioms of wrapping C APIs such as classic RAII, slim RAII and fat RAII. Then I’ve looked at an industry quality wrapper. In this article I’m revisiting and improving this “fit RAII” version, that is suitable for larger projects.

Code

cpp_util_lib/raii_with_invalid_value.h

First let’s create a class template to deal with resources that have a invalid value. For this simple example is not strictly necessary, because we’ll only use it once with FILE *.

It’s a RAII class with the main purpose of closing the handle to clean up the resource on destructor, if it does not have the invalid value.

Other similar templates can be imagined for situations like where there is no invalid value and we need an extra bool, or for the case where additional data is required to clean up the resource.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#pragma once
namespace cpp_util
{
  template<typename Traits>
  // requires Traits defines
  // - a handle type - the type of the handle
  // - a function static void close_handle(handle) noexcept - the function to
  //   close the handle
  // - and an invalid_value constant - the special value indicating "no handle"
  class raii_with_invalid_value
  {
  public:
    using handle = typename Traits::handle;

  private:
    handle h_;

  public:
    raii_with_invalid_value() noexcept :
      h_{ Traits::invalid_value }
    {
    }

    explicit raii_with_invalid_value(handle h) noexcept :
      h_{ h }
    {
    }

    ~raii_with_invalid_value()
    {
      if (is_valid())
      {
        Traits::close_handle(h_);
      }
    }

    raii_with_invalid_value(const raii_with_invalid_value &) = delete;
    raii_with_invalid_value & operator=(const raii_with_invalid_value &) = delete;

    raii_with_invalid_value(raii_with_invalid_value && other) noexcept :
      h_{ other.h_ }
    {
      other.h_ = Traits::invalid_value;
    }

    raii_with_invalid_value & operator=(raii_with_invalid_value && other) noexcept
    {
      handle tmp = other.h_;
      other.h_ = Traits::invalid_value;
      if (is_valid())
      {
        Traits::close_handle(h_);
      }
      h_ = tmp;
      return *this;
    }

    bool is_valid() const noexcept
    {
      return h_ != Traits::invalid_value;
    }

    explicit operator bool() const noexcept
    {
      return is_valid();
    }

    handle get() const noexcept
    {
      return h_;
    }

    handle & handle_reference() noexcept
    {
      return h_;
    }

    handle * handle_pointer() noexcept
    {
      return &h_;
    }

    void reset(handle h = Traits::invalid_value) noexcept
    {
      if (is_valid())
      {
        Traits::close_handle(h_);
      }
      h_ = h;
    }

    handle release() noexcept
    {
      handle tmp = h_;
      h_ = Traits::invalid_value;
      return tmp;
    }
  };
}

The constructor taking a handle is explicit so that we don’t accidentally take the ownership of the handle.

The class is moveable and has a default constructor which allows for scenarios where it’s default created without a handle value and the handle is provided later. The destructor then has to check for a valid handle before cleaning the resource.

The class is not copyable because the destructor would be called twice for both the original and the copy. This does not make sense for most resources e.g. it makes sense to copy the file contents, but not the file handle for independent usage. Such a copy for a file handle is either not possible or possible but undesirable most of the time.

The get, reset and release methods create some similarities in usage to smart pointers.

handle_reference and handle_pointer are provided for direct access to the handle. Such access is a bit dangerous, but sometimes useful (hence the longer names), less dangerous than using naked handles for C APIs which e.g. use the return value for errors and provide created handles via a pointer to handle argument.

The operator bool checks for is_valid(), it is explicit, limiting it’s use e.g. to if tests.

All the methods are noexcept either explicitly or implicitly, which is useful when doing low level interactions with C code.

Why use raii_with_invalid_value instead of std::unique_ptr?

Unlike std::unique_ptr it handles the case where the invalid value is not nullptr. It provides the is_valid() to check for validity, which checks against the correct invalid value.

operator* and operator->, provided by std::unique_ptr, but not raii_with_invalid_value, don’t usually make sense for handles. Handles are sometimes opaque types or they are not even pointers (such as the handle type for POSIX open which is int). Instead raii_with_invalid_value provides additional functions such as handle_pointer that are useful when dealing with handles as explained above.

Equality is not implemented, because it does not make sense. The handles are often opaque and each valid handle has a different value, so the only equal values will be those of invalid handles. Equality is linked to copying. The standard implemented equality for std::unique_ptr. That is arguable a design mistake in std::unique_ptr. Equality there does not make sense for the same reason, only nullptr pointers will compare equal. Note that equality makes sense for std::shared_ptr, which has copy, it answers the question “do these two pointers point to the same thing?”, which is possible for std::shared_ptr.

Another reason is that because of the traits mechanism it gives an option to have different handle wrapper types even when all the characteristics are the same: same handle type, same invalid value, same closing function. E.g. HANDLE (also SC_HANDLE) is used for many APIs in Windows, not always meaning the same thing (files, threads, events, semaphores etc.)

But fundamentally it’s semantics. While you might sometimes just about use std::unique_ptr, raii_with_invalid_value is better suited tool to deal with much more of the usual variation in handle behaviour in C APIs and refer to all such handle wrappers by the same name, leaving std::unique_ptr for the role of managing single owned heap allocated memory.

cpp_util_lib/handle_arg.h

Second let’s create a class template that we can use as a function argument, for when the function accepts either a raw handle or the RAII class owning the handle.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#pragma once
namespace cpp_util
{
  template<typename RaiiClass>
  class handle_arg
  {
    using handle = typename RaiiClass::handle;

    handle h_;

  public:
    handle_arg(handle h) noexcept :
      h_{ h }
    {
    }

    handle_arg(const RaiiClass & raii) noexcept :
      h_{ raii.get() }
    {
    }

    handle_arg(RaiiClass &&) noexcept = delete;

    operator handle() const noexcept
    {
      return h_;
    }
  };
}

This class is a bit like std::string_view that can be used as an argument for a function that accepts either a string literal or a std::string, but unlike std::string_view that can construct from temporary std::string, I made the choice that it’s consructor does not accept a temporary RAII class, because that’s the safest option as it’s harder to have dangling reference to a temporary that’s out of scope, with the minor inconvenience that one can’t do read(open(...), ...); on a single line using the file functions below, instead two lines will be required.

This handle_arg helps because the options for functions that need a handle are:

  • provide two overloads, one that takes a raw handle and another one that takes a RAII class reference
  • call .get() each time on the RAII class to pass it to a C++ function that only takes the raw handle

Note that the RAII class raii_with_invalid_value does not provide an implicit conversion to the raw handle because that creates the risk that it’s used to call the function that closes the handle. Similarly std::unique_ptr does not convert to the raw pointer, to avoid accidental delete some_unique_ptr;, one has to call .get() instead. Similarly std::string does not convert to char *, in that case to avoid occasional ambiguity with operator[], one has to call .c_str(); instead.

cstdio_lib/file_raii.h

Given raii_with_invalid_value, we can now easily create file_raii, a wrapper for FILE *. It’s single responsibility is to ensure that a valid FILE * is closed using fclose precisely once. Similarly file_arg uses handle_arg.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#pragma once

#include "../cpp_util_lib/raii_with_invalid_value.h"
#include "../cpp_util_lib/handle_arg.h"

#include <cstdio>

namespace cstdio
{
  namespace detail
  {
    struct file_raii_traits
    {
      using handle = FILE *;

      static constexpr auto invalid_value = nullptr;

      static void close_handle(handle h) noexcept
      {
        static_cast<void>(std::fclose(h));
      }
    };
  }

  using file_raii = cpp_util::raii_with_invalid_value<detail::file_raii_traits>;
  using file_arg = cpp_util::handle_arg<file_raii>;
}

NOTE: for the cases where the invalid value involves a reinterpret_cast, as it’s the case in Windows for INVALID_HANDLE_VALUE, you might need to use static inline auto invalid_value = ... instead of the syntax above that uses constexpr. Reason is constexpr does not like reinterpret_cast.

cstdio_lib/error.h

These are just some functions to handle errors by raising exceptions. It turns out that there are recurring patterns of error handling. A better implementation would use std::system_error instead of std::runtime_error.

1
2
3
4
5
6
7
8
#pragma once

namespace cstdio::error
{
  void throw_errno(const char * function_name, int errno_value);
  void throw_errno(const char * function_name);
  void throw_failed(const char * function_name);
}

cstdio_lib/error.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "error.h"

#include <cstring>
#include <stdexcept>
#include <string>

namespace cstdio::error
{
  void throw_errno(const char * function_name, int errno_value)
  {
    std::string message = "Function ";
    message += function_name;
    message += " failed. Error: ";
    message += std::strerror(errno_value);

    throw std::runtime_error(message);
  }

  void throw_errno(const char * function_name)
  {
    throw_errno(function_name, errno);
  }

  void throw_failed(const char * function_name)
  {
    std::string message = "Function ";
    message += function_name;
    message += " failed";

    throw std::runtime_error(message);
  }
}

cstdio_lib/file.h

Here is the basic API wrapping level. It consists of functions grouped in a namespace. Their single responsibility it to be thin wrappers that deal with error handling.

It is not the traditional OOP approach, which surprises some, but it has it’s advantages.

For input parameters most require a file_arg which means they can be used with a file_raii instance, but also with a non-owning FILE * like stdout.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pragma once

#include "file_raii.h"

#include <cstddef>

namespace cstdio::file
{
  file_raii open(const char * file_name, const char * mode);
  size_t read(file_arg h, char * buffer, size_t size);
  void write(file_arg h, const char * buffer, size_t size);
  bool is_eof(file_arg h);
  void close(file_raii & x);
}

cstdio_lib/file.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include "file.h"

#include "error.h"

namespace cstdio::file
{
  file_raii open(const char * file_name, const char * mode)
  {
    file_raii result{ std::fopen(file_name, mode) };
    if (!result.is_valid())
    {
      error::throw_errno("fopen");
    }
    return result;
  }

  size_t read(file_arg h, char * buffer, size_t size)
  {
    size_t read_count{ std::fread(buffer, 1, size, h) };
    if ((read_count != size) && ferror(h))
    {
      error::throw_errno("fread");
    }
    return read_count;
  }

  void write(file_arg h, const char * buffer, size_t size)
  {
    size_t write_count{ std::fwrite(buffer , 1, size, h) };
    if (write_count != size)
    {
      error::throw_errno("fwrite");
    }
  }

  bool is_eof(file_arg h)
  {
    return (0 != std::feof(h));
  }

  void close(file_raii & x)
  {
    int result = std::fclose(x.release());
    if (result != 0)
    {
      error::throw_failed("fclose");
    }
  }
}

app_lib/file_util.h

1
2
3
4
5
6
7
8
#pragma once

namespace app_lib::file_util
{
  void copy_file(
    const char * src_file_name,
    const char * dst_file_name);
}

app_lib/file_util.cpp

copy_file is build on top of the basic API wrapping level. The whole point of this idiom is to make easy to write functions like this correctly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "file_util.h"

#include "../cstdio_lib/file.h"

namespace app_lib::file_util
{
  void copy_file(
    const char * src_file_name,
    const char * dst_file_name)
  {
    auto src = cstdio::file::open(src_file_name, "rb");
    auto dst = cstdio::file::open(dst_file_name, "wb");
    char buffer[1024];

    do
    {
      size_t read_count = cstdio::file::read(src, buffer, sizeof(buffer));

      if (read_count > 0)
      {
        cstdio::file::write(dst, buffer, read_count);
      }
    } while ( ! cstdio::file::is_eof(src));

    cstdio::file::close(dst);
  }
}

cpp_util_lib/console.h

The function here is not strictly necessary for this simple example, but useful if you’re writing more than one executable.

It’s responsibility is to handle exceptions at a boundary (e.g. main in this case) over which exceptions should not be thrown.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#pragma once

#include <iostream>

namespace cpp_util::console
{
  template<typename Fn>
  int main_wrap(Fn fn)
  {
    try
    {
      fn();
      std::cout << "\nOK\n";
      return 0;
    }
    catch(const std::exception & e)
    {
      std::cerr << "\nException: " << e.what() << "\nFAILED\n";
      return 1;
    }
  }
}

app/main.cpp

The single responsibility of main is to provide the entry point into the application.

1
2
3
4
5
6
7
8
#include "../cpp_util_lib/console.h"
#include "../app_lib/file_util.h"

int main() {
  return cpp_util::console::main_wrap([]() {
    app_lib::file_util::copy_file("src.bin", "dst.bin");
  });
}

Single responsibility

In this idiom example I tried to show how to code in such a way that each unit (set of .h and .cpp files) has a clear responsibility. That is important for testability, reuse.

Checking return code for fclose

Some functions that free resources cannot fail in a correctly written program, on correct hardware. E.g. delete will succeed (assuming that it’s called correctly, once for memory allocated with new).

Some functions that free resources return an error code. Of this category some only have a nominal error code, in practice with correct inputs they will succeed (they are similar to delete above).

However fclose is different. Documentation might not be clear, but often inside fclose the file is flushed to the disk, which might fail (especially if the file is remote).

The strategy here is:

  • We explicitly call close at the end of copy_file. If fclose fails, close throws and copy_file trows as well, indicating failure.
  • Also close sets the FILE * to nullptr to ensure fclose will not be called again in the destructor. To do that, it takes the argument as a file_raii reference instead of a file_arg.
  • The return value of fclose in file_raii_traits is ignored. It is called from destructors and move assignment, that is not the place to throw. Another option would have been to call std::terminate, like in std::thread, but this is probably not appropriate.

Argument validation

One observation with the approach above is that there is no extensive runtime validation. Interface contracts are sometimes narrow. E.g. the functions in the cstdio::file namespace that receive a FILE * expect it to be valid, they do not validate it (not even checking for nullptr, though we could have put some debug build only asserts).

The point is to address automatically some concerns, not to prevent logical errors at runtime cost.

Summary

At a superficial look this approach looks more verbose than the classic RAII, slim RAII, fat RAII and even pure C approaches. It totals 325 lines of code.

However it will pay off on various scenarios that are likely in a large project where you will write more code that reuses existing code in new combinations.