5 ms·
If there really serious about this, they'll realize they need to add erased but not specialized types to rust, and existential types. If you can replace v
by Ericson2314 3y ago
If there really serious about this, they'll realize they need to add erased but not specialized types to rust, and existential types.
If you can replace
void qsort(void base[.size * .nmemb], size_t nmemb, size_t size,
int (*compar)(const void [.size], const void [.size]))
with
extern "C" fn qsort<erase T>(base: *const T, nmemb: usize, compar: fn(*const T, *const T) -> std::ffi::c_int)
such that `qsort` is still single a regular C function, not something that corresponds to many separate C functions, many good things are possible.
In particular for C++, being able to do
struct MyVTable<erase T> {
virt_method_0: fn(*const T) -> bool,
virt_method_2: fn(*const T, usize) -> (usize, usize),
};
struct MyType<erase R> {
vtable: *const MyVTable<MyType<R>>,
rest_of_fields: R,
};
Opens a lot of doors. People while whine because it is exposing the C++ ABI in the Rust API, but that's fine. That's what `private` is there to fix.
- eximius 3y agoWhat does `erase T` mean here? "We don't deref it so don't worry about the type, just erase it?"
- jcranmer 3y agoNormally, if you have generics, Rust makes copies of the function (/struct/enum/etc.) (aka "monomorphization"), one for each type that you use with it. The idea is to have a generic function but only one copy of the function, presumably because none of the code actually relies on the details of T.
- TheCoreh 3y agoIsn't this just `&dyn Trait`? Would it be a stack allocated, owning version of that, that can be moved? A pure `dyn Trait`?
- Ericson2314 3y agoNo. &mut dyn Foo is sugar for something like struct DynFoo<erase T> { vtable: &mut VTableFoo<T> ptr: &mut T } This is the underlying feature. `dyn` is a bit of an ad-hoc hack around not having this feature.
- eximius 3y agoI wonder if we really need an explicit syntax or if the compiler just need to disable monomorphization for functions that don't use the interior type e.g., only passing around references like this example. Without trait bounds there isn't much you can do with it anyway besides generic code like this. I guess the question is 'does Rust emit' or elide an unused generic function with unused interior types?
- Ericson2314 3y agoExplicit syntax is much better. "Just infer it" means we have to trace entire call graphs, because we have to know if our callees need monomorphization in order to know if we need monomorphization.
- jcranmer 3y agoIt's... not the most compelling feature in the world. The main use case I think I see is slightly easing the burden of writing external function signatures for a function that passes a user-definable type to a function callback parameter like qsort does. But there's relatively few of those functions in existence, and often times, you want a more ergonomic function definition anyways (see e.g. https://news.ycombinator.com/item?id=39267277 https://news.ycombinator.com/item?id=39267277 for qsort). The other major use case is if you've got something like a Vec<T> where several of the helper methods can be erasable. But it's already possible to do this with helper functions--the standard library takes advantage of it--so it's not entirely clear that you need a language-level feature for this use case.
- ithkuil 3y agoPolymorphic type erasure is the technique where the type is only used during type checking but has no impact on the generated code, namely the compiler doesn't emit specialized code for each instance of the type.
- throwaway894345 3y agoCan I get an ELI5 on erased, specialized, and existential types?
- Ericson2314 3y agospecialized: always substitute every type variable until they are all gone, creates many different "specializations" or "instantiations". This is like Rust erased: just ignore the type variables for compilation. This is like the typescript -> javascript compilation. (Also how Java, Scala, Haskell, OCaml, etc. type paramters work) Existential: "there exists a type such that..." see https://en.wikipedia.org/wiki/Existential_quantification https://en.wikipedia.org/wiki/Existential_quantification https://en.wikipedia.org/wiki/System_F https://en.wikipedia.org/wiki/System_F
- avgcorrection 3y agoIs “erased” how Haskell typeclasses work?
- tome 3y agoNo, the opposite. They're either specialized (when they can be) or (effectively) existential (when they must be). That is, the compiler either inlines the specific definition of the type class methods if it knows them, or passes them around at run time.
- Ericson2314 3y agoAgreed, except I would not call it "existential" C++ stores references to dictionaries from/with the things they describe, that's "existential" Haskell passes in extra parmeters that are dictionaries and doesn't (except for user-written code that does) keep around extra references to them. That's more like "universal" than "existential".
- Ericson2314 3y agoErased is how types in Haskell work. Type classes are compiled as "dictionaries": structs of methods. Sometimes they can specialize them (just pull the thing out of the dictionary statically), but in general they become that extra parameters at run time.
- johncolanduoni 3y agoCan't you do this today by just writing a macro that generates a generic wrapper that casts/transmutes the pointers to void* before/after running the function? It'll almost certainly get inlined at that point so I'm not sure how much having this functionality native to Rust helps.
- Ericson2314 3y agoYes, but to me that's no different to saying "we don't need structs", we can do everything with `[u8; N]` can casts. Yes, that's true, but it's not type safe, and I don't really want to program that way.
- Ericson2314 3y agoYes, but to me that's no different to saying "we don't need structs, we can do everything with `[u8; N]` and casts". Yes, that's true, but it's not type safe, and I don't really want to program that way.
- johncolanduoni 3y agoHow is it not just as typesafe? Rust will enforce the same typing constraints on the arguments to qsort either way. You're making attestations about how the C code behaves that the Rust compiler cannot type-check for you, but both methods have that fundamental issue.
- Ericson2314 3y agoI won't be able to explain this succinctly. Sorry to be an asshole, but you are in Plato's cave of only having monomorphized types. The point of erased types is to be able to put in the language invariants that hitherto are only in our heads, but unless you have a typed-based mental model for what makes vtables/dictionaries safe (say, based on experience in other languages) you won't have ideas you can't write down, and thus you won't know what is missing. I wish there was a good blog-sized resource I could point you to that did explain it instead, but I don't know of any off-hand. I guess, try to wrote `dyn Trait` things (like a heterogenous collection) without `dyn Trait`, and then when it doesn't work without unsafe Rust, you will see what I mean.
- MaulingMonkey 3y agoYour proposal seems like an unnecessary half measure to me. If you want to go fully ergonomic, you can expose: pub unsafe fn qsort<E, C: FnMut(&E, &E) -> core::cmp::Ordering>(elements: &mut [E], mut comp: C) In rust as currently stands: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=f22c513498d39d1c74fab1be683ab6dd https://play.rust-lang.org/?version=stable&mode=debug&editio... On the other hand, both this wrapper and yours are counterproductive if the element size is dynamic (e.g. perhaps you're dealing with some nonsense like:) struct ITableColumn { virtual ~ITableColumn() {} virtual void* base() = 0; virtual size_t stride() const = 0; virtual int (*)(void*, void*) get_comparison_func() const = 0; };
- Ericson2314 3y agoI wrote my example based on the libc quicksort. Yes, in greenfield code other things are better. But it's useful to look at things to bind via FFI as they curently exist to stress-test our current abilities.
- MaulingMonkey 3y agoTo be clear, my playground link is calling libc's qsort. The FFI definition: extern "C" { fn qsort(ptr: *mut c_void, count: size_t, size: size_t, comp: extern "C" fn(*const c_void, *const c_void) -> c_int); } And the call: unsafe { qsort(ptr, len, size_of::<E>(), comp_wrapper::<E, C>) }; Are both hidden away in the body of the wrapper fn.
- Ericson2314 3y agoI am writing a single abstract declaration (I suppose I should have used `extern "C" {...}` to be clear) that one can only use safely. You are writing some unsafe code with safe wrapper. These are not the same.
- MaulingMonkey 3y ago> I am writing a single abstract declaration (I suppose I should have used `extern "C" {...}` to be clear) that one can only use safely. Your abstract declaration still wouldn't be safe. Remaining unsafety includes: • possible dangling pointers • possible incorrect lengths • possible libc bugs with ZST elements • undefined behavior if the sort fn misbehaves - (recently reported as a security issue against glibc because that being UB is dumb even if allowed by the standard: https://news.ycombinator.com/item?id=39264396 https://news.ycombinator.com/item?id=39264396 ) This is why I call it a "half measure". I also fail to see how your proposed abstract declaration would simplify either my wrapper, or other code that would actually bother to use the raw FFI definition in any significant way. This is why I further call it "unnecessary". It also fails to specify which underlying FFI parameter size_of::<T>() would actually be passed into. > You are writing some unsafe code with safe wrapper. My wrapper remains `unsafe` as well, but it ameliorates everything it reasonably can. > These are not the same. No, but my wrapper demonstrates an actual use case of your raw FFI definition... and shows the actual concerns of surrounding code that aren't significantly helped by your declaration.