HNHacker News
TopNewBestAskShowJobs

dgrunwald

308 karma · joined July 26, 2017

submissionscomments
dgrunwald··on EDG C++ front-end goes public
Despite using C++ for a few years now, the EDG source code is still mostly C code (and in fact, the C++ code is still using the old .c file names). In particular, there's no usage of the C++ standard library.

Where other languages might use inheritance, EDG still uses the C-style `union { ... } variant;`.

On a related note, compiling EDG is extremely fast: on my machine, the EDG frontend compiles in <10s; whereas clang takes >10min (caution unfair comparison: clang includes much more than just a frontend).

dgrunwald··on EDG C++ front-end goes public
It's one of the 4 big remaining C++ compiler frontends: gcc, clang, MSVC, EDG. Most other C++ compilers are based on either EDG or clang. (e.g. the Intel C++ compiler used to be based on the EDG frontend, though modern versions are based on clang instead)

The "frontend" is the part of the compiler that understands the input language: lexer, parser, template instantiation, constexpr evaluation, ... The EDG frontend produces an intermediate representation (IL) that is then used by the different compiler vendors to generate machine code. Or do code analysis.

EDG can also be used as a C++-to-C compiler.

dgrunwald··on C's Flexible Integer Sizes Were Not a Design Mistake
> ILP64 (wherein int is 64 bits) exists. It's not very popular, but it exists; e.g. ICC supports it.

ILP64 is problematic for existing code: there is lots of stuff like hashcode computations using uint32_t with multiplications, relying on the C standard guaranteeing wraparound for unsigned overflows. But with 64-bit int, uint32_t will promote to a signed int, and overflows will thus be undefined behavior. This problem already exists with uint16_t multiplications on current architectures, but moving the problem to uint32_t will cause trouble for a lot of existing code that thought using fixed-size types like uint32_t would be safe.

dgrunwald··on Subnormal floating-point numbers are expensive on Intel processors
For 32-bit floats, subnormals are the numbers closer to 0 than 2**(-126) == 0.0000000000000000000000000000000000000117549. For 64-bit doubles, it's 2**(-1022), a number starting with 308 decimal zeroes.
dgrunwald··on Move in C++ without a std:move
I don't know why I wrote the comment about parameters earlier -- (N)RVO is about return values. Those have a different reason for being passed behind a hidden pointer: the class type might have self-referencing pointers, so there must be an explicit move/copy constructor call whenever it changes address, to give the class an opportunity to update those pointers. This cannot work when returning in a register: the callee doesn't know the target address, and the caller doesn't the source address, so neither can call the move constructor. Thus, all ABIs must pass a pointer (or let caller+callee agree on a memory location in some other way) for types that aren't trivially copyable.
dgrunwald··on Six curl CVEs after OpenAI and Anthropic came back with zero
> Memory safety bugs aren't any different from other bugs.

This is highly dependent on what kind of software you are writing.

On the one end, there's stuff like an image format parser in a browser -- a pure function from untrusted bytes to untrusted pixels. In a memory-safe language, it's pretty hard for such code to have vulnerabilities (other than DoS) in such code -- you'd have to explicitly go out of your way to do weird stuff (open unrelated files, start subprocesses, ...). Simple logic bugs can only lead the wrong pixels or panics. But when memory safety bugs are possible, remote code execution is common in such code. Memory-safe languages make a massive difference here!

On the other end, you have stuff like a javascript JIT compiler, which turns untrusted javascript into trusted machine code -- here pretty much any logic bug leading to "wrong output" can be turned into a remote code execution exploit. Memory safe languages are not very useful here.

dgrunwald··on Move in C++ without a std:move
The optimization is often possible even if the computer does not see the call, because most (all?) ABIs have always required hidden pointer parameters for class types with non-trivial destructors.

https://godbolt.org/z/9WvnEvEYh Note how `std::unique_ptr<int>` effectively passed as a `int**`; and that the by-value unique_ptr is not destroyed at the end of the function -- destroying parameters is instead the caller's job (and commonly only happens at the end of the full expression containing the call -- though this choice is implementation-defined). But that can only work if the caller can see the updated value of the parameter (to avoid double-free for `clear`) -> thus the need to pass the parameter by hidden pointer.

dgrunwald··on I used Fable to rewrite 65kLoC of Go in Rust. It cost $400
The compiler cannot optimize that into `while(true)` because the original code does not encounter undefined behavior when `limit` is small enough to fit into `int`. What it can do: infer that `limit <= INT_MAX` and use that to optimize the code following after the loop (and in some cases, even the code before the loop).
dgrunwald··on Malicious Rust crate Arrayref runs a build-time payload
The malicious code could use life-before-main hacks to gain code execution if it's linked at all, even if uncalled. https://grack.com/blog/2026/06/11/life-before-main/
dgrunwald··on Linux kernel will support $ORIGIN, sort of
$ORIGIN was only supported when the loader was looking for other dependencies. The loader itself (field PT_INTERP) is loaded by the kernel. So prior to this change, every program must hardcode the absolute path to ld.so. With support for an $ORIGIN-relative loader, each program could use its own copy of ld.so.
dgrunwald··on Do you hate XML? (2010)
> Furthermore parsing JSON or YAML gives you the basic data types like lists and dictionaries. Parsing XML gives you an AST that requires a lot more effort to turn into data in your domain.

