Hacker Newsnew | past | comments | ask | show | jobs | submit | snuxoll's commentslogin

> where allocating using malloc(3) in one DLL then freeing it with free(3) in another being a crash.

This can still happen all the time on UNIX systems. glibc's malloc implementation is a fine general purpose allocator, but there's plenty of times where you want to bring in tcmalloc, jemalloc, etc. Of course, you hope that various libraries will resolve to your implementation of choice when the linker wires everything up, but they can opt not to just as easily.


No actually, this doesn't happen the same way on modern Unix. The way symbol resolution works is just not the same. A library asking for an extern called "malloc" will get the same malloc. To use those other allocators, you would typically give them a different symbol name, or make the whole process use the new one.

A dll import on Windows explicitly calls for the DLL by name. You could have some DLLs explicitly ask for a different version of the Visual Studio runtime, or with different threading settings, release vs debug etc., and a C extern asking for simply the name "malloc", no other details, will resolve to that, possibly incompatible with another DLL in the same process despite the compiler's perspective of it just being extern void *malloc(size_t) and no other detail, no other decoration, rename of the symbol etc.. there might be a rarely used symbol versioning pragma to accomplish similar on a modern gcc/clang/elf setup but it's not the way anybody does this.

I would argue that the modern Unix way, with these limitations, is better, by the way. Maybe some older Unix in the early days of shared libraries, early 90s or so, tried what Windows does, I don't know. But it's not common today.


> No actually, this doesn't happen the same way on modern Unix. The way symbol resolution works is just not the same. A library asking for an extern called "malloc" will get the same malloc. To use those other allocators, you would typically give them a different symbol name, or make the whole process use the new one.

This is, yes, the behavior of both the ELF specification as well as the GNU linker.

I'm not here to get into semantics of symbol namespaces and resolution though, I can just as easily link a static jemalloc into an arbitrary ELF shared object and use it inside for every allocation and not give a damn about what the global malloc() symbol points to. There's a half dozen other ways I can have a local malloc() symbol as well instead of having the linker bind the global one.

Which, is the entire point I'm trying to make. Is this a bigger problem on Windows versus UNIX-like platforms due to the way runtime linker support is handled? Yes. Is it entirely possible to have the same issue, however? Yes, absolutely.


In about 27 years of using Linux and BSD I don't think I've seen it once. If you work professionally in C on Windows it is a practical concern, an everyday occurrence.

Another absurdly common issue is passing a FILE * across a DLL boundary. It is highly unlikely to work. I used to have to train new hires not to do this and tell partner teams working on C APIs to include I/o abstractions that don't involve FILE*, which would illicit a response as if I'm an alien.


This will work for any application compiled against uCRT, which has been the default for 10 years now.

https://learn.microsoft.com/en-us/cpp/windows/universal-crt-...


While they're not "the same", classic COM (or OLE? the whole history is a mess) did actually have ProgIDs, and WinRT introduces proper "classes" and namespaces (having given up global registration for everything but system provided API's) with proper "names" (you can even query them at runtime with IInspectable::GetRuntimeClassName).

Microsoft tried to do a lot with COM when they first released it, it wasn't just a solution for having a stable cross-language ABI, it was a way to share component libraries across multiple applications on a system, and a whole lot more.

> but that also wouldn't have flown when COM was invented and resources overall were much more scarce than they are today.

And this ultimately is the paradox of COM. There were good ideas, but given Microsoft's (mostly kept) promise of keeping old software working the bad ones have remained baked in.


What do you mean by "owned" strings?

WinRT, which is ultimately just an evolution of COM, has HSTRING which can own the data inside it (as well as contain a reference to an existing chunk of memory with fast-pass strings).


As somebody who's been, for whatever reason, toying around with writing a COM-style ABI layer in Rust, there's a lot of good ideas in there and I think a lot of the hatred comes from the DLL hell that was spawned by registration; along with the, unfortunately necessary, boilerplate.

Virtual dispatch absolutely has an overhead, but absolutely nobody in their right mind should be using COM interfaces in a critical section of code. When we're talking things like UI elements, HTTP clients, whatever, the overhead of an indirect call is negligible compared to the time spent inside a function.

