Rust panics under the hood, and implementing them in .NET
fractalfir.github.io
fractalfir.github.io
I'm using nostd and panic=abort to keep the library small (around 200kb after being stripped), but it's really not a well-supported setup.
Depending on the platform, I get an error that the _Unwind_Resume symbol is not defined (despite it not supposed to be needed), or if I define it myself I get a duplicate symbol error on other platforms. I ended up linking to gcc_eh on Linux just to avoid the issue.
I just find it strange that out of all the possible issues with using nostd, unwinding is causing the most issues.
Try `-Z build-std=core` if you're not already doing it.
I do know that for regular std-using programs, std has its own panic strategy that it was compiled with, so even if you set your own code's panic strategy to abort, it still ends up pulling in unwind-related code from libstd. The solution to that is to also recompile libstd with the same parameters as your own code, which is what `-Z build-std` does.
I don't know if the same thing applies even to libcore, ie for no_std programs. FWIW, coincidentally I spent yesterday making a no_std panic=abort program for a custom freestanding target, and the only missing symbols it needed from me were memcpy and memcmp, even though the code does do panicking things like slice indexing. That program requires `-Z build-std=core` since it's for a custom freestanding target.
> The project aims to provide a way to easily use Rust libraries in .NET. It comes with a Rust/.NET interop layer, which allows you to easily interact with .NET code from Rust
Are there some standout Rust libraries out there? Very curious about the motivation and use cases.My question here is more along the lines of: "what Rust libs are out there that I might be interested in from .NET?"
If you do not want JITing, you can already "pre-compile" dotnet code, and you will achieve near-native performance in either case—far better performance than running Rust as IL, as the author indicated.
In my opinion, the use case for this is minuscule at best.
AOT is still not "there yet" in some cases though? At least that was my understanding. It cannot do every single .NET project out there yet, but it can do some.
[1] https://learn.microsoft.com/en-us/dotnet/core/deploying/nati...
I imagine this pattern will become more common for fat clients where it is desirable have a single implementation with idiomatic language bindings.
I can say with regards to panics, .NET is very nice to wrap Rust panics into `SEHException` classes (though of course we strive to be completely panic free).
The use cases I see are going to be things such as sharing a library over multiple platforms. I've done that with Go in the past via cgo.
Which enables stuff like rayon where you can basically blindly replace map with parallel map and if it compiles, it _should_ be safe to run.
(I'm not super familiar with the .NET ecosystem, so it's quite possible there's equivalent tooling for enforced thread safety. I haven't heard much about it though, if so.)
I think the main advantages of Rust are its heavier reliance on static dispatch when e.g. writing iterator expressions (underlying type system as of .NET 8 makes them equally possible, a guest language could choose to emit ref struct closures that reference values on stack, but C# can never take a such change because it would be massively breaking, a mention goes to .NET monomorphizing struct generics in the exact same way it happens in Rust), fearless concurrency, access to a large set of tools that already serve C/C++ and confidence that LLVM is going to be much more robust against complex code, and of course deterministic memory reclamation that gives it signature low memory footprint. Rust is systems-programming-first, while C# is systems-programming-strong-second.
Other than that, C# has good support for features that allow you to write allocation-free or manual memory management reliant code. It also has direct counterpart to Rust's slice - Span<T> which transparently interoperates with both managed and unmananged memory.
Out of interest, why?
There have been many projects to improve this like https://github.com/dubiousconst282/DistIL and community libraries that reimplement LINQ with structs and full monomorphization, but the nature of most projects written in C# means their developers usually are not interested or do not need the zero-cost-like abstractions, which limits the adoption, and for C# itself it would need to evolve, and willingly accept a complete copy of existing APIs in LINQ with new semantics, which is considered, and I agree, a bad tradeoff where the simpler cases can be eventually handled through compiler improvements, especially now that escape analysis is back on the menu.
Which is why, in order to "properly" provide Rust-like cost model of abstractions as the first-class citizen, only a new language that targets .NET would be able to do so. Alternatively, F# too has more leeway in what it compiles its inferred types to, but its a small team and as a language F# has different priorities as far as I know.
(And of course the flip side of Rust here is that you need to be able to figure out how to represent your code and data to make it happy, which provides new and interesting limitations to how you can write stuff without delving into "unsafe" territory... something something TANSTAAFL)
Good info though; thanks!
Most of the time - it is, sort of. As in, accessing types that are not meant for concurrent access may lead to logic bugs, but the chance of this violating memory safety is almost nonexistent with the standard library and slim with community ones (for example, such library may use a native dependency which itself is not thread-safe, usually it's clear whether this is the case or not but the risk exists).
The common scenarios are well-known - use Interlocked.Add instead of +=, ConcurrentDictionary<K, V> instead of a plain one, etc. .AsParallel() itself already is able to collect the data in parallel - you just use .ToArray and call it a day.
Other than that, most APIs that are expected to be used concurrently are thread-safe - from the top of my head: HttpClient, Socket, JsonSerializerOptions, Channel<T> and its Reader/Writer can be shared by many threads (unless you specify single reader/writer on construction to reduce synchronization). Task<T> can be awaited multiple times, by multiple threads too. A lot of C# code already assumes concurrent execution, and existing concurrency primitives usually reduce the need for explicit synchronization. Worst case someone just slaps lock (instance) { ... } on it and gets on with their life.
This is to say, Rust provides watertight guarantees in a way C# is simply unable to, and when you are writing low-level code, you are on your own in C# where-as Rust has your back. But in other situations - C# is generally not known to suffer from race conditions and async/await usually allows to flow the data in a linear fashion in multi-tasking code, allowing the underlying implementation to do synchronization for you.
Honestly I'm more interested in code contracts than Rust, as they allow you to make a set of statements of your system which can then be validated statically. I've had very good results using them (and am forever grateful to the colleague who introduced me to them)... and I am interested in Rust (having dabbled with it, only I'm yet to use it in a paying or production project).
So sideways as you said - with C# you can usually rely on the GC for memory management, have fast compile times and what I feel is a more flexible model.. and top tier tooling and a huge and wide range of libraries. Rust can be much more efficient and has much better language-level properties wrt threat safety and a more mature story around native code.
I'd use Rust for system-level code or places where I'd otherwise think of using C++. I keep having discussions with people who want to use Rust for everything - CLI tools, web services (which are doing pumping), business logic and the like. It's getting tiring. The trade-offs are bad. C#, Go and Java are all far better suited & cover pretty much all the niches.
Does Rust have anything to spot resource leaks (i.e. the infamous IDisposable in C#)?
I think is more common that you have some in-house library in a different language (Rust in this case) and you want to reuse them without rewriting them.
Pity that never made the cross platform jump.
Just specify the headers to generate bindings for, and then use the generated interop code. Not too different from importing them as it was in the past.
Likewise the whole WinRT/UWP, .NET Native and C++/CX, were under WinDev umbrella, not DevDiv.
Which is why there is this schizophrenic way of how .NET is currently handled on Windows desktop.
For all intents and purposes, the non-unsafe subset of Rust is a safe language, and the distinction pretty much does not exist at the IL level.
It seems like you would get rid of performance benefits of rust.
But .net can do unsafe pointer operations and it can pin objects to avoid GC (they call it "handles" iirc).
In a single-threaded program, a panic is not supposed to be "caught" and aborts everything. If the main thread panics, the program stops.
But in a multi-threaded program, a panic terminates only the thread it happened in, and the program is allowed to handle that case without termination.
I'm guessing that setting `panic=abort` changes this behavior, but I'm not sure.
Docs: https://doc.rust-lang.org/std/macro.panic.html#current-imple..., the third sentence of https://doc.rust-lang.org/std/thread/fn.spawn.html
What you could do is create a single new thread and pretend as though its the main thread, observing any panics on the original main thread.
Edit: your mental model should be to avoid thinking about panics (beyond avoiding them in the first place). Panics are supposed to be extremely rare, and typically something that would be difficult to recover from. They are not exceptions; they are not designed to be caught. The difficulty in dealing with them is a feature that prevents anti patterns. If something panics you have a bug.
Catching panics in Rust is meant pretty much only to avoid unwinding across an FFI boundary, since that would be undefined behavior. Pretty much every other use of `catch_unwind` is a mistake, it's not guaranteed to catch all panics (they can also just abort) so panics are rather different from exceptions. Panics are intended to be useless for control flow, unwinding exists to help debugging, and any panic indicates a bug in the invoking program.
Fundamentally panic recovery works the same way in all threads—for both the main thread and spawned threads the standard library implements panic handling by wrapping the user code in catch_unwind().[2][3] It's more or less possible to override the standard library's behavior for the main thread by wrapping all the code in your main() function in a catch_unwind() and then implementing whatever fallback behavior you want, like waiting for other threads to exit before terminating. In some cases something like this happens automatically, for instance if the main thread spawns other threads using std::thread::scope.[4]
[1]: https://doc.rust-lang.org/std/thread/#the-threading-model [2]: https://github.com/rust-lang/rust/blob/7042c269c166191cd5d8d... [3]: https://github.com/rust-lang/rust/blob/7042c269c166191cd5d8d... [4]: https://doc.rust-lang.org/std/thread/fn.scope.html
Then the Windows only focus (for a while there was Rotor), and Microsoft being Microsoft, all that interest faded away and people focused on the JVM, even though MSIL was designed for all kind of languages (well dynamic came later with Iron languages and DLR), including C and C++.
Nowadays CLR almost feels to have changed into C# Language Runtime, given the C# centric decision on modern .NET design.
Also a solution for code generators, analysers, interceptors usage in .NET libraries and how to consume them from F#, some day, if ever.
And most CIL ABI additions to the CLR driven by C# totally break them, because the C# ecosystem adopts them immediately (recently even breaking changes in the shared framework!), and there's no modern equivalent to the Common Language Specification.
Plus, no library writer would care if there was a CLS 2.0 because every CLR language other than C# and F# is in maintenance mode or simply abandoned.
Definitely! VB.Net is basically "dead" and F# will never really reach critical mass.
Not that it matter much nowadays, given that its development has been placed on freeze mode.
1: https://dotlisp.sourceforge.net/dotlisp.htm
In general, the debugger is not intentionally made unavailable but rather the "properietary" one is just the original Visual Studio debugger extracted into a standalone package adapted to cross-platform.
Other than that, CoreCLR exposes rich debugger API, and debugger implementations just integrate with it, there are no "private bits" that would make this task insurmountable, there was just not much need to do so so far, and some Linux folks (as evidenced by skimming through other .NET submissions here) tend to have irrational and unsubstantiated hatred towards .NET which hinders otherwise positive community efforts.
> rather the "properietary" one
The fact that it is proprietary intentionally makes it unavailable for use outside of Visual Studio and Xamarin Studio - this actually caused debugging to be unavailable in Rider for a while a few years ago before they built their own.
Given the confidence of your reply, one would assume you'd know this? Unless it's the exact problem I outlined previously, in which case please consider sharing grievances about something you do use actively instead, rather than what you think are .NET's issues.
- The MS Debugger was use in Rider - thus was perfectly functional from a technical perspective.
- It was later discovered that the license was proprietary, allowed only for MS products. VS Code is one of those. The extension may legally not be used with VS Codium or other such telemetry-neutering builds.
- The debugger was removed, and debugging of Core CLR apps was unavailable while JetBrains found an alternative (which did not take very long).
As I alluded to, the fact that this worked, and was just prevented by licensing makes it a construct solely of proprietary software licensing. It was well documented at the time:
- https://blog.jetbrains.com/dotnet/2017/02/15/rider-eap-17-nu...
- https://blog.jetbrains.com/dotnet/2017/02/23/rider-eap-18-co... news-about-coreclr-on-windows
As for daily driving: I was the first person outside of JetBrains to get hands on Rider. The fact that I don't write C# _daily_ in 2024 does not mean I have no first-hand knowledge of what was happening in 2016-2018, or indeed today.
Would you like to put it against Go for lacking package manager, Java for being stuck on version 8 or Rust for not having stable language server? /s
Or, to phrase it differently, "this is an issue" - "it was an issue in 2018" - "no, you don't get it, it's a valid criticism because nothing can ever be improved". You see how flawed this argument is?
I'm so tired of these low effort replies here that it's just sad, in technical conversations in other contexts I'd equally defend another language when someone blatantly misconstrues the facts. I don't have a horse in this race at this point, it's simply annoying to try to converse productively when the quality of replies is this low. I should probably spend time elsewhere.
[0]: https://github.com/dotnet/vscode-csharp (this is _not_ DevKit, which is optional, the actual language support like Roslyn language server is part of the base extension, and you really don't need DevKit which has "extra VS-style accommodations" most of which can be done with different extensions, if that's what you want)
Iff you use Microsoft's builds of VS Code. Just like back in 2018.
> At the same time, Rider uses its own homegrown debugger, that works even better and has nice time-travel capability.
So... still useless for the rest of us.
Why? How does the situation look in Java?
It also seems you have not read the original reply. So to reiterate, there is https://github.com/Samsung/netcoredbg too.
In any case, I assume none of this has any use to you and the reply is posted simply as bad faith engagement, as it continues to happen whenever a piece of software that uses .NET is mentioned, because usually very few people within community have/take issue with the current (rich) tooling options.
Note how many comments here and in similar submissions completely ignore the topic at hand and instead try to criticize the points that their authors assume are an issue with .NET itself.
jdb is part of OpenJDK, and doesn't try to implement any such restrictions. Neither does gdb, for that matter.
But there is also a cultural difference. .NET libraries (including the standard library) are notoriously poor at implementing useful .ToString() overrides, because it's all designed to assume that you will use a debugger.
For comparison, Scala and Rust have cultures that emphasize printf-friendliness, and I rarely have to reach for a debugger at all. The difference it makes for my sanity is immense (as someone who wasted years on the shitshow that is .NET).
> It also seems you have not read the original reply. So to reiterate, there is https://github.com/Samsung/netcoredbg too.
I spent way too long trying to get netcoredbg to work, and couldn't get it to do much of anything. Maybe it's less of a shitshow now? Given that your original reply wasn't "yeah nobody uses the MS debugger anyway", I somehow doubt it.
> and the reply is posted simply as bad faith engagement
I mostly get annoyed when I see bad faith arguments that old problems are irrelevant because they're old, even if the problem has never actually been addressed.
This got me curious. Turns out there exists an actively maintained fork of the official C# extension that comes with NetCoreDbg instead: https://github.com/muhammadsammy/free-vscode-csharp
I was able to successfully debug simple async code with it after installing the vsix, disabling the official one and restarting VS Code without changing any other settings.
So, for the trivial case it works. Submitted issues do indicate further compatibility problems like not supporting "Debug.Write*" methods (just use a logger or Console.Write* I guess?) or instability when bridging this extension to something that isn't VS Code.
Still, someone even managed to get it to work with Cursor: https://github.com/dgokcin/dotnet-cursor-debugging-with-brea...
> For comparison, Scala and Rust have cultures that emphasize printf-friendliness, and I rarely have to reach for a debugger at all. The difference it makes for my sanity is immense (as someone who wasted years on the shitshow that is .NET).
This is the first time I hear someone tout print-based debugging as an advantage. The approach F# takes with printfn "%A" might be more to your taste. Otherwise, DebuggerDisplay and DebuggerTypeProxy are there for a reason, and I don't understand the case for not using a debugger. But if you really want to, there are many ways to make the output pretty. Making a simple '.Print()' extension method that will do indented JsonSerializer.Serialize is already a start. Records also come with default ToString implementation.
https://thegraybook.vvvv.org/reference/getting-started/dotne...
Really looking forward to this one.
What do you mean by that? A compiler backend isn't even exposed to high level concepts like traits.