New define SIMDUTF_NO_LIBCXX to build with no libc++ or libc++abi#959
New define SIMDUTF_NO_LIBCXX to build with no libc++ or libc++abi#959mitchellh wants to merge 1 commit intosimdutf:masterfrom
Conversation
|
@mitchellh Running tests. This looks reasonable to me. |
|
Replaced my commit to address the CI failures, please try again thank you. |
This adds a new define `SIMDUTF_NO_LIBCXX` that can be used to build simdutf without libc++ or libc++abi. This is very useful for environments where these dependencies are undesirable. When SIMDUTF_NO_LIBCXX is present, we strive as much as possible to avoid breaking ABI but some is necessary since some of the public ABI relies on libc++ (e.g. returns `std::string` and such). As such, the presence of the define should be considered a separate ABI for all intents and purposes. > [!IMPORTANT] > > **I'm sorry this diff is so big.** I know the importance of smaller > PRs, but this is one of those things that's hard to break into > smaller pieces because it either works or doesn't... ## Why? I'm the author of the Ghostty terminal. Ghostty has used simdutf for many years and it's a critical dependency we're happy with. Recently, we've been pursuing `libghostty-vt` (https://libghostty.tip.ghostty.org/), a C library that exposes a rich set of functionality for complete terminal emulation. One of the beautiful things about `libghostty-vt` is that if you disable our SIMD features, it has on dependencies at all (not even libc!). It is completely freestanding. But if you enable SIMD, it requires libc and libc++. The only libc++ requirement is for simdutf. So I'm interested in rectifying that and making it possible to use simdutf in a way that doesn't require libc++ so that `libghostty-vt` doesn't either. I ultimately intend to not require libc either, but that's a much bigger lift. In the mean time, we can provide fully static builds of `libghostty-vt` that have only the libc symbols we use. Much harder for C++. :) ## Build This mode is enabled explicitly by defining `SIMDUTF_NO_LIBCXX=1` when building simdutf. The important detail is that this is an all-or-nothing build contract: every translation unit that compiles simdutf sources or includes simdutf headers must see the same define. In practice, that means passing `-DSIMDUTF_NO_LIBCXX=1` consistently to both simdutf itself and any code that includes `simdutf.h`. A typical build looks like: ``` c++ -std=c++17 \ -DSIMDUTF_NO_LIBCXX=1 \ -fno-exceptions -fno-rtti \ -Iinclude -Isrc \ -c src/simdutf.cpp -o simdutf.o ``` If the goal is a stricter environment with no C++ standard library in the final artifact, this can also be paired with `-nostdinc++` and `-nostdlib++`, which is what the CI coverage exercises for the single-header build to ensure that there's no access to libc++ at all and that it also can't be linked. ## Approach ### Type Shims I try to achieve this in a minimally invasive way. The main idea is to provide a header `stl_compat.h` that shims the libc++ types in the NO_LIBCXX case. In the standard case (libc++), the types are zero overhead aliases or inline passthroughs. In the NO_LIBCXX case, the types are minimally implemented for only what simdutf directly needs. Therefore, the **diff is very large, but almost all the changes are trivial replacements from e.g. `std::pair` to `simdutf::internal::pair`.** There are some types I didn't want to shim at all, e.g. `std::span`, but these were necessary for some of the benchmarks (e.g. the base64 ones). That's why that's there. ### Reduced Surface Area This is intentionally a reduced-surface contract rather than an attempt to fully emulate the standard C++ functionality. Any public API that directly exposes `std::*` either changes shape or is compiled out in this mode. In practice this means things like `implementation::name()` and `implementation::description()` return `const char *` instead of `std::string`, and helpers such as `to_string(encoding_type)` similarly return C strings. APIs that inherently depend on stdlib facilities are not available at all. That includes `std::span` convenience overloads, `std::text_encoding` interop, and the `std::atomic_ref`-based helpers. ### Avoiding C++ Runtime Hooks Avoiding libc++ headers is only part of the problem. There are also a few compiler-emitted ABI hooks that can sneak in even when the source no longer mentions `std::*`. The big one is function-local statics, which can introduce `__cxa_guard_*` references for thread-safe initialization. In the NO_LIBCXX path I move the implementation singletons and dispatch storage to translation-unit scope so the runtime detection model stays the same without requiring those guard functions. There is also a weak `__cxa_pure_virtual` trap shim for the abstract implementation vtable case. Correct dispatch should never hit it, but without it some toolchains still insist on a `libc++abi` dependency just for that unreachable symbol. It is marked weak so that in compilation units that do pull in libc++abi, the real `__cxa_pure_virtual` will take precedence. The C wrapper is also adjusted to bridge through compiler-native char16_t/char32_t forms without assuming the normal C++ stdlib type machinery is available. ## Verification This type of feature can regress easily, so I added explicit verification. There is now a dedicated script and CI workflow that: - compiles the core sources with SIMDUTF_NO_LIBCXX - checks their undefined symbols for forbidden C++ ABI/runtime entries such as __cxa_guard_*, exception machinery, RTTI, and dynamic-cast support - links smoke tests without the C++ standard library and verifies the resulting binaries do not depend on libc++, libc++abi, libstdc++, or libsupc++ - exercises both the normal runtime-dispatch path and the fallback-only/single-implementation path - verifies that the single-header build still works with `-nostdinc++` and `-nostdlib++` The goal here is not just "make it compile on my machine" but also to make it maintainable. ## Performance I ran paired benchmarks for the standard build and the `SIMDUTF_NO_LIBCXX` build. The benchmarks included all possible benchmark programs in this repo and all modes. The conclusion is that performance is broadly unchanged. On the main throughput benchmarks, most realistic-input results move by only low-single-digit percentages, and many are effectively flat. I reran them a few times and they're sometimes faster sometimes slower so I'm going to say its noise. The important part is there is no real difference. ## AI Disclaimer I did this work alongside Codex. I'm not a C++ expert but I'm a very experienced systems-level programmer. I constantly changed direction and nudged the AI in different directions and also did a final pass on the set of changes. I'm happy to make adjustments as requested if this is desired and I can understand and explain any part of this diff.
|
CI is green. 🥳 |
|
I see this as a niche use case and those are fine, but not at any cost.
From my point of view, there needs to be a lot more justification for this. Is there any other user than ghostty that would benefit from this? Can you also please elaborate on the "environments where these dependencies are undesirable."? |
Agreed, that's why in all places I tried to keep it syntactically the same but with a namespace shift. I was hoping that'd be good enough where actual code change wasn't really necessary, maybe some muscle memory. 😄 On the maintenance side, I also added the CI check so that maintainers can be confident in continuing to work and merge PRs without breaking the
Also don't love it, but I think this is only really in implementation.cpp and one of the portability checks, or am I mistaken? Otherwise, I reused macros to prevent multiple macros, e.g.
I can only speak for myself, though I hope others would speak up. The existence of the prior issue (#158) and some of the responses about this on X (https://x.com/SheriefFYI/status/2044501603244511479) seem to imply this helps others.
I'll just speak to Ghostty and why It hink generally that's also useful. Ghostty itself is embedding simdutf within a library, not an app (libghostty: https://libghostty.tip.ghostty.org/). Library consumption particularly in bindings is a WHOLE lot easier with less dependencies, ideally none then it can fully statically link. As an example, the Go bindings are simplified from this since cgo automatically links libc but not libc++, so it forced us to have pkg-config compatibility stuff for libc++. As another example, the Rust bindings libghostty-rs have been having trouble with their own linking of libc++ (again, libc being easy). I can't speak to the specifics but maintainers and users of both encouraged me to prioritize this. Additionally, libghostty is built to run in Wasm and does so freestanding (no libc either so no emscripten and so on needed. MUCH easier to run and much lower cost). We can't support SIMD there yet but Wasm does support SIMD and from what I can tell it should work well, at least better than scalar. As a next step, I plan on pursuing a WebAssembly backend for simdutf (#212) with an initial emscripten requirement due to libc, but I also ultimately plan to produce a no-libc version (via providing bindings) so we can use simdutf in the browser without emscripten. That may be a step far for upstream but just so you know to what ends and why I'm pursuing this. |
|
Sorry I should also add that ultimately I respect whatever outcome this PR has. If the maintainers decide that this isn't desirable then I understand and I'm confident in my ability to maintain a downstream fork for myself and Ghostty will continue to use the fork, since no-libc/c++ is a hard goal of our project. |
|
@pauldreik I'll wait to see whether you put your approval before commenting further. Some thoughts.
|
|
So I started from this program... #include <stdio.h>
#include "simdutf.cpp"
#include "simdutf.h"
int main(int, char *[]) {
const char *source = "1234";
// 4 == strlen(source)
bool validutf8 = simdutf::validate_utf8(source, 4);
if (validutf8) {
puts("valid UTF-8");
} else {
puts("invalid UTF-8");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}And I build it in two different ways (using this PR): I get... You save about half a millisecond by building without the C++ standard library. So there is that. Interestingly, you need all these flags... That's what I did not know how to do back in 2022. And it depressed me a bit. |
Yep! That disables RTTI (not used by simdutf so zero issue) and exceptions (aborts programs if any leak through) which are a risk but shouldn't happen in the public ABI codepaths if I recall (they should be handled before then). So it also shouldn't be a problem. But that eliminates a ton of symbols showing up for the ABI use case.
And btw, not sure its fair to say it was done by AI. As I noted in my disclosure, I did use assistance, but I came up with the shape of the solution, hand-wrote the initial But in any case, its not like I was driving blind here. 😄 |
I hope this doesn't come across as rude, but if I were evaluating simdutf to be a dependency of an application or library, the libc++ dependency would be an immediate disqualification. Parsing text does not fundamentally depend on C++ (nor does it depend on C, but that's a separate conversation). I think it's safe to say that a large number of potential users would consider this library viable if it were written only with C rather than C++. Given that the API does not really benefit from C++, arguably delivering the functionality via C would be strictly an improvement from users' point of view. Anecdotally, I have a music player application that I work on, and I recently ported chromaprint away from C++ so that my music player could eliminate the last libc++ dependency. Maybe it's hard to see from the end users' perspective but the libc++ dependency really is burdensome. |
For reference, the simdutf library is used in systems where exceptions are disabled such as Chromium V8. Disabling exceptions is common in C++. |
|
so if I try to summarize the benefits:
and the downsides:
There is a concept of freestanding implementation: https://en.cppreference.com/w/cpp/freestanding.html which I only heard of but I have no experience working with it. How would that fit in to the desired usecase for this? Even if I sympathize with projects wanting to be able to do this, I don't like this change. |
|
Ok. So give @pauldreik's concerns, we cannot merge this PR as-is. But it does not mean we can do NOTHING. Let me see. |
|
I think we can achieve the same result with minimal changes, just picking up the essential parts. See #962 |
|
I am closing this PR, because I really like the follow up PR at #962 It uses the same ideas, and building blocks, but it should make the maintenance much easier... which is something we have to worry about in such a complex library. |
|
@mitchellh I would like to thank you for your very well written PR and the constructive interaction here. I very much appreciate your effort of writing the description by hand such that it was easy to understand your motivation and changes. |
|
Thanks @pauldreik. Thanks for the review too and completely respect your opinions. Glad we could all find a way to get this through in some form. |
Resolves #158
This adds a new define
SIMDUTF_NO_LIBCXXthat can be used to build simdutf without libc++ or libc++abi. This is very useful for environments where these dependencies are undesirable. I already have a Ghostty PR verifying this change using a custom amalgam build from my fork.This is similar to Google Highway's
HWY_NO_LIBCXXdefine that achieves the same thing .When SIMDUTF_NO_LIBCXX is present, we strive as much as possible to avoid breaking ABI but some is necessary since some of the public ABI relies on libc++ (e.g. returns
std::stringand such). As such, the presence of the define should be considered a separate ABI for all intents and purposes.When SIMDUTF_NO_LIBCXX is not present, we strive to change nothing. There is some additional noise in this PR towards that goal (e.g. in
implementation.cpp). The idea is that the standard build should remain ABI, functionally, and stylistically as identical as possible.AI Disclaimer: I did this work alongside Codex. I'm not a C++ expert but I'm a very experienced systems-level programmer. I constantly changed direction and nudged the AI in different directions and also did a final pass on the set of changes.
I'm happy to make adjustments as requested if this is desired and I can understand and explain any part of this diff. This PR description was fully hand-written by me.
Type of change
Important
I'm sorry this diff is so big. I know the importance of smaller PRs, but this is one of those things that's hard to break into smaller pieces because it either works or doesn't...
Why?
I'm the author of the Ghostty terminal. Ghostty has used simdutf for many years and it's a critical dependency we're happy with. Recently, we've been pursuing
libghostty-vt(https://libghostty.tip.ghostty.org/), a C library that exposes a rich set of functionality for complete terminal emulation.
One of the beautiful things about
libghostty-vtis that if you disable our SIMD features, it has on dependencies at all (not even libc!). It is completely freestanding.But if you enable SIMD, it requires libc and libc++. The only libc++ requirement is for simdutf. So I'm interested in rectifying that and making it possible to use simdutf in a way that doesn't require libc++ so that
libghostty-vtdoesn't either.I ultimately intend to not require libc either, but that's a much bigger lift. In the mean time, we can provide fully static builds of
libghostty-vtthat have only the libc symbols we use. Much harder for C++. :)Build
This mode is enabled explicitly by defining
SIMDUTF_NO_LIBCXX=1when building simdutf.The important detail is that this is an all-or-nothing build contract: every translation unit that compiles simdutf sources or includes simdutf headers must see the same define. In practice, that means passing
-DSIMDUTF_NO_LIBCXX=1consistently to both simdutf itself and any code that includessimdutf.h.A typical build looks like:
If the goal is a stricter environment with no C++ standard library in the final artifact, this can also be paired with
-nostdinc++and-nostdlib++, which is what the CI coverage exercises for the single-header build to ensure that there's no access to libc++ at all and that it also can't be linked.Approach
Type Shims
I try to achieve this in a minimally invasive way. The main idea is to provide a header
stl_compat.hthat shims the libc++ types in the NO_LIBCXX case. In the standard case (libc++), the types are zero overhead aliases or inline passthroughs. In the NO_LIBCXX case, the types are minimally implemented for only what simdutf directly needs.Therefore, the diff is very large, but almost all the changes are trivial replacements from e.g.
std::pairtosimdutf::internal::pair.There are some types I didn't want to shim at all, e.g.
std::span, but these were necessary for some of the benchmarks (e.g. the base64 ones). That's why that's there.Reduced Surface Area
This is intentionally a reduced-surface contract rather than an attempt to fully emulate the standard C++ functionality.
Any public API that directly exposes
std::*either changes shape or is compiled out in this mode. In practice this means things likeimplementation::name()andimplementation::description()returnconst char *instead ofstd::string, and helpers such asto_string(encoding_type)similarly return C strings.APIs that inherently depend on stdlib facilities are not available at all. That includes
std::spanconvenience overloads,std::text_encodinginterop, and thestd::atomic_ref-based helpers.Avoiding C++ Runtime Hooks
Avoiding libc++ headers is only part of the problem. There are also a few compiler-emitted ABI hooks that can sneak in even when the source no longer mentions
std::*.The big one is function-local statics, which can introduce
__cxa_guard_*references for thread-safe initialization. In the NO_LIBCXX path I move the implementation singletons and dispatch storage to translation-unit scope so the runtime detection model stays the same without requiring those guard functions.There is also a weak
__cxa_pure_virtualtrap shim for the abstract implementation vtable case. Correct dispatch should never hit it, but without it some toolchains still insist on alibc++abidependency just for that unreachable symbol. It is marked weak so that in compilation units that do pull in libc++abi, the real__cxa_pure_virtualwill take precedence.The C wrapper is also adjusted to bridge through compiler-native char16_t/char32_t forms without assuming the normal C++ stdlib type machinery is available.
Verification
This type of feature can regress easily, so I added explicit verification.
There is now a dedicated script and CI workflow that:
-nostdinc++and-nostdlib++The goal here is not just "make it compile on my machine" but also to make it maintainable.
Performance
I ran paired benchmarks for the standard build and the
SIMDUTF_NO_LIBCXXbuild. The benchmarks included all possible benchmark programs in this repo and all modes.The conclusion is that performance is broadly unchanged. On the main throughput benchmarks, most realistic-input results move by only low-single-digit percentages, and many are effectively flat. I reran them a few times and they're sometimes faster sometimes slower so I'm going to say its noise.
The important part is there is no real difference.
AI Disclosure: I'm repeating this again at the bottom in case you missed it at the top. I hand-wrote this entire PR description, I manually reviewed every change in this PR and can understand and discuss it intelligently with you, I did use Codex to assist in writing this, though!