Reworking string management in WebKit’s GStreamer code

When you work on WebKit’s GStreamer code, you end up dealing with strings a lot. GStreamer and GLib APIs use C strings everywhere: element names, property values, caps descriptions, SDP fields… The list is long. The problem was that we were handling all those strings in a somewhat inconsistent way, and the interaction between C strings and WebKit’s own string types was not as clean as it should be.

This is why I started a meta-effort (Bug 289787) to rework how we manage strings in the GStreamer code of WebKit. The outcome is mainly two new classes: CStringView and GMallocString. In this post I want to explain what they are and why they were needed.

The problem

WebKit has its own string types, like WTF::String and WTF::CString, which are designed around WebKit’s internal needs. They handle reference counting, different encodings, and all kind of operations. But when you interface with GLib and GStreamer, you receive char* pointers from C APIs that are typically UTF-8 encoded and null-terminated. Converting back and forth between these and WebKit strings was often done in ad-hoc ways, sometimes creating unnecessary copies and sometimes not being very explicit about ownership.

Another issue was encoding. WebKit internally works with different encodings (Latin1, UTF-16), but all the strings coming from GLib and GStreamer are UTF-8. There was no compile-time enforcement of this distinction, so it was easy to mix things up.

At the beginning, using StringView to handle these C strings was kind of enough. It had a rawCharacters method that allowed us to just wrap the C string pointer and work with it, even though there was no way to enforce encoding correctness. It was not ideal, but it worked. Then WebKit began to move towards using spans and that is when the real problems popped up: spans carry a pointer and a size, but the size was not accounting for the null terminator. So when you wrapped a null-terminated C string into a span, the null terminator was left out, and if any code down the line relied on it being there, you had memory issues. This was the moment it became clear that we needed a proper type that understood null termination as a first-class concept.

CStringView: a non-owning view of null-terminated UTF-8 strings

My first version of CStringView was actually a bigger class. I had added many string operations as member functions: startsWith, endsWith, contains, find, case-insensitive comparisons… It felt natural to me as a developer — you go to the header of a class and you see everything you can do with it. But during the review of PR #51619, Darin Adler made a very good point: CStringView should only handle what makes it special, which is the null termination guarantee. All the other string operations should work on spans, because they do not depend on null termination and should not be duplicated for every string type. If you have a CStringView named view, you just write contains(view.span(), ...) instead of view.contains(...). This way, the same functions work for any span of characters, and CStringView stays small and focused. I was initially skeptical, but after writing the code I have to say he was right.

So the final CStringView is a lightweight, non-owning view over a null-terminated UTF-8 string. Think of it like std::string_view but with two key differences: it guarantees null termination and it works with char8_t instead of char.

class CStringView final {
    // ...
    static CStringView unsafeFromUTF8(const char* string);
    static CStringView fromUTF8(std::span<const char8_t> spanWithNullTerminator);

    const char* utf8() const;
    size_t lengthInBytes() const;
    std::span<const char8_t> span() const;
    std::span<const char8_t> spanIncludingNullTerminator() const;
    bool isEmpty() const;
    bool isNull() const;
};

Using char8_t is important because it prevents mixing encodings at compile time. If you try to pass a char* to something expecting char8_t, the compiler will complain. This is exactly what we wanted: making encoding mismatches a compilation error rather than a runtime bug. This requirement came from Geoffrey Garen during earlier reviews to ensure we were not mixing Latin1 or UTF-16 with this class. You might have noticed the unsafeFromUTF8 factory method: the unsafe prefix is a WebKit convention for APIs that deal with raw pointers without a known size. Since a bare const char* has no size information, we have to trust the caller and compute the length with strlen, which is inherently unsafe. The safe counterpart, fromUTF8, takes a std::span where the size is already known.

CStringView also has a utf8() method that returns a plain const char* for interfacing with C APIs that expect it. The class is designed to sit on the stack (heap allocation is forbidden) and it carries no overhead since it is essentially just a span.

The work on making the span-based operations available in StringCommon (Bug 299946) involved improving the templates to support char8_t spans, so that operations like startsWith, endsWith, contains, find and case-insensitive comparisons work seamlessly. Many of these methods were only templated for Latin1Character and needed some tweaking to also accept char8_t. On top of that, some of these functions accepted different character types for their different parameters, but we wanted to ensure that char8_t was checked at compile time so that it could not be mixed with any other encoding-incompatible type. For example, comparing a UTF-8 code unit with a Latin1 character byte by byte is simply incorrect for non-ASCII characters, so the compiler should prevent you from doing it in the first place. These utility functions can be found in StringCommon.h and StdLibExtras.h.

After the class was ready, I went through the GStreamer code and increased its use (Bug 299443). This touched a significant amount of files across the WebRTC, media player, and media stream code, replacing raw const char* juggling with proper CStringView usage.

If you ever need to convert a CStringView into a String, for example to supply it to a WebCore API, the proper way is straightforward: String newString = cStringView.span();. The String constructor that takes a std::span<const char8_t> handles the UTF-8 conversion correctly without needing any intermediate wrappers.

For the opposite direction, converting a String into a CStringView, the path goes through CString: you first call .utf8() on the String to get a CString (which performs the encoding conversion), and then wrap it with CStringView::unsafeFromUTF8(cString.data()). It is important to keep the CString alive as long as the CStringView is in use, since CStringView does not own the data. That said, this kind of conversion is not common in practice. If you already have a String, the logical step is to keep using it all the way to the end of the WebKit API onion and, when you finally need a const char* for a C API, just call myString.utf8().data().

GMallocString: an owning wrapper for GLib-allocated strings

The second piece was GMallocString (Bug 303909).

Many GLib and GStreamer APIs return newly allocated strings that you must free with g_free(). Before GMallocString, we had GUniquePtr<char> for this, but it was just a raw pointer wrapper with no string-specific functionality. You could not easily compare it, convert it to a WebKit string, or even get its length without calling strlen yourself.

GMallocString solves this by wrapping an owned, g_malloc-allocated, null-terminated UTF-8 string. It can adopt strings in several ways:

// From a raw char* (takes ownership):
auto str = GMallocString::unsafeAdoptFromUTF8(g_strdup("hello"));

// From a GUniquePtr<char>:
GUniquePtr<char> gstr(g_strdup("world"));
auto str2 = GMallocString::unsafeAdoptFromUTF8(WTFMove(gstr));

// Copy from a CStringView:
GMallocString str3(myCStringView);

Once you have a GMallocString, you get the same span(), utf8(), lengthInBytes() interface as CStringView. You can compare them with ==, convert to CStringView with toCStringView(), and use them with safePrintfType for safe logging. The class is move-only (no copies), which is the right semantics for an owning wrapper.

The nice thing is that GMallocString preserves the original null-terminated C string without performing any copy when adopting, and it frees it with g_free() when it goes out of scope. This makes it a perfect fit for the GLib/GStreamer interop pattern where you receive an allocated string and need to use it for a while before discarding it.

The bigger picture

These two classes together cover the two main use cases for C string handling in our code:

  • You do not own the string: use CStringView. No allocations, no copies, just a view. Ideal for parameters, string literals, and temporary references.
  • You own a GLib-allocated string: use GMallocString. It adopts the allocation, provides the same interface, and cleans up when done.

Both enforce UTF-8 encoding through char8_t, both provide span-based access for interacting with the rest of WTF’s string infrastructure, and both support equality comparisons with each other and with ASCIILiteral.

The end result is that the GStreamer code in WebKit is now more explicit about string ownership and encoding, and there are fewer raw char* pointers floating around without clear semantics. I also found and fixed a bug in CString::isEmpty() (Bug 303428) along the way, which was returning size_t instead of bool.

I think the codebase is in a much better shape now in this regard. There is still work to do, but the foundations are there.

Thanks go to my reviewers Darin Adler, Philippe Normand and Adrian Perez de Castro for their patience and thorough reviews, and to Igalia for sponsoring this work.

Leave a Reply

Your email address will not be published. Required fields are marked *