4 ms·
I'm thinking of using C++ for a personal project specifically for the lambdas and RAII. I have a case where I need to create a static templated lambda to be pa
by nesarkvechnep 10mo ago
I'm thinking of using C++ for a personal project specifically for the lambdas and RAII.
I have a case where I need to create a static templated lambda to be passed to C as a pointer. Such thing is impossible in Rust, which I considered at first.
- pornel 10mo agoYeah, 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.
- nesarkvechnep 10mo agoI know about this technique but it uses too much unsafe for my taste. Not that it's bad or anything, just a personal preference.
- pornel 10mo agoIt 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)
- nesarkvechnep 10mo agoYes 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.
- skavi 10mo ago> 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&edition=2024&gist=c39a6aa8d5d3d26c51c1aade628602c5 https://play.rust-lang.org/?version=nightly&mode=debug&editi... (did this on mobile, so please excuse any messiness).
- eqvinox 10mo agoIf 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.
- MindSpunk 10mo agoThis 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.
- queuebert 10mo agoIn Rust, could you instead use a templated struct wrapping a function pointer along with #[repr(C)]?