The one thing I'm personally trying to see if there's any room for improvement on in a clean slate design, is error handling / HRESULT values. Exceptions get abused for flow control and stack unwinding is expensive, so even if there was a sane way to implement cross-language exception handling it's a non starter. But HRESULT leads to IErrorInfo, ISupportErrorInfo, thread local state SetErrorInfo/GetErrorInfo, which is a whole extra bunch of fun to deal with.

There's the option of going the GObject and AppKit route, using an out parameter for an Error type - but you have to worry about freeing/releasing this in your language bindings or risk leaking memory.


Registration free COM has existed since Windows XP, if I get my timeline right without bothering to look it up.

All modern Windows APIs introduced since Vista have been COM, classical Win32 C APIs are seldom introduced nowadays.

Certainly current Windows 11 performance problems have nothing to do with using COM all over the place, rather Webwidgets instead of native code, hiring people that apparently never did Windows programming, that apparently do AI driven coding.

Ah, macOS and iDevices driver model is equally based in COM like design, one would expect drivers to be something where performance matters.

Then there is XPC, Android IPC, and one could consider D-BUS as well, if it was more widely adopted across the GNU/Linux world.


You are absolutely right on all counts, although XPC/Binder/D-Bus aren't really something to compare against the core of COM (the ABI model), and I think many Windows developers would have some unkind things to say about DCOM.

> Virtual dispatch absolutely has an overhead, but absolutely nobody in their right mind should be using COM interfaces in a critical section of code.

I could definitely be wrong, but I think C++ style "virtual dispatch" (ie, following two pointers instead of one to get to your function) doesn't really cost anything anymore, except for the extra pointers taking up cache space.

Don't all of the Windows DirectX gaming interfaces use COM? And isn't AAA gaming performance critical?


> Don't all of the Windows DirectX gaming interfaces use COM? And isn't AAA gaming performance critical?

Yes, on both counts. You will also, on average, be making fewer calls to ID3D12CommandQueue methods than one would think - you'd submit an entire vertex buffer for a model (or specific components of it that need the same pipeline state, at least) at once, allocate larger pools of memory on the GPU and directly write textures to it, etc.

This is the entire design behind D3D12, Vulkan, and Metal - more direct interaction with the GPU, batching submission, and caching command buffers for reuse.

When I'm talking about "critical sections" of code, I mean anything with a tight loop where you can reasonably expect to pin a CPU core with work. For a game, this would be things like creating vertex buffers, which is why all three major API's take these as bare pointers to data structures in memory instead of requiring discrete calls to create and populate them.


WinRT is certainly not a "clean slate design", but still a useful comparison to see where Microsoft themselves iterated on the COM design with decades of hindsight.

Pity that the great tooling that came with it is now gone, alongside UWP.

WinRT tooling on Win32 side is a bad joke.

I almost lost count of how many COM frameworks have come and gone since OLE 1.0 days.


A 5% increase is still a sizable when you consider the number of licenses that even an SMB may have. I don't deal with our MS licensing directly at $DAYJOB, but we've got something like 1300+ employees most (all?) of whom have M365 E5 licenses, that adds up to (roughly) an extra $4K/mo or $48K/yr when it comes time to renew our annual licensing.

Is it going to break the bank and send us into a financial death spiral? Absolutely not. But, you get enough companies deciding to jack up pricing at around the same time and it comes out to a significant increase in our lights-on budget - death by a million cuts hurts just as much as Broadcom raking us over the coals with VMWare license increases.


5% is 5%. If you have more employees, you also presumably have more revenue. That's why percentages rather than absolutes are the right metric. And keeping up with inflation isn't "jacking up pricing".

If you are legally authorized to sign on behalf of a business you can sign court filings for it.

Important to note that .context() is something from `anyhow`, not part of the stdlib.

I don't really see it as any more or less verbose.

If I return Result<T, E> from a function in Rust I have to provide an exhaustive match of all the cases, unless I use `.unwrap()` to get the success value (or panic), or use the `?` operator to return the error value (possibly converting it with an implementation of `std::From`).

No more verbose than Go, from the consumer side. Though, a big difference is that match/if/etc are expressions and I can assign results from them, so it would look more like

    let a = match do_thing(&foo) {
      Ok(res) => res,
      Err(e) => return e
    }
instead of:

     a, err := do_thing(foo)
     if err != nil {
       return err // (or wrap it with fmt.Errorf and continue the madness
                  // of stringly-typed errors, unless you want to write custom
                  // Error types which now is more verbose and less safe than Rust).
    }
I use Go on a regular basis, error handling works, but quite frankly it's one of the weakest parts of the language. Would I say I appreciate the more explicit handling from both it and Rust? Sure, unchecked exceptions and constant stack unwinding to report recoverable errors wasn't a good idea. But you're not going to have me singing Go's praise when others have done it better.

Do not get me started on actually handling errors in Go, either. errors.As() is a terrible API to work around the lack of pattern matching in Go, and the extra local variables you need to declare to use it just add line noise.


Especially since the second example only gives you a stringly-typed error.

If you want to add 'proper' error types, wrapping them is just as difficult in Go and Rust (needing to implement `Error` in Go or `std::Error` in Rust). And, while we can argue about macro magic all day, the `thiserror` crate makes said boilerplate a non-issue and allows you to properly propagate strongly-typed errors with context when needed (and if you're not writing library code to be consumed by others, `anyhow` helps a lot too).


fmt.Errorf with %w directive in fact wraps an error. It will return an fmt.wrapError struct which can be inspected using `errors.Is`. So it's not stringly typed anymore.

I am fully aware of how fmt.Errorf works as well as what's inside the `errors` package in the Golang stdlib, as I do work with the language regularly.

In practice, this ends up with several issues (and I'm just as guilty of doing a bunch of them when I'm writing code not intended for public consumption, to be completely fair).

fmt.Errorf is stupid easy to use. There's a lot of Go code out there that just doesn't use anything else, and we really want to make sure we wrap errors to provide 'context' since there's no backtraces in errors (and nobody wants to force consuming code to pay that runtime cost for every error, given there's no standard way to indicate you want it).

errors.New can be used to create very basic errors, but since it gives you a single instance of a struct implementing `error` there's not a lot you can do with it.

The signature of a function only indicates that it returns `error`, we have to rely on the docs to tell users what specific errors they should expect. Now, to be fair, this is an issue for languages that use exception's - checked exceptions in Java notwithstanding.

Adding a new error type that should be handled means that consumers need to pay attention to the API docs and/or changelog. The compiler, linters, etc don't do anything to help you.

All of this culminates to an infuriating, inconsistent experience with error handling.


> Beyond these concerns, I also don't love enums for errors because it means adding any new error type will be a breaking change. I don't love the idea of committing to that, but maybe I'm overthinking?

Is it a new error condition that downstream consumers want to know about so they can have different logic? Add the enum variant. The entire point of this pattern is to do what typed exceptions in Java were supposed to do, give consuming code the ability to reason about what errors to expect, and handle them appropriately if possible.

If your consumer can't be reasonably expected to recover? Use a generic failure variant, bonus points if you stuff the inner error in and implement std::Error so consumers can get the underlying error by calling .source() for debugging at least.

> By contrast, I just wrap Go errors with `fmt.Errorf("opening file `%s`: %w", filePath, err)` and handle any special error cases with `errors.As()` and similar and move on with life. It maybe doesn't feel _elegant_, but it lets me get stuff done.

Nothing stopping you from doing the same in Rust, just add a match arm with a wildcard pattern (_) to handle everything but your special cases.

In fact, if you suspect you are likely to add additional error variants, the `#[non_exhaustive]` attribute exists explicitly to handle this. It will force consumers to provide a match arm with a wildcard pattern to prevent additions to the enum from causing API incompatibility. This does come with some other limitations, so RTFM on those, but it does allow you to add new variants to an Error enum without requiring a major semver bump.


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: