utf8.h rewrites the C string API in UTF-8 terms, with visible gaps
📚 single header utf8 string functions for C and C++
At a glance
- What is it?
- sheredom/utf8.h is a single header file that reimplements the C string API for UTF-8 text, counting codepoints where string.h counts bytes. It fits C codebases that measure user text, and it leaves Unicode case folding and buffer repair undocumented.
- Who is it for?
- One header file is the right shape for a codebase that already owns its string handling and needs codepoint counts without pulling a Unicode library into the build. It is the wrong choice when case folding has to match a full Unicode database, or when a C++ 20 build must hand char8_t* buffers to code expecting std::string.
- Can I use it commercially?
- Yes. Unlicense is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 7 days ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Text handling in C counts bytes, this header counts codepoints
The string.h functions you already have work on bytes, and they are honest about that. strlen reports how much memory a string occupies, not how many letters a reader sees. An accented French word, a Japanese name or an emoji occupies several bytes, so every length, search and comparison built on those primitives inherits a unit that does not match the text.
utf8.h exists to fix that unit rather than the storage format. It keeps the familiar names, swaps the str prefix for utf8, and redefines what a count means. `utf8len` returns the number of utf8 codepoints in a utf8 string rather than the number of bytes, and the same substitution runs through the search, span and copy functions. For a developer writing log formatting, config parsing or filename handling in C, that is a small edit with a large effect on how text gets sliced.
Its audience is a programmer who already lives in the C string API and wants codepoint semantics without adding a Unicode library to the build. Someone looking for a string type with iteration and encoding awareness is asking for something else entirely.
Eight string functions lose constexpr and five get no counterpart
Matching the C API is the stated design goal, and the mapping table shows exactly where the match stops. Twenty-one functions from string.h get a utf8 prefixed name, and most carry two ticks: complete, and usable in a C++14 constexpr context. 8 of them are complete but lose the constexpr tick, including utf8cat, utf8cpy, utf8ncat, utf8ncpy, utf8dup, utf8ndup, utf8pbrk and utf8rchr. Those are the routines that touch or allocate memory at run time, which is why a compile time string operation cannot call them.
Five more rows are empty. strcoll, strfry, strsep, strtok and strxfrm have no utf8 counterpart at all, and a blank cell there reads as a deliberate refusal rather than an oversight. Locale aware collation, the character shuffling in strfry, tokenisation and transform all depend on behaviour that byte reinterpretation cannot reproduce. The cost lands on the caller: code that reaches for strtok to split a utf8 line has to fall back to the C function and drop back to byte semantics at exactly that point.
3 more entries arrive from strings.h, namely utf8casecmp, utf8ncasecmp and utf8casestr. Their status differs from every row above, in a way the mapping itself flags.
The design notes rename bytes to codepoints, the function entries do not
The Design section makes a blanket claim about the documentation: anywhere the string.h or strings.h documentation refers to bytes, the wording was changed to utf8 codepoints. A close reading of the entries shows the rewrite never landed everywhere. `utf8size` still describes its result as the number of bytes in the utf8 string, and utf8pbrk still describes matching any byte in the accept string.
Those survivors are not careless wording, they describe the real shape of the API, but they leave the unit of each function something you look up rather than assume:
size_t utf8len(const void *str);size_t utf8size(const void *str);Only for a pure ASCII string do `utf8size_lazy` and utf8len agree, and utf8size always runs one higher because it counts the null terminating byte. The bounded variant mixes the units further:
size_t utf8nlen(const void *str, size_t n);utf8nlen accepts a limit in bytes and answers with a count of codepoints, so the argument and the return value on the same call measure different things.
utf8ncat can split a codepoint where utf8ncpy cuts clean
The two bounded copy routines disagree about what happens when a byte limit lands in the middle of a multibyte sequence.
void *utf8ncat(void *dst, const void *src, size_t n);utf8ncat writes at most n+1 bytes, and its entry states plainly that it can produce an invalid utf8 string if n falls partway through a utf8 codepoint.
void *utf8ncpy(void *dst, const void *src, size_t n);utf8ncpy takes the same n and behaves differently: if n falls partway through a codepoint, or if dst has no room for a null terminator, the final string is cut short to preserve utf8 validity.
Same prefix, same parameter shape, opposite safety rule. A call site that clamps user text to a fixed byte budget through the append form can leave a dangling lead byte at the end of the buffer, and the void* return carries no flag that says so. The copy form hands back a valid string, but it offers no way to ask whether anything was dropped either, so neither function reports truncation through its result.
Validation names the offending codepoint, repair takes a replacement value
`utf8valid` returns 0 on success or the position of the invalid utf8 codepoint on failure, which turns validation into a check you can resume rather than a bare boolean.
void *utf8valid(const void *str);utf8nvalid adds a byte bound to the same check, and utf8makevalid takes a replacement codepoint so a malformed buffer can be repaired in place rather than discarded.
int utf8makevalid(void *str, utf8_int32_t replacement)A catch sits inside that reference. The API section stops at the utf8makevalid line, so the one function that rewrites bad bytes has a declaration and no described behaviour. Nothing states what becomes of the replaced codepoints, or whether the repaired result is shorter, longer or the same size.
5 of the sixteen entries in the utf8 specific block carry a documented signature and behaviour. Ten more, from utf8codepoint and utf8rcodepoint through utf8uprcodepoint, have no entry at all, and utf8makevalid gets only the bare signature above. Anyone planning a decoder, a case converter or a codepoint appender works from names and tick marks.
Nine case helpers are flagged incomplete and left without entries
9 functions carry a struck through tick in the complete column: utf8casecmp, utf8ncasecmp and utf8casestr from strings.h, then utf8isupper, utf8islower, utf8lwr, utf8upr, utf8lwrcodepoint and utf8uprcodepoint. The cross out is the project's own signal that the port is unfinished, yet all 9 keep their C++14 constexpr mark in the column beside it.
Only 3 of them have documentation, and it says no more than that they compare case insensitively. Nothing states which characters fold, whether the mapping covers ASCII or full Unicode, or whether a locale is consulted, so two builds configured with different locales are described neither as differing nor as agreeing. The six conversion helpers have no entry at all, which leaves utf8lwr and utf8upr as names carrying a promise and no contract behind it. For text containing accented Latin, Greek or Cyrillic, the documentation gives no basis for trusting the comparison, and the incomplete mark is the only warning on offer.
Copying utf8.h into a project takes one include line
Dropping the file into a source tree is the whole procedure:
#include "utf8.h"The supported platforms are Linux, macOS and Windows, and the supported compilers are gcc, clang, MSVC's cl.exe and clang-cl.exe: 3 operating systems and 4 compilers, with no portability shim needed on any of them. The repository holds 7 top level entries, namely .clang-format, .github/, .gitignore, LICENSE, README.md, test/ and utf8.h itself. There is no package manifest, no build script and no documented command for running test/, so CI wiring stays with you. Badges point at a GitHub Actions workflow named CMake and at AppVeyor, yet no clone line, cmake invocation or compiler flag list appears anywhere in the usage notes.
The repository's only tag is named pre-ai and carries the date 2026-05-22, and the last push was on 2026-10-06. Vendoring the header raises no paperwork question, because utf8.h carries the Unlicense, a public domain dedication rather than a notice you have to reproduce.
char8_t in C++20 and the wide-character route to UTF-8
One design decision carries a direct cost: in C++ 20 the signatures use char8_t* instead of char*. A project on C++14 today will find its call sites no longer type match the moment the standard moves to C++ 20, because a const char* literal is a different type from const char8_t*, and the documented API contains no conversion helper to bridge the two. String arguments arrive as `const void *`, which keeps the same code usable from both languages but leaves the C++ compiler with nothing to check.
The alternative approach is to stop measuring bytes altogether. C++ 20 std::u8string and the C11 <uchar.h> family, including mbrtoc32 and its reverse conversion, produce explicit codepoints rather than reinterpreting a byte buffer, and they pair with the standard case conversion and character classification tables. That route has a cost of its own: those paths carry a locale or encoding label, they need conversion code at the boundary where utf8.h simply reads the buffer, and they do not hand you the string.h shaped vocabulary that existing C code is written against. utf8.h wins when the surrounding code is already C style and the counts are the only thing wrong; the standard routes win when the surrounding code is C++ style and a string type is expected.
Editorial conclusion
One header file is the right shape for a codebase that already owns its string handling and needs codepoint counts without pulling a Unicode library into the build. It is the wrong choice when case folding has to match a full Unicode database, or when a C++ 20 build must hand char8_t* buffers to code expecting std::string. Before adopting, check that the helpers you plan to call have a documented entry, because ten of the sixteen utf8 specific functions have none, and because utf8ncat can leave a buffer split mid codepoint.
Frequently asked questions
How does sheredom/utf8.h count characters in a string?
utf8len returns the number of utf8 codepoints in a utf8 string, excluding the null terminating byte, where strlen would return bytes. If you need the byte count instead, utf8size includes the null terminating byte and utf8size_lazy excludes it.
Which platforms and compilers does sheredom/utf8.h support?
The supported platforms are Linux, macOS and Windows, and the supported compilers are gcc, clang, MSVC's cl.exe and clang-cl.exe. There is no package manager entry; usage notes say only to include the header in your code.
Does sheredom/utf8.h check whether a string is valid UTF-8?
Yes. utf8valid returns 0 on success or the position of the invalid utf8 codepoint on failure, and utf8nvalid applies the same check to at most n bytes. utf8makevalid takes a replacement codepoint, though the API reference stops at that signature without describing what the repair does.
Does sheredom/utf8.h work in C++ 20?
It is written for it: the design notes state that in C++ 20 the API uses char8_t* instead of char*. Call sites written against char* need a cast of their own, because no conversion helper appears in the documented API and string arguments are taken as const void *.
Is sheredom/utf8.h still being worked on?
The last push was on 2026-10-06. The repository carries a single tag, pre-ai, dated 2026-05-22.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sheredom-utf8-h)Was this useful?