Cached at:
05/16/26, 03:37 AM
# Simdutf Can Now Be Used Without libc++ or libc++abi
Source: [https://mitchellh.com/writing/simdutf-no-libcxx](https://mitchellh.com/writing/simdutf-no-libcxx)
As of[this PR](https://github.com/simdutf/simdutf/pull/959#top),[simdutf](https://github.com/simdutf/simdutf)can be used without libc\+\+ or libc\+\+abi[1](https://mitchellh.com/writing/simdutf-no-libcxx#user-content-fn-1)\.
Simdutf was the final remaining libc\+\+ dependency in[libghostty\-vt](https://libghostty.tip.ghostty.org/)[2](https://mitchellh.com/writing/simdutf-no-libcxx#user-content-fn-2)\. After[updating Ghostty to use this new simdutf build](https://github.com/ghostty-org/ghostty/pull/12291), we were able to remove libc\+\+ and libc\+\+abi completely from our dependencies\.
## The Benefits of Not Depending on libc\+\+
Not depending on libc\+\+ makes a library more portable \(embedded, WebAssembly, freestanding environments\), simplifies cross\-compilation \(no target\-specific C\+\+ standard library needed\), reduces binary size, and can simplify static linking\.
As a general purpose, low\-level library, simdutf should be as portable and flexible as possible\. If a downstream consumer can easily use libc\+\+, great\! But if they can't, they shouldn't be blocked from using simdutf since it fundamentally doesn't need it\.
## libc\+\+ vs libc\+\+abi
There are two parts to extricating a program from libc\+\+\.
First,`libc\+\+`is the C\+\+ standard library, which provides things like`std::vector`,`std::string`, etc\. If you import`<vector\>`or even the libc C\+\+ fallbacks like`<cstring\>`, you're depending on libc\+\+\.
The sneakier part is`libc\+\+abi`, which provides the C\+\+ ABI, including things like exception handling, virtual function tables, RTTI, etc\. If you use any C\+\+ features that require these, you're depending on libc\+\+abi, even if you don't import any C\+\+ standard library headers*at all*\.
For example, the minimal C\+\+ program below depends on libc\+\+abi because it uses function\-local static variables, which require thread\-safe initialization that is part of the C\+\+ ABI:
```
struct Implementation {
int version;
};
const Implementation& get_impl() {
static const Implementation impl{1};
return impl;
}
int main() {
return get_impl().version;
}
```
## Extricating simdutf from libc\+\+
Let's talk about`libc\+\+`first \(not the ABI\)\.
simdutf is a C\+\+ library that makes heavy use of the latest and greatest C\+\+ features and though I don't know him personally,[Daniel Lemire](https://lemire.me/en/)seems to really love utilizing C\+\+ features to their fullest extent\. So, simdutf is very much a C\+\+ project\.
For there to be any chance that my changes would be accepted, I had to make sure that the project could continue to use C\+\+ features without it being a headache\.
### STL Usage
The approach I decided to take was to introduce a[`stl\_compat\.h`header](https://github.com/mitchellh/simdutf/blob/nolibcxx/include/simdutf/internal/stl_compat.h)that centralizes all of the C\+\+ standard library types\. In normal libc\+\+ mode, everything in`stl\_compat\.h`is a simple include or minimal alias to the corresponding C\+\+ standard library type with no runtime overhead\.
In`NO\_LIBCXX`mode,`stl\_compat\.h`provides its own implementations of the C\+\+ types that simdutf uses, but only just enough for them to be compatible with what simdutf needs\. For example,`stl\_compat\.h`provides its own implementation of`std::pair`\.
As a result, the changes required end up mostly looking like this throughout the diff:
```
-std::pair<const char *, char32_t *>
+internal::pair<const char *, char32_t *>
arm_convert_latin1_to_utf32(const char *buf, size_t len,
char32_t *utf32_output) {
```
### ABI Compatibility
My goal was to retain as much ABI compatibility as possible\. Some of the[public ABI](https://en.wikipedia.org/wiki/Application_binary_interface)expose C\+\+ types such as`std::string`, so in those scenarios, ABI had to be broken\. Otherwise, it is fully retained\.
Given`SIMDUTF\_NO\_LIBCXX`is a new feature that applies changes to the compilation unit, I felt that it was acceptable to break ABI*only when this flag is present*\. In the existing case where the`NO\_LIBCXX`flag is not present, the ABI is fully retained and simdutf can be updated without breaking ABI for existing users\.
I was pleasantly surprised to find that the ABI breakage was very minimal, and only applied to a handful of diagnostic functions \(e\.g\. to get the name of the active implementation\) and helpers to work with other C\+\+ types \(e\.g\. text encoding`std::string`\)\. Since by definition someone using`SIMDUTF\_NO\_LIBCXX`is not interested in libc\+\+, these ABI breakages felt like a feature rather than a bug\.
## Extricating simdutf from libc\+\+abi
This task was significantly more complex\.
The main problem was that`libc\+\+abi`dependencies don't usually show up as obvious source\-level includes\. They show up because the compiler quietly emits calls into the C\+\+ ABI runtime for ordinary\-looking language features\. To detect these, I had to write a script that decompiles the object files and looks for symbols such as`\_\_cxa\_guard\_acquire`\.
The biggest offender in simdutf was the runtime dispatch layer\. The original code relied heavily on function\-local static variables\. In C\+\+, those locals are guarded by thread\-safe initialization helpers such as`\_\_cxa\_guard\_acquire`and`\_\_cxa\_guard\_release`, which are provided by the C\+\+ ABI runtime\. So even though the code didn't mention libc\+\+abi anywhere, the compiled object still depended on it\. The fix for this is to use translation\-unit static variables instead of function\-local statics in`NO\_LIBCXX`mode\.
```
#if SIMDUTF_IMPLEMENTATION_ICELAKE
#ifdef SIMDUTF_NO_LIBCXX
static const icelake::implementation icelake_singleton{};
#endif
static const icelake::implementation *get_icelake_singleton() {
#ifdef SIMDUTF_NO_LIBCXX
return &icelake_singleton;
#else
static const icelake::implementation icelake_singleton{};
return &icelake_singleton;
#endif
}
#endif
```
Next, simdutf models each backend as a subclass of an abstract`implementation`interface\. That design can stay, but abstract\-class vtables still reference`\_\_cxa\_pure\_virtual`for impossible\-to\-call pure virtual entries\. In`SIMDUTF\_NO\_LIBCXX`mode I chose to provide a tiny local shim and ensure the runtime never actually reaches it\. I marked this as weak so that the symbol can be overridden by the C\+\+ ABI if it is present\.
```
#ifdef SIMDUTF_NO_LIBCXX
// The abstract implementation vtable still carries pure-virtual slots even
// though correct dispatch never reaches them in this build mode. Provide the
// narrowest possible ABI shim so stricter no-libcxx objects do not require
// libc++abi just for this unreachable hook. Keep it weak so a toolchain's real
// libc++abi definition wins if one is linked in anyway.
extern "C" SIMDUTF_WEAK [[noreturn]] void __cxa_pure_virtual() noexcept {
__builtin_trap();
}
#endif
```
Finally, I wrote a script to audit the build with`\-fno\-exceptions`and`\-fno\-rtti`and check that no symbols such as`\_\_cxa\_guard\_\*`,`\_\_gxx\_personality`,`\_\_cxa\_throw`,`typeinfo`, or`\_\_dynamic\_cast`ever show up\. This was added to the simdutf CI to ensure that we never accidentally reintroduce libc\+\+abi dependencies in the future for`NO\_LIBCXX`builds\.
## Validation
### Internal
simdutf is a correctness and performance critical library, so I had to be sure my changes didn't impact either of those\. I modified the pre\-existing test and benchmark suites to run in both`NO\_LIBCXX`and normal modes and ensured that all tests passed and benchmarks were unaffected\.
The important part about this is that I committed the changes necessary to ensure that the`NO\_LIBCXX`mode is compatible with the existing test and benchmark suites\. This means that future changes to simdutf can continue to validate both\.
### External: Ghostty
Next, I updated Ghostty to use the new simdutf from my fork, updated our builds to use`SIMDUTF\_NO\_LIBCXX`, and added our own suite of tests to verify our artifacts have no dependencies on libc\+\+ or libc\+\+abi\.
Ghostty has a robust set of tests that verify our UTF\-8 decoding behavior \(especially invalid inputs\)\. And Ghostty has a built\-in benchmark suite that tests our UTF\-8 throughput in a variety of scenarios\. I ran all the Ghostty tests and benchmarks and verified that they all passed and that our UTF\-8 performance was unaffected, as expected\.
## Pull Request
Getting something working and getting something merged are two different things\.
I know too well as a maintainer myself the disparity between "this works" and "this can be merged\." I know the challenges of verifying someone's work and being confident in maintaining it going forward\. I know the challenge of opening a large PR and not having clarity of why it is being opened\. And I know the burden of recent AI slop\.
So I put in the effort I would want from an all\-star contributor and tried to be that person for the simdutf maintainers\.
First, I reviewed the entire diff \(yes, all ~3,000 lines of it\)\. Then I reviewed it again\. I re\-read the entire diff by hand three or four times\. I made multiple changes based on things I probably would've commented on myself, even if functionally it was fine\.
Next, I hand\-wrote a detailed PR description that explains the motivation, the approach, the limitations, and the validation\. I wanted to make sure that the maintainers were aware of the details but also how much thought I put into the details\.
Finally, I disclosed that I did use AI to assist me in writing the code\. But I made it clear that I manually reviewed everything, I didn't use AI in writing any of the PR description or comments, and that I was capable and comfortable as a human to defend and modify any proposed changes\.
Ironically, the full diff took me about 2 hours to put together, but the extra validation work and PR preparation took me about 3 hours\. I spent more time on the human boundary than the code itself, as we should out of respect for the effort maintainers put into their projects\.
## Final Status
The[simdutf PR](https://github.com/simdutf/simdutf/pull/959)is still under review\. Initial feedback is positive, and I'm open to any and all changes requested of me\. There's a possibility that the maintainers will not want to merge it, and that's okay, too\.
If you want to use simdutf without libc\+\+ or libc\+\+abi, you can[use my fork](https://github.com/mitchellh/simdutf/tree/nolibcxx)in the meantime\. The same instructions for producing an amalgamated single\-file plus header build apply\. When building the C\+\+ and including the header, just make sure to define`SIMDUTF\_NO\_LIBCXX`to get the no\-libc\+\+ version of the library\.
The[Ghostty PR](https://github.com/ghostty-org/ghostty/pull/12291)is now merged\. As such,`libghostty\-vt`no longer depends on libc\+\+ or libc\+\+abi for SIMD builds\.
1. libc\+\+ is the C\+\+ standard library \(e\.g\.`std::vector`,`std::string`, etc\.\) and libc\+\+abi is the C\+\+ ABI library \(e\.g\. exception handling, RTTI, etc\.\)\.[↩](https://mitchellh.com/writing/simdutf-no-libcxx#user-content-fnref-1)
2. Note that with SIMD disabled, libghostty\-vt has always had zero dependencies, not even libc\. It is a fully freestanding library\.[↩](https://mitchellh.com/writing/simdutf-no-libcxx#user-content-fnref-2)