More precisely: in XML, elements (nodes) are named/labeled. ("node-labeled graph") In JSON, keys (edges) are named. ("edge-labeled graph")

In programming, we need names for the fields in our structures (edges between objects), so JSON is a much better match than XML (which needs contortions to handle this use case -- e.g. by having nesting levels alternate between element=node and element=edge). Only in some object-oriented cases (which derived class should the deserializer construct?) do you care about node labels -- but usually that's in addition to edge labels, so a "_type" key in JSON is still easier than XML.

dgrunwald··on The time the x86 emulator team found code so bad they fixed it during emulation
`rmdir /s /q` in a command prompt is significantly faster than Windows Explorer.

Yes C: is slow due to filters and Dev Drive is faster; but this difference can only be felt when using the command line; Windows Explorer has so much additional overhead that the overhead from file filters is insignificant in comparison.

dgrunwald··on C extensions, portability, and alternative compilers
In our compiler (in a code analysis tool), we have

   #pragma immutable_macro __attribute__
After this pragma, any attempt to #define/#undef the macro "__attribute__" will be silently ignored. This lets us (or our customers) bypass such stupidity in library headers. It's also often useful to replace broken macros with working versions.
dgrunwald··on Shell Tricks That Make Life Easier (and Save Your Sanity)
It's a normal command called "View: Reopen Closed Editor".
dgrunwald··on NaN Is Weird
Yes `int` acts as if it was a subtype of `float`: https://typing.python.org/en/latest/spec/special-types.html#...
dgrunwald··on NaN Is Weird
But in Python the type checker does not complain about `x: float = 0`, because for the purpose of type checking (but not at runtime), `int` is considered a subtype of `float`: https://typing.python.org/en/latest/spec/special-types.html#...
dgrunwald··on NaN Is Weird
In most languages, `x: float = 0` involves an implicit conversion from int to float. In Python, type annotations have no impact on runtime behavior, so even though the type checker accepts this code, `type(x)` will be `int` -- python acts as if `int` was a subtype of `float`.

It would be weird if the behavior of `1 / x` was different depending on whether `0` or `0.0` was passed to a `x: float` parameter -- if `int` is a subtype of `float`, then any operation allowed on `float` (e.g. division) should have the same behavior on both types.

This means Python had to choose at least one:

1. division violates the liskov substitution principle

2. division by zero involving only integer inputs returns NaN

3. division by zero involving only float inputs throws exception

4. It's a type error to pass an int where a float is expected.

They went with option 3, and I think I agree that this is the least harmful/surprising choice. Proper statically typed languages don't have to make this unfortunate tradeoff.

dgrunwald··on Rust is just a tool
Don't forget the compatibility issues: Fil-C isn't really usable if you mix C with other languages in the process.

It's especially problematic to have multiple different garbage collectors; so given the desire to reuse libraries across language boundaries, there's still a strong demand for Yolo-C, C++ or Rust.

dgrunwald··on Index, Count, Offset, Size
When accessing individual elements, 0-based and 1-based indexing are basically equally usable (up to personal preference). But this changes for other operations! For example, consider how to specify the index of where to insert in a string. With 0-based indexing, appending is str.insert(str.length(), ...). With 1-based indexing, appending is str.insert(str.length() + 1, ...). Similarly, when it comes to substr()-like operations, 0-based indexing with ranges specified by inclusive start and exclusive end works very nicely, without needing any +1/-1 adjustments. Languages with 1-based indexing tend to use inclusive-end for substr()-like operations instead, but that means empty substrings now are odd special cases. When writing something like a text editor where such operations happen frequently, it's the 1-based indexing that ends up with many more +1/-1 in the codebase than an editor written with 0-based indexing.
dgrunwald··on Microsoft gave FBI set of BitLocker encryption keys to unlock suspects' laptops
> make sure not to sign into your Microsoft account or link it to Windows again

That's not so easy. Microsoft tries really hard to get you to use a Microsoft account. For example, logging into MS Teams will automatically link your local account with the Microsoft account, thus starting the automatic upload of all kinds of stuff unrelated to MS Teams.

In the past I also had Edge importing Firefox data (including stored passwords) without me agreeing to do so, and then uploading those into the Cloud.

Nowadays you just need to assume that all data on Windows computers is available to Microsoft; even if you temporarily find a way to keep your data out of their hands, an update will certainly change that.

