Memory Management

BtcK enables clients to use a different standard library than the one used to build the library itself. For example, the library may be built with GCC, while the client application uses MSVC. As a result, memory allocated by the library with malloc or new cannot be safely deallocated by the client with free and delete, and vice versa.

Single objects that are instances of types provided by BtcK are allocated using a constructor function, BtcK_<type>_New, and deallocated with the corresponding destructor function, BtcK_<type>_Free. See Object Lifetime Management for more details about the lifetime of single objects.

There are two types that BtcK deliberately does not provide: String and Buffer.

ToString

Many types in BtcK provide a function to convert an object to a string. In C, this function is called BtcK_<type>_ToString, while higher-level languages may use special methods such as __str__. For the reasons highlighted above, this function cannot return a char* that must be freed with free.

One approach would be to introduce a dedicated string type, BtcK_String, with data and size members and a corresponding destructor function BtcK_String_Free. The problem with that approach is that language bindings would need to convert from this string type into their own string type using a function like PyUnicode_FromStringAndSize(), which would involve additional memory allocations.

Instead, the _ToString functions do not allocate any memory at all, but write into a buffer that is provided by the caller. They then return the number of characters that would have been written if the buffer had been sufficiently large. This API, which is modelled after snprintf, allows clients to use a static buffer for small strings and only fall back to dynamic allocation when the static buffer is not large enough. It also allows passing NULL as the buffer in order to determine the required buffer length.

For an example of client-side usage, this is how the Python bindings determine the required length and then use that information to allocate and write into a string:

PyObject* tx_string(struct BtcK_Transaction const* tx)
{
    int const length = BtcK_Transaction_ToString(tx, NULL, 0);
    if (length < 0) {
        PyErr_SetNone(PyExc_ValueError);
        return NULL;
    }

    PyObject* ret = PyUnicode_New(length, 127);
    if (ret == NULL) {
        return NULL;
    }

    BtcK_Transaction_ToString(tx, PyUnicode_DATA(ret), length + 1);
    return ret;
}

On the implementation side, if the _ToString function is implemented in terms of snprintf, allowing NULL as the buffer and returning the theoretical length rather than the number of bytes actually written is natural.

If the function is implemented in terms of std::format(), it should write into the buffer with std::format_to_n() (see cppreference) only if the buffer is not nullptr, but should always return std::formatted_size() (see cppreference).

If the function currently constructs a std::string by other means, such as using std::stringstream or by repeatedly calling += or append, it should copy the string into the buffer with std::string::copy() (see cppreference) and then return the result of std::string::size() (see cppreference). With such an implementation and the example code above, the same string will be constructed twice. It is therefore recommended to refactor the implementation using the C++20 format library. Such a refactoring is possible without affecting the API of the function.

ToBytes

Many types in BtcK also provide a function to convert an object to a buffer. In C, this function is called BtcK_<type>_ToBytes, while higher-level languages may use special methods such as __bytes__. Like _ToString, this function cannot return memory that must be deallocated with free. It is also not desirable to introduce a BtcK_Buffer type that would require copying from BtcK’s buffer into the language’s buffer type with a function like PyBytes_FromStringAndSize(), which would result in additional memory allocations.

But unlike _ToString, there is no easy way to predetermine the required size of a buffer. Hence, BtcK uses an approach that is inspired by and compatible with PEP 782. The following extract from the Python bindings shows an example of the client-side usage:

static int write_bytes(void const* bytes, size_t size, void* writer)
{
    return PyBytesWriter_WriteBytes(
        (PyBytesWriter*)writer, bytes, (Py_ssize_t)size);
}

PyObject* tx_bytes(struct BtcK_Transaction const* tx)
{
    PyBytesWriter* writer = PyBytesWriter_Create(0);
    if (BtcK_Transaction_ToBytes(tx, write_bytes, writer) != 0) {
        PyBytesWriter_Discard(writer);
        return NULL;
    }

    return PyBytesWriter_Finish(writer);
}