April 7, 2022·5 min read·engineeringcmusllibc

musl 1.2.3 gives qsort callbacks their own context

musl adds qsort_r with an explicit callback argument and makes free preserve errno. Both changes improve the contracts between reusable C code and its callers.

A sorting callback often needs information that is absent from the two elements it compares. Ordinary qsort gives it no parameter for that information. The caller must arrange some other way to supply the context, which complicates reusable code. Rich Felker's musl 1.2.3 announcement, published on April 7, adds qsort_r and identifies the interface as intended for a future POSIX revision.

This is a small library addition with a direct effect on API design. The comparator receives a separate context pointer supplied by the caller. A sorting operation can therefore carry its own comparison policy without asking the surrounding program to establish shared state. The release also makes free preserve errno, another change that reduces interference between a library function and the code around it.

C's compact language has always appealed to me, but much of its usability depends on interfaces outside the language proper. These additions interest me because they make ownership of information more explicit at ordinary library boundaries. They do not require a new abstraction mechanism in the compiler.

Context becomes an argument

The 1.2.3 stdlib header declares qsort_r with the comparison function followed by the caller's context pointer. The comparator itself receives pointers to two elements and then that same context. In this version, the declaration is visible under _GNU_SOURCE or _BSD_SOURCE.

This example sorts integers according to a direction supplied for this particular call. It uses the signature in musl 1.2.3.

#define _GNU_SOURCE
#include <stdlib.h>

static int compare_ints(const void *left, const void *right, void *context)
{
    int a = *(const int *)left;
    int b = *(const int *)right;
    int direction = *(const int *)context;
    return direction * ((a > b) - (a < b));
}

void sort_descending(int *values, size_t count)
{
    int direction = -1;
    qsort_r(values, count, sizeof *values, compare_ints, &direction);
}

The example's comparison expression produces a negative, zero, or positive result without subtracting the input integers. Its context belongs to the enclosing call and remains alive until the sort returns. Nothing in this interface requires the direction to occupy global storage. The context could instead point to a larger structure describing an application's ordering policy.

This is also an API whose exact declaration matters. A reference to a function name alone is insufficient evidence that independently supplied implementations accept the same argument order. For this release, the checked header establishes both the location of the context argument in qsort_r and its position in the callback. A compatibility wrapper must match those types before invoking the function.

The sorting implementation remains bounded

The tagged sorting source implements the operation through __qsort_r and exports qsort_r as a weak alias. Internally, the comparator type includes the context pointer, and helper routines pass that pointer through each comparison. The context therefore follows the operation into its internal sorting steps.

The implementation identifies its algorithm as smoothsort, an adaptive heapsort variant. Its source comment states constant auxiliary memory usage, worst-case running time proportional to n log n, and behaviour close to linear for mostly sorted data. Those are the implementation's documented algorithmic properties. They are not a benchmark comparing musl with another library on a particular workload.

The code also shows a fixed-size temporary buffer used when cycling elements. Larger elements are copied in chunks through that buffer. This detail explains how an interface accepting arbitrary element widths can avoid allocating a temporary object proportional to the entire input. The public addition concerns how the comparator receives its context, while the implementation keeps its existing bounded-storage approach.

I prefer this kind of interface change when it removes a reason to introduce hidden state. C already has the necessary mechanism in a pointer supplied by the caller. Extending the callback contract makes that mechanism available where the ordinary sorting signature leaves it out.

Cleanup must not replace the error

The announcement describes preservation of errno by free as a future POSIX requirement. In practical terms, that affects code which releases memory after some other operation fails. A cleanup call should not replace the error information the caller intends to report.

This does not imply that freeing memory reports an error through errno. The point is preservation of an existing value. Code that interprets errno after an operation still needs that operation's documented failure result before treating the value as meaningful. The change makes a particular cleanup step less intrusive within that error path.

Several fixes in the same release concern failure paths. The announcement reports a null dereference in aligned-allocation functions when the underlying malloc fails. It also fixes a 1.2.2 regression in duplocale when the allocator is replaced. These are situations where library code has to respect an allocation contract even when ordinary allocation does not succeed or the allocator comes from another component.

Thread names reveal a Linux dependency

Among the small extension interfaces is pthread_getname_np. Its 1.2.3 implementation has distinct paths for the calling thread and another thread. The calling thread's name comes through prctl with PR_GET_NAME. For another thread, musl opens that thread's comm file under /proc/self/task/.

The implementation requires a buffer of at least 16 bytes and returns ERANGE for a shorter one. In the proc-file path it strips the trailing newline after a successful read. These details are more useful than interpreting the function name as a promise that retrieving a name has no environmental dependencies. The path used for another thread depends on the process's view of procfs.

While reading another thread's name, the implementation disables cancellation and restores the previous cancellation state after closing the file. It also returns an error number directly when opening or reading the file fails. That structure makes the temporary file descriptor and the error result visible parts of the implementation, even though the caller sees a compact function signature.

The release's other corrections include an out-of-bounds timezone-data read for distant-past timestamps and incorrect TLS-pointer reporting through dl_iterate_phdr. Felker also identifies incorrect rounding in fmaf on all soft-float architectures. Each fix addresses a specific contract that callers can depend on without seeing the implementation. Alongside the new context-bearing sort, these changes make musl 1.2.3 relevant to applications that use existing interfaces as well as code that adopts the new ones.