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

Yeah, Rust closures that capture data are fat pointers { fn*, data* }, so you need an awkward dance to make them thin pointers for C.

    let mut state = 1;
    let mut fat_closure = || state += 1;
    let (fnptr, userdata) = make_trampoline(&mut &mut fat_closure);

    unsafe {
        fnptr(userdata);
    }

    assert_eq!(state, 2);

    use std::ffi::c_void;
    fn make_trampoline<C: FnMut()>(closure: &mut &mut C) -> (unsafe fn(*mut c_void), *mut c_void) {
        let fnptr = |userdata: *mut c_void| {
            let closure: *mut &mut C = userdata.cast();
            (unsafe { &mut *closure })()
        };
        (fnptr, closure as *mut _ as *mut c_void)
    }
    
It requires a userdata arg for the C function, since there's no allocation or executable-stack magic to give a unique function pointer to each data instance. OTOH it's zero-cost. The generic make_trampoline inlines code of the closure, so there's no extra indirection.

> Rust closures that capture data are fat pointers { fn, data }

This isn’t fully accurate. In your example, `&mut C` actually has the same layout as usize. It’s not a fat pointer. `C` is a concrete type and essentially just an anonymous struct with FnMut implemented for it.

You’re probably thinking of `&mut dyn FnMut` which is a fat pointer that pairs a pointer to the data with a pointer to a VTable.

So in your specific example, the double indirection is unnecessary.

The following passes miri: https://play.rust-lang.org/?version=nightly&mode=debug&editi...

(did this on mobile, so please excuse any messiness).


This is a problem for all capturing closures though, not just Rust's. A pure fn-ptr arg can't have state, and if there's no user data arg then there's no way to make a trampoline. If C++ was calling a C API with the same constraint it would have the same problem.

Well, capturing closures that are implemented like C++ lambdas or Rust closures anyway. The executable stack crimes do make a thin fn-ptr with state.


If Rust has a stable ABI on where the data* is in the function arguments (presumably first?), you don't need to do anything if it matches the C code's expected function signature including the user context arg.

Unfortunately a lot of existing C APIs won't have the user arg in the place you need it, it's a mix of first, last, and sometimes even middle.


I know about this technique but it uses too much unsafe for my taste. Not that it's bad or anything, just a personal preference.

It can be done in 100% safe code as far as Rust is concerned (if you use `dyn Fn` type instead of c_void).

The only unsafe here is to demonstrate it works with C/C++ FFI (where void* userdata is actually not type safe)


Yes but my problem wasn’t with the user data pointer but the fact that I needed a STATIC generic lambda. Static because the C library then forks and continues to call the lambda in the new process but I also type based conversions in it.

There's Rust for Dreamcast (https://dreamcast.rs) via Rust's GCC backend.

If you create wrappers that provide additional type information, you do get extra safety and nicer interfaces to work with.

Apple is extending Swift specifically for kernel development.

In Rust dev, I haven't needed Valgrind or gdb in years, except some projects integrating C libraries.

Probably kernel dev isn't as easy, but for application development Rust really shifts majority of problems from debugging to compile time.


My current project is C++ backend. I do a lot of debugging but all of it concerns business logic, some scientific calculations and the likes. In this situations having Rust will give me exactly zero benefits. As for "safety". I am a practical man and I pay my own money for development. Being able to use modern C++ I have forgotten when was the last time I had any memory related issue. My backends run for years serving many customers with no complaints in this department. Does not mean of course they're really really safe but I sleep well ;)

There's always someone willing to write COBOL for the right premium.

I'm working on Rust projects, so I may have incomplete picture, but I'm from what I see when devs have a choice, they prefer working with Rust over C++ (if not due to the language, at least due to the build tooling).


Writing C++ is easier than writing Rust. But writing safe multithreaded code in C++?

I don't want to write multithreaded C++ at all unless I explicitly want a new hole in my foot. Rust I barely have any experience with, but it might be less frustrating than that.


Anecdotally, my "wow, this Rust business might really go somewhere" moment was when I tried adding multithreading to a random tool I made (dispatching tasks to new threads).

Multithreading had not been planned or architected for, it took 30 min, included the compiler informing me I couldn't share a hashmap with those threads unsynchronised, and informing me on how to fix it.


I've had similar experience when the compiler immediately found unsynchronized state deep inside a 3rd party library I've been using. It was a 5-minute fix for what otherwise could have been mysterious unreproducible data corruption.

These days even mobile phones have multicore CPUs, so it's getting hard to find excuses for single-threaded programs.


The idea behind the safe/unsafe split is to provide safe abstractions over code that has to be unsafe.

