Repository navigation
Accessing associated consts (and uncalled function pointers) from a non-const trait #56
Description
Activity
The open question here is one of type system design -- that's why this all already works on nightly; if we do not have to worry about a sane type system that ensures some form of const-safety, this is an easy problem.
Specifically, the hard part is: we also will eventually want to have function pointers that can be called in
const. And maybe we even want to have function pointers that definitely point to aconst fn, even at runtime. The blocker for stabilizing what currently works on nightly is being sure that this does not prevent properly solving these other problems later.The "use associated const of non-const trait impl" is the exact same problem. That's why I think we should use the same solution for everything, and the current RFC makes that hard.
What I use for what
make_ffi_plugindoes is to use an associated constant of a generic type.struct MakeFFIPlugin<T>(T); impl<T> MakeFFIPlugin<T> where T: Plugin { const MAKE: FFIPlugin = FFIPlugin { major: T::VERSION.0, minor: T::VERSION.1, do_stuff: Some(do_stuff_callback::<T>), }; } struct Foo; impl Plugin for Foo { const VERSION: (u32, u32) = (1, 0); fn do_stuff() { println!("Hello!"); } } #[no_mangle] static FOO_PLUGIN: FFIPlugin = MakeFFIPlugin::<Foo>::MAKE;
I also use another workaround for passing the associated constants of a trait to
const fns (while guaranteeing that they are the associated constants of a particular type and trait), but this case is simple enough that I didn't think it was necessary to mention.Interesting, thanks @rodrimati1992 - I'm a little surprised that works since your initializers do basically the same amount of compile-time computation, but I'll try switching to that. (I guess the answer is they don't actually do computation, and your approach makes that explicit.)
And yeah, wanting to avoid something that turns out to be an unsupportable design is exactly why we want to use stable :)
And yeah, wanting to avoid something that turns out to be an unsupportable design is exactly why we want to use stable :)
Fair, but then just stabilizing what we currently have won't help. ;)
Associated constants on stable can do a few more things than
const fncan do, like callstd::mem::transmute(the function is usable in constants from the current stable release, 1.46.0).- Reacted by Geoffrey Thomas and Joe Richey
Ah, right, it turns out we're trying to do something more complicated with the type system.
There's a large number of functions in
struct file_operationsthat you can implement, and most users won't implement all of them. In C, you'd set them to NULL. We can't provide a default trait impl, because there generally isn't one that acts like NULL would. (For instance, thereadsyscall checks forf_op->readand calls that, otherwise it calls some helper function onf_op->read_iterif that's there, otherwise it returns EINVAL. We can't provide a defaultreadimpl that would cause the syscall to fall back toread_iter.)We're currently handling that with multiple traits and a builder pattern. The base
FileOperationstrait just defines theopen()method. Then there areRead,Write, etc. traits that define the corresponding methods. Then there's a baseBuilderthat just setsopen: Some(open_callback::<T>)and everything else toNone, and implementations likeimpl<T: Read> Builder<T> {pub const fn read(mut self)}fills in various operations. This is safe (because you cannot callbuilder.read()if you don'timpl Read), though a tiny bit ugly.I don't think I can do that in a static initializer - while you can call
ifin a static initializer, there's no obvious way to do something likeread = if T impl Read {Some(read_callback::<T as Read>)} else {None}- that's not valid code and I don't think there is any way to make it valid.I think I can do it with
specialization, but I can't figure out how to do it withmin_specialization, and switching from#![feature(const_fn)]to#![feature(specialization)]seems like a downgrade becausespecializationis unsound. :)Alternatively, I can do it by asking users to write associated consts of type
Option<fn>instead of member functions, which does seem to work, but that's kind of weird. Here's a worked-out example - try commenting outREADandWRITEinimpl Plugin for MyPlugin. It does work on stable and we could probably hide the ugliness behind a macro. (Which would avoid the builder pattern, tbh.)I think this issue can be closed, the example in the original post now compiles on Rust 1.61 (the current beta): https://play.rust-lang.org/?version=beta&mode=debug&edition=2021&gist=ffffcd69b4592ae252f2549ed1f82234
(if there's already an issue for this, please let me know - I've been looking around and I haven't quite seen it.)
In stable, you currently can't access associated consts of a trait in a const fn. I see some discussion in the hidden comments of rust-lang/rust#57563 and in #1 about this problem, but they seem geared towards the idea of having a
const trait. I would like to be able to access associated consts in a non-const trait.Specifically, what I really want is to be able to get a function pointer from a trait, but not actually call the function. (This seems to me like it's basically equivalent to getting an associated const - it's something statically known at compile time.) And I want that function itself to be non-const. I see some discussion in rust-lang/rust#63997, but again, that seems like it's envisioning passing pointers to other const functions into the const function and calling them. I just want the pointer itself, which is const, and I want to let the function
Both of these work in nightly with
#![feature(const_fn)], but since the discussion in the above issues is leaning towards const traits and pointers to const fns, I wanted to make sure there was a plan for regular traits and (const) pointers to non-const fns. (Also, it seems to me like this specific case could be stabilized now, though I do see the comments about whether this would imply stabilizing trait bounds without requiring you to specifyconst impl.)The motivating example here is providing plugins to some existing C program via FFI. Usually you'd register these by making some structure with some metadata and some function pointers, like
This basically perfectly matches Rust's trait concepts, so it'd be nicest to implement this by making a trait:
You want
make_ffi_pluginto be a const fn so that you can assign it to a static. Sometimes you want this because the interface for loading plugins is to declare a static with a specific name, sometimes you just want this for security hardening (making sure that the resulting FFIPlugin struct is in the read-only section of program memory with a static lifetime and not on the heap):This works great in nightly Rust (playground link). We use this for Linux kernel ops structs (where the
extern "C"callback function is more complicated than the above examples, and maps appropriately between raw pointers provided by the kernel and safe Rust types in our code).In stable Rust, it complains "trait bounds other than
Sizedon const fn parameters are unstable" and "function pointers in const fn are unstable". I think that's because of the two issues I linked above. But we're not fully using the trait inside the const fn - we're definitely not callingdo_stufffrom constant context - nor are we calling any function pointers. We just need references to them. (And we very much needPluginto be a non-const trait.)Would it be possible to stabilize this specific use case? (If this is the sort of thing that wants implementation help, I'd be happy to try!)
Is this resolved by the
?constsyntax from rust-lang/rfcs#2632? It's not clear to me whetherconst fn make_ffi_plugin<T: ?const Plugin>()would be allowed to access associated consts + function pointers involving type T.