{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/mitchellh-com/2026-04-15-simdutf-can-now-be-used-without-libc-or-libc-abi/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "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).\n\nSimdutf 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.\n\n## The Benefits of Not Depending on libc++\n\nNot 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.\n\nAs 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.\n\n## libc++ vs libc++abi\n\nThere are two parts to extricating a program from libc++.\n\nFirst, `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++.\n\nThe 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*.\n\nFor 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:\n\n```\nstruct Implementation {\n    int version;\n};\n\nconst Implementation& get_impl() {\n    static const Implementation impl{1};\n    return impl;\n}\n\nint main() {\n    return get_impl().version;\n}\n```\n\n## Extricating simdutf from libc++\n\nLet's talk about `libc++` first (not the ABI).\n\nsimdutf 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.\n\nFor 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.\n\n### STL Usage\n\nThe 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.\n\nIn `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`.\n\nAs a result, the changes required end up mostly looking like this throughout the diff:\n\n```\n-std::pair<const char *, char32_t *>\n+internal::pair<const char *, char32_t *>\narm_convert_latin1_to_utf32(const char *buf, size_t len,\n                            char32_t *utf32_output) {\n```\n\n### ABI Compatibility\n\nMy 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.\n\nGiven `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.\n\nI 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.\n\n## Extricating simdutf from libc++abi\n\nThis task was significantly more complex.\n\nThe 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`.\n\nThe 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.\n\n```\n#if SIMDUTF_IMPLEMENTATION_ICELAKE\n  #ifdef SIMDUTF_NO_LIBCXX\nstatic const icelake::implementation icelake_singleton{};\n  #endif\nstatic const icelake::implementation *get_icelake_singleton() {\n  #ifdef SIMDUTF_NO_LIBCXX\n  return &icelake_singleton;\n  #else\n  static const icelake::implementation icelake_singleton{};\n  return &icelake_singleton;\n  #endif\n}\n#endif\n```\n\nNext, 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.\n\n```\n#ifdef SIMDUTF_NO_LIBCXX\n// The abstract implementation vtable still carries pure-virtual slots even\n// though correct dispatch never reaches them in this build mode. Provide the\n// narrowest possible ABI shim so stricter no-libcxx objects do not require\n// libc++abi just for this unreachable hook. Keep it weak so a toolchain's real\n// libc++abi definition wins if one is linked in anyway.\nextern \"C\" SIMDUTF_WEAK [[noreturn]] void __cxa_pure_virtual() noexcept {\n  __builtin_trap();\n}\n#endif\n```\n\nFinally, 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.\n\n## Validation\n\n### Internal\n\nsimdutf 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.\n\nThe 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.\n\n### External: Ghostty\n\nNext, 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.\n\nGhostty 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.\n\n## Pull Request\n\nGetting something working and getting something merged are two different things.\n\nI 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.\n\nSo I put in the effort I would want from an all-star contributor and tried to be that person for the simdutf maintainers.\n\nFirst, 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.\n\nNext, 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.\n\nFinally, 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.\n\nIronically, 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.\n\n## Final Status\n\nThe [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.\n\nIf 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.\n\nThe [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.\n\n## Footnotes\n\n1. 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)\n\n2. 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)\n",
  "authors": [],
  "captured_at": "2026-09-08T10:44:40.429509+00:00",
  "category": "programming",
  "content_html": "<p>As of <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/simdutf/simdutf/pull/959#top\">this PR</a>, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/simdutf/simdutf\">simdutf</a> can be used without libc++ or libc++abi <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://mitchellh.com/writing/simdutf-no-libcxx#user-content-fn-1\">1</a>.</p>\n<p>Simdutf was the final remaining libc++ dependency in <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://libghostty.tip.ghostty.org\">libghostty-vt</a><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://mitchellh.com/writing/simdutf-no-libcxx#user-content-fn-2\">2</a>. After <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/ghostty-org/ghostty/pull/12291\">updating Ghostty to use this new simdutf build</a>, we were able to remove libc++ and libc++abi completely from our dependencies.</p>\n<h2>The Benefits of Not Depending on libc++</h2>\n<p>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.</p>\n<p>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.</p>\n<h2>libc++ vs libc++abi</h2>\n<p>There are two parts to extricating a program from libc++.</p>\n<p>First, <code>libc++</code> is the C++ standard library, which provides things like <code>std::vector</code>, <code>std::string</code>, etc. If you import <code>&lt;vector&gt;</code> or even the libc C++ fallbacks like <code>&lt;cstring&gt;</code>, you're depending on libc++.</p>\n<p>The sneakier part is <code>libc++abi</code>, 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 <em>at all</em>.</p>\n<p>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:</p>\n<pre><code><span class=\"code-snippet\" data-language=\"CSS\"><span class=\"syntax-source syntax-css\"><span class=\"syntax-meta syntax-selector syntax-css\">struct Implementation </span><span class=\"syntax-meta syntax-property-list syntax-css\"><span class=\"syntax-punctuation syntax-section syntax-property-list syntax-css\">{</span>\n    <span class=\"syntax-meta syntax-property-name syntax-css\">int</span> <span class=\"syntax-meta syntax-property-name syntax-css\">version</span>;\n</span><span class=\"syntax-punctuation syntax-section syntax-property-list syntax-css\">}</span>;\n\n<span class=\"syntax-meta syntax-selector syntax-css\">const Implementation&amp; get_impl(</span>) <span class=\"syntax-meta syntax-property-list syntax-css\"><span class=\"syntax-punctuation syntax-section syntax-property-list syntax-css\">{</span>\n    <span class=\"syntax-meta syntax-property-name syntax-css\">static</span> <span class=\"syntax-meta syntax-property-name syntax-css\">const</span> I<span class=\"syntax-meta syntax-property-name syntax-css\">mplementation</span> <span class=\"syntax-meta syntax-property-name syntax-css\">impl</span>{1</span><span class=\"syntax-punctuation syntax-section syntax-property-list syntax-css\">}</span>;\n<span class=\"syntax-meta syntax-selector syntax-css\">    return impl;\n}\n\nint <span class=\"syntax-entity syntax-name syntax-tag syntax-css\">main</span>(</span>) <span class=\"syntax-meta syntax-property-list syntax-css\"><span class=\"syntax-punctuation syntax-section syntax-property-list syntax-css\">{</span>\n    <span class=\"syntax-meta syntax-property-name syntax-css\">return</span> <span class=\"syntax-meta syntax-property-name syntax-css\">get</span>_<span class=\"syntax-meta syntax-property-name syntax-css\">impl</span>().<span class=\"syntax-meta syntax-property-name syntax-css\">version</span>;\n</span><span class=\"syntax-punctuation syntax-section syntax-property-list syntax-css\">}</span>\n</span></span></code></pre>\n<h2>Extricating simdutf from libc++</h2>\n<p>Let's talk about <code>libc++</code> first (not the ABI).</p>\n<p>simdutf is a C++ library that makes heavy use of the latest and greatest C++ features and though I don't know him personally, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://lemire.me/en/\">Daniel Lemire</a> seems to really love utilizing C++ features to their fullest extent. So, simdutf is very much a C++ project.</p>\n<p>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.</p>\n<h3>STL Usage</h3>\n<p>The approach I decided to take was to introduce a <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/mitchellh/simdutf/blob/nolibcxx/include/simdutf/internal/stl_compat.h\"><code>stl_compat.h</code> header</a> that centralizes all of the C++ standard library types. In normal libc++ mode, everything in <code>stl_compat.h</code> is a simple include or minimal alias to the corresponding C++ standard library type with no runtime overhead.</p>\n<p>In <code>NO_LIBCXX</code> mode, <code>stl_compat.h</code> 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, <code>stl_compat.h</code> provides its own implementation of <code>std::pair</code>.</p>\n<p>As a result, the changes required end up mostly looking like this throughout the diff:</p>\n<pre><code><span class=\"code-snippet\">-std::pair&lt;const char *, char32_t *&gt;\n+internal::pair&lt;const char *, char32_t *&gt;\narm_convert_latin1_to_utf32(const char *buf, size_t len,\n                            char32_t *utf32_output) {\n</span></code></pre>\n<h3>ABI Compatibility</h3>\n<p>My goal was to retain as much ABI compatibility as possible. Some of the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://en.wikipedia.org/wiki/Application_binary_interface\">public ABI</a> expose C++ types such as <code>std::string</code>, so in those scenarios, ABI had to be broken. Otherwise, it is fully retained.</p>\n<p>Given <code>SIMDUTF_NO_LIBCXX</code> is a new feature that applies changes to the compilation unit, I felt that it was acceptable to break ABI <em>only when this flag is present</em>. In the existing case where the <code>NO_LIBCXX</code> flag is not present, the ABI is fully retained and simdutf can be updated without breaking ABI for existing users.</p>\n<p>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 <code>std::string</code>). Since by definition someone using <code>SIMDUTF_NO_LIBCXX</code> is not interested in libc++, these ABI breakages felt like a feature rather than a bug.</p>\n<h2>Extricating simdutf from libc++abi</h2>\n<p>This task was significantly more complex.</p>\n<p>The main problem was that <code>libc++abi</code> 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 <code>__cxa_guard_acquire</code>.</p>\n<p>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 <code>__cxa_guard_acquire</code> and <code>__cxa_guard_release</code>, 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 <code>NO_LIBCXX</code> mode.</p>\n<pre><code><span class=\"code-snippet\">#if SIMDUTF_IMPLEMENTATION_ICELAKE\n  #ifdef SIMDUTF_NO_LIBCXX\nstatic const icelake::implementation icelake_singleton{};\n  #endif\nstatic const icelake::implementation *get_icelake_singleton() {\n  #ifdef SIMDUTF_NO_LIBCXX\n  return &amp;icelake_singleton;\n  #else\n  static const icelake::implementation icelake_singleton{};\n  return &amp;icelake_singleton;\n  #endif\n}\n#endif\n</span></code></pre>\n<p>Next, simdutf models each backend as a subclass of an abstract <code>implementation</code> interface. That design can stay, but abstract-class vtables still reference <code>__cxa_pure_virtual</code> for impossible-to-call pure virtual entries. In <code>SIMDUTF_NO_LIBCXX</code> 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.</p>\n<pre><code><span class=\"code-snippet\">#ifdef SIMDUTF_NO_LIBCXX\n// The abstract implementation vtable still carries pure-virtual slots even\n// though correct dispatch never reaches them in this build mode. Provide the\n// narrowest possible ABI shim so stricter no-libcxx objects do not require\n// libc++abi just for this unreachable hook. Keep it weak so a toolchain&#39;s real\n// libc++abi definition wins if one is linked in anyway.\nextern &quot;C&quot; SIMDUTF_WEAK [[noreturn]] void __cxa_pure_virtual() noexcept {\n  __builtin_trap();\n}\n#endif\n</span></code></pre>\n<p>Finally, I wrote a script to audit the build with <code>-fno-exceptions</code> and <code>-fno-rtti</code> and check that no symbols such as <code>__cxa_guard_*</code>, <code>__gxx_personality</code>, <code>__cxa_throw</code>, <code>typeinfo</code>, or <code>__dynamic_cast</code> ever show up. This was added to the simdutf CI to ensure that we never accidentally reintroduce libc++abi dependencies in the future for <code>NO_LIBCXX</code> builds.</p>\n<h2>Validation</h2>\n<h3>Internal</h3>\n<p>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 <code>NO_LIBCXX</code> and normal modes and ensured that all tests passed and benchmarks were unaffected.</p>\n<p>The important part about this is that I committed the changes necessary to ensure that the <code>NO_LIBCXX</code> mode is compatible with the existing test and benchmark suites. This means that future changes to simdutf can continue to validate both.</p>\n<h3>External: Ghostty</h3>\n<p>Next, I updated Ghostty to use the new simdutf from my fork, updated our builds to use <code>SIMDUTF_NO_LIBCXX</code>, and added our own suite of tests to verify our artifacts have no dependencies on libc++ or libc++abi.</p>\n<p>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.</p>\n<h2>Pull Request</h2>\n<p>Getting something working and getting something merged are two different things.</p>\n<p>I know too well as a maintainer myself the disparity between &quot;this works&quot; and &quot;this can be merged.&quot; 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.</p>\n<p>So I put in the effort I would want from an all-star contributor and tried to be that person for the simdutf maintainers.</p>\n<p>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.</p>\n<p>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.</p>\n<p>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.</p>\n<p>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.</p>\n<h2>Final Status</h2>\n<p>The <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/simdutf/simdutf/pull/959\">simdutf PR</a> 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.</p>\n<p>If you want to use simdutf without libc++ or libc++abi, you can <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/mitchellh/simdutf/tree/nolibcxx\">use my fork</a> 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 <code>SIMDUTF_NO_LIBCXX</code> to get the no-libc++ version of the library.</p>\n<p>The <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/ghostty-org/ghostty/pull/12291\">Ghostty PR</a> is now merged. As such, <code>libghostty-vt</code> no longer depends on libc++ or libc++abi for SIMD builds.</p>\n<h2>Footnotes</h2>\n<ol>\n<li>\n<p>libc++ is the C++ standard library (e.g. <code>std::vector</code>, <code>std::string</code>, etc.) and libc++abi is the C++ ABI library (e.g. exception handling, RTTI, etc.). <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://mitchellh.com/writing/simdutf-no-libcxx#user-content-fnref-1\">↩</a></p>\n</li>\n<li>\n<p>Note that with SIMD disabled, libghostty-vt has always had zero dependencies, not even libc. It is a fully freestanding library. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://mitchellh.com/writing/simdutf-no-libcxx#user-content-fnref-2\">↩</a></p>\n</li>\n</ol>\n",
  "content_markdown": "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).\n\nSimdutf 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.\n\n## The Benefits of Not Depending on libc++\n\nNot 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.\n\nAs 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.\n\n## libc++ vs libc++abi\n\nThere are two parts to extricating a program from libc++.\n\nFirst, `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++.\n\nThe 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*.\n\nFor 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:\n\n```\nstruct Implementation {\n    int version;\n};\n\nconst Implementation& get_impl() {\n    static const Implementation impl{1};\n    return impl;\n}\n\nint main() {\n    return get_impl().version;\n}\n```\n\n## Extricating simdutf from libc++\n\nLet's talk about `libc++` first (not the ABI).\n\nsimdutf 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.\n\nFor 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.\n\n### STL Usage\n\nThe 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.\n\nIn `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`.\n\nAs a result, the changes required end up mostly looking like this throughout the diff:\n\n```\n-std::pair<const char *, char32_t *>\n+internal::pair<const char *, char32_t *>\narm_convert_latin1_to_utf32(const char *buf, size_t len,\n                            char32_t *utf32_output) {\n```\n\n### ABI Compatibility\n\nMy 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.\n\nGiven `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.\n\nI 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.\n\n## Extricating simdutf from libc++abi\n\nThis task was significantly more complex.\n\nThe 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`.\n\nThe 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.\n\n```\n#if SIMDUTF_IMPLEMENTATION_ICELAKE\n  #ifdef SIMDUTF_NO_LIBCXX\nstatic const icelake::implementation icelake_singleton{};\n  #endif\nstatic const icelake::implementation *get_icelake_singleton() {\n  #ifdef SIMDUTF_NO_LIBCXX\n  return &icelake_singleton;\n  #else\n  static const icelake::implementation icelake_singleton{};\n  return &icelake_singleton;\n  #endif\n}\n#endif\n```\n\nNext, 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.\n\n```\n#ifdef SIMDUTF_NO_LIBCXX\n// The abstract implementation vtable still carries pure-virtual slots even\n// though correct dispatch never reaches them in this build mode. Provide the\n// narrowest possible ABI shim so stricter no-libcxx objects do not require\n// libc++abi just for this unreachable hook. Keep it weak so a toolchain's real\n// libc++abi definition wins if one is linked in anyway.\nextern \"C\" SIMDUTF_WEAK [[noreturn]] void __cxa_pure_virtual() noexcept {\n  __builtin_trap();\n}\n#endif\n```\n\nFinally, 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.\n\n## Validation\n\n### Internal\n\nsimdutf 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.\n\nThe 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.\n\n### External: Ghostty\n\nNext, 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.\n\nGhostty 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.\n\n## Pull Request\n\nGetting something working and getting something merged are two different things.\n\nI 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.\n\nSo I put in the effort I would want from an all-star contributor and tried to be that person for the simdutf maintainers.\n\nFirst, 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.\n\nNext, 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.\n\nFinally, 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.\n\nIronically, 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.\n\n## Final Status\n\nThe [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.\n\nIf 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.\n\nThe [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.\n\n## Footnotes\n\n1. 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)\n\n2. 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)\n",
  "dateCreated": "2026-09-08T10:44:40.429509+00:00",
  "date_created": "2026-04-15T00:00:00+00:00",
  "date_modified": "2026-04-15T00:00:00+00:00",
  "date_published": "2026-04-15T00:00:00+00:00",
  "description": "As of this PR, simdutf can be used without libc++ or libc++abi 1. 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…",
  "external_url": "https://mitchellh.com/writing/simdutf-no-libcxx",
  "git": {
    "commit": "ebb0941c5c06a2ab2377342d607535262a3e969e",
    "path": "items/mitchellh-com/2026/04/2026-04-15-simdutf-can-now-be-used-without-libc-or-libc-abi.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/ebb0941c5c06a2ab2377342d607535262a3e969e/items/mitchellh-com/2026/04/2026-04-15-simdutf-can-now-be-used-without-libc-or-libc-abi.md"
  },
  "id": "urn:aggr:item:9208cebd5b5530c449da3a8870b2355c9c7b5723",
  "isBasedOn": {
    "@id": "https://mitchellh.com/writing/simdutf-no-libcxx"
  },
  "mainEntity": {
    "@id": "https://mitchellh.com/writing/simdutf-no-libcxx",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/mitchellh-com/2026-04-15-simdutf-can-now-be-used-without-libc-or-libc-abi/#webpage"
    },
    "dateModified": "2026-04-15T00:00:00+00:00",
    "datePublished": "2026-04-15T00:00:00+00:00",
    "headline": "Simdutf Can Now Be Used Without libc++ or libc++abi",
    "timeRequired": "PT8M",
    "url": "https://mitchellh.com/writing/simdutf-no-libcxx",
    "wordCount": 1650
  },
  "name": "Archived snapshot: Simdutf Can Now Be Used Without libc++ or libc++abi",
  "preview": null,
  "reading_minutes": 8,
  "replicated_at": null,
  "source": "mitchellh-com",
  "source_name": "Mitchell Hashimoto",
  "summary": null,
  "tags": [],
  "temporalCoverage": "2026-09-08T10:44:40.429509+00:00",
  "title": "Simdutf Can Now Be Used Without libc++ or libc++abi",
  "url": "https://aggr.aymericbeaumet.com/items/mitchellh-com/2026-04-15-simdutf-can-now-be-used-without-libc-or-libc-abi/",
  "word_count": 1650
}