Mitchell Hashimoto

Simdutf Can Now Be Used Without libc++ or libc++abi

April 15, 2026

As of this PR, simdutf can be used without libc++ or libc++abi1.

Simdutf was the final remaining libc++ dependency in libghostty-vt2. After updating Ghostty to use this new simdutf build, we were able to remove libc++ and libc++abi completely from our dependencies.

Note that at the time of writing this, the upstream simdutf PR is not merged, and I don't know if it will be merged. Initial feedback is positive, but the maintainers may choose not to merge it for any reason. The Ghostty project has already incorporated the no-libc++ simdutf build into our main branch.

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 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 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 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 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 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 is now merged. As such, libghostty-vt no longer depends on libc++ or libc++abi for SIMD builds.

Footnotes

  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.).

  2. Note that with SIMD disabled, libghostty-vt has always had zero dependencies, not even libc. It is a fully freestanding library.