dgrunwald··on How we made Python's packaging library 3x faster
That loop isn't N²: if there are long sequences of dashes, every iteration will cut the lengths of those sequences in half. So the loop has at most lg(N) iterations, for a O(N*lg(N)) total runtime.
dgrunwald··on Why is Zig so cool?
cygwin is a POSIX-emulating library intended for porting POSIX-only programs to Windows. That is: when compiling for cygwin, you'd use the cygwin POSIX APIs instead of the Windows APIs. So anything compiled with cygwin won't be a normal Windows program.

There's no reason to use cygwin with Rust, since Rust has native Windows support. The only reason to use x86_64-pc-cygwin is if you would need your program to use a C library that is not available for Windows, but is available for cygwin.

If you don't want to/can't use the MSVC linker, the usual alternative is Rust's `x86_64-pc-windows-gnu` toolchain.

dgrunwald··on Why is Zig so cool?
If you need `0..=n`, you can't write `0..(n+1)` because that addition might overflow.
dgrunwald··on The provenance memory model for C
But the source character set remains implementation-defined, so compilers do not have to directly support unicode names, only the escape notation.

Definitely a questionable choice to throw off readers with unicode weirdness in the very first code example.

dgrunwald··on Making C and Python Talk to Each Other
The article is using the Python C API directly.

pybind11 is a C++ wrapper that makes the Python API more friendly to use from C++ (e.g. smart pointers instead of manual reference counting)

dgrunwald··on Exploiting Undefined Behavior in C/C++ Programs: The Performance Impact [pdf]
> But if i is indeed put immediately after, then the function should indeed return 5 for n=3.

That's not how compilers work. The optimization changing `return i;` into `return 0;` happens long before the compiler determines the stack layout.

In this case, because `return i;` was the only use of `i`, the optimization allows deleting the variable `i` altogether, so it doesn't end up anywhere on the stack. This creates a situation where the optimization only looks valid in the simple "flat memory model" because it was performed; if the variable `i` hadn't been optimized out, it would have been placed directly after `arr` (at least in this case: https://godbolt.org/z/df4dhzT5a), so the optimization would have been invalid.

There's no infrastructure in any compiler that I know of that would track "an optimization assumed arr[3] does not alias i, so a later stage must take care not to place i at that specific point on the stack". Indeed, if array index was a runtime value, the compiler would be prevented from ever spilling to the stack any variable that was involved in any optimizations.

So I think your general idea "the allowable behaviors of an out-of-bounds write is specified by the possible actual behaviors in a simple flat memory model for various different stack layouts" could work as a mathematical model as an alternative to UB-based specifications, but it would end up not being workable for actual optimizing compiler implementations -- unless the compiler could guarantee that a variable can always stay in a register and will never be spilled (how would the compiler do that for functions calls?), it'd have to essentially treat all variables as potentially-modified by basically any store-via-pointer, which would essentially disable all optimizations.

dgrunwald··on C stdlib isn't threadsafe and even safe Rust didn't save us
The problem with `setenv` is that people expect one process to have one set of environment variables, which is shared across multiple languages running in that process. This implies every language must let its environment variables be managed by a central language-independent library -- and on POSIX systems, that's libc. So if libc refuses to provide thread-safety, that impacts not just C, but all possible languages (except for those that cannot call into C-libraries; as those don't need to bother synchronizing the environment with libc).
dgrunwald··on C stdlib isn't threadsafe and even safe Rust didn't save us
How exactly would that help in this situation?

If both Rust and C have independent standard libraries loaded into the same process, each would have an independent set of environment variables. So setting a variable from Rust wouldn't make it visible to the C code, which would break the article's usecase of configuring OpenSSL.

The only real solution is to have the operating system provide a thread-safe way of managing environment variables. Windows does so; but in Linux that's the job of libc, which refuses to provide thread-safety.

dgrunwald··on Rust: Investigating an Out of Memory Error
Yes, that is typically the way to go.

Collecting a call stack only requires unwinding information (which is usually already present for C++ exceptions / Rust panics), not full debug symbols. This gives you a list of instruction pointers. (on Linux, the glibc `backtrace` function can help with this)

Print those instruction pointers in a relative form (e.g. "my_binary+0x1234") so that the output is independent of ASLR.

The above is all that needs to happen on the production/customer machines, so you don't need to ship debug symbols -- you can ship `strip`ped binaries.

On your own infrastructure, keep the original un-stripped binaries around. We use a script involving elfutil's eu-addr2line with those original binaries to turn the module+relative_address stack trace into a readable symbolized stack trace. I wasn't aware of llvm-symbolizer yet, seems like that can do the same job as eu-addr2line. (There's also binutil's addr2line but in my experience that didn't work as well as eu-addr2line)

dgrunwald··on C++ String Conversion: Exploring std:from_chars in C++17 to C++26
Caution with these functions: in most cases you need to check not only the error code, but also the `ptr` in the result. Otherwise you end up with `to_int("0x1234") == 0` instead of the expected `std::nullopt`, because these functions return success if they matched a number at the beginning of the string.
Page 1 of 2Next →