The unsafe parts have to be written and verified manually very carefully, but once that's done, the compiler can ensure that all further uses of these abstractions are correct and won't cause UB.

Everything in Rust becomes "unsafe" at some lower level (every string has unsafe in its implementation, the compiler itself uses unsafe code), but as long as the lower-level unsafe is correct, the higher-level code gets safety guarantees.

This allows kernel maintainers to (carefully) create safe public APIs, which will be much safer to use by others.

C doesn't have such explicit split, and its abstraction powers are weaker, so it doesn't let maintainer create APIs that can't cause UB even if misused.


m68k Linux is supported by Rust, even in the LLVM backend.

Rust also has an experimental GCC-based codegen backend (based on libgccjit (which isn't used as a JIT)).

So platforms that don't have either LLVM nor recent GCC are screwed.


how on earth is linux being compiled for platforms without a GCC?

additionally, I believe the GCC backend is incomplete. the `core` library is able to compile, but rust's `std` cannot be.


>nor recent GCC are screwed.

Not having a recent GCC and not having GCC are different things. There may be architectures that have older GCC versions, but are no longer supported for more current C specs like C11, C23, etc.


I don't believe Rust for Linux use std. I'm not sure how much of Rust for Linux the GCC/Rust effort(s) are able to compile, but if it was "all of it" I'm sure we'd have heard about it.

What's really nice is where you don't need defensive programming in Rust.

If your function gets ownership of, or an exclusive reference to an object, then you know for sure that this reference, for as long as it exists, is the only one in the entire program that can access this object (across all threads, 3rd party libraries, recursion, async, whatever).

References can't be null. Smart pointers can't be null. Not merely "can't" meaning not allowed and may throw or have a dummy value, but just can't. Wherever such type exists, it's already checked (often by construction) that it's valid and can't be null.

If your object's getter lends an immutable reference to its field, then you know the field won't be mutated by the caller (unless you've intentionally allowed mutable "holes" in specific places by explicitly wrapping them in a type that grants such access in a controlled way).

If your object's getter lends a reference, then you know the caller won't keep the reference for longer than the object's lifetime. If the type is not copyable/cloneable, then you know it won't even get copied.

If you make a method that takes ownership of `self`, then you know for sure that the caller won't be able to call any more methods on this object (e.g. `connection.close(); connection.send()` won't compile, `future.then(next)` only needs to support one listener, not an arbitrary number).

If you have a type marked as non-thread safe, then its instances won't be allowed in any thread-spawning functions, and won't be possible to send through channels that cross threads, etc. This is verified globally, across all code including 3rd party libraries and dynamic callbacks, at compile time.


Article mostly focuses on code practices to avoid making logical mistakes when iterating on your program.

I fully agree with the actually great thing being what not to have to look out for and my first thought when seeing the headline was: "Doesn't the type system handle most of that stuff?"

In other languages I get most of the benefits by sticking to functional programming practices and not mutating stuff all over the place. Rust's type system sort of encodes that, and maybe a little more, by making safe mutation a known non-interfering thing.


I don’t see how your comment is relevant, none of things you mention are covered in the article. This was an article about logic bugs that can exist in spite of the borrow checker.

You do the same thing, if that's really the architecture you need.

Channels communicating between persistent workers are fine when you need decoupled asynchronous operation like that. However, channels and detached coroutines are less appropriate in a bunch of other situations, like fork-join, data parallelism, cancellation of task trees, etc. You can still do it, but you're responsible for adding that structure, and ensuring you don't forget to wait for something, don't forget to cancel something.


You can accomplish fork-join, data parallelism, and cancellation of task trees in a with `errgroup` in Go (which provides a way to approach structured concurrency).

So at least those are a subset of Go's concurrency model.


> So at least those are a subset of Go's concurrency model.

That's why the article about structured concurrency compared it to goto. Everything is a subset of goto. It can do everything that structured programming can do, and more! With goto you can implement your own conditions, switches, loops, and everything else.

The problem is not the lack of power, but lack of enforced structure. You can implement fork-join, but an idiomatic golang implementation won't stop you from forking and forgetting to join.

Another aspect of it is not really technical, but conventions that fell out of what the language offers. It's just way more common to DIY something custom from a couple of channels, even if it could be done with some pre-defined standard pattern. To me, this makes understanding behavior of golang programs harder, because instead of seeing something I already know, like list.par_iter().map().collect(), I need to recognize such behavior across a larger block of code, and think twice whether each channel-goroutine dance properly handles cancellations, thread pool limits, recursive dependencies, is everything is correctly read-only/atomic/locked, and so on.


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

Search: