Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accessing associated consts (and uncalled function pointers) from a non-const trait #56

Closed
geofft opened this issue Aug 29, 2020 · 8 comments

Comments

@geofft
Copy link

geofft commented Aug 29, 2020

(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 specify const 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

#[repr(C)]
struct FFIPlugin {
    major: u32,
    minor: u32,
    do_stuff: Option<extern "C" fn()>,
}

This basically perfectly matches Rust's trait concepts, so it'd be nicest to implement this by making a trait:

trait Plugin {
    const VERSION: (u32, u32);
    fn do_stuff();
}

extern "C" fn do_stuff_callback<T: Plugin>() {
    T::do_stuff()
}

const fn make_ffi_plugin<T: Plugin>() -> FFIPlugin {
    FFIPlugin {
        major: T::VERSION.0,
        minor: T::VERSION.1,
        do_stuff: Some(do_stuff_callback::<T>),
    }
}

You want make_ffi_plugin to 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):

impl Plugin for MyPlugin {
    const VERSION: (u32, u32) = (1, 0);
    fn do_stuff() {println!("Hello world");}
}

#[no_mangle]
pub static FFI_PLUGIN: FFIPlugin = make_ffi_plugin::<MyPlugin>();

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 Sized on 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 calling do_stuff from constant context - nor are we calling any function pointers. We just need references to them. (And we very much need Plugin to 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 ?const syntax from rust-lang/rfcs#2632? It's not clear to me whether const fn make_ffi_plugin<T: ?const Plugin>() would be allowed to access associated consts + function pointers involving type T.

@RalfJung
Copy link
Member

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 a const 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.

@rodrimati1992
Copy link

rodrimati1992 commented Aug 29, 2020

What I use for what make_ffi_plugin does is to use an associated constant of a generic type.

full working example

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.

@geofft
Copy link
Author

geofft commented Aug 29, 2020

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 :)

@RalfJung
Copy link
Member

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. ;)

@rodrimati1992
Copy link

rodrimati1992 commented Aug 29, 2020

Associated constants on stable can do a few more things than const fn can do, like call std::mem::transmute (the function is usable in constants from the current stable release, 1.46.0).

@Amanieu
Copy link
Member

Amanieu commented Aug 29, 2020

Another example: https://github.com/Amanieu/parking_lot/blob/74218898303e2ccbc57b864ad868b859f57e1fb8/lock_api/src/mutex.rs#L147

@geofft
Copy link
Author

geofft commented Aug 30, 2020

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_operations that 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, the read syscall checks for f_op->read and calls that, otherwise it calls some helper function on f_op->read_iter if that's there, otherwise it returns EINVAL. We can't provide a default read impl that would cause the syscall to fall back to read_iter.)

We're currently handling that with multiple traits and a builder pattern. The base FileOperations trait just defines the open() method. Then there are Read, Write, etc. traits that define the corresponding methods. Then there's a base Builder that just sets open: Some(open_callback::<T>) and everything else to None, and implementations like impl<T: Read> Builder<T> {pub const fn read(mut self)} fills in various operations. This is safe (because you cannot call builder.read() if you don't impl Read), though a tiny bit ugly.

I don't think I can do that in a static initializer - while you can call if in a static initializer, there's no obvious way to do something like read = 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 with min_specialization, and switching from #![feature(const_fn)] to #![feature(specialization)] seems like a downgrade because specialization is 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 out READ and WRITE in impl 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.)

@josephlr
Copy link

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

See rust-lang/rust#93827

@oli-obk oli-obk closed this as completed Apr 19, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants