Writing to a field in a MaybeUninit structure? - rust

I'm doing something with MaybeUninit and FFI in Rust that seems to work, but I suspect may be unsound/relying on undefined behavior.
My aim is to have a struct MoreA extend a struct A, by including A as an initial field. And then to call some C code that writes to the struct A. And then finalize MoreA by filling in its additional fields, based on what's in A.
In my application, the additional fields of MoreA are all integers, so I don't have to worry about assignments to them dropping the (uninitialized) previous values.
Here's a minimal example:
use core::fmt::Debug;
use std::mem::MaybeUninit;
#[derive(Clone, Copy, PartialEq, Debug)]
#[repr(C)]
struct A(i32, i32);
#[derive(Clone, Copy, PartialEq, Debug)]
#[repr(C)]
struct MoreA {
head: A,
more: i32,
}
unsafe fn mock_ffi(p: *mut A) {
// write doesn't drop previous (uninitialized) occupant of p
p.write(A(1, 2));
}
fn main() {
let mut b = MaybeUninit::<MoreA>::uninit();
unsafe { mock_ffi(b.as_mut_ptr().cast()); }
let b = unsafe {
let mut b = b.assume_init();
b.more = 3;
b
};
assert_eq!(&b, &MoreA { head: A(1, 2), more: 3 });
}
Is the code let b = unsafe { ... } sound? It runs Ok and Miri doesn't complain.
But the MaybeUninit docs say:
Moreover, uninitialized memory is special in that the compiler knows that it does not have
a fixed value. This makes it undefined behavior to have uninitialized data in a variable
even if that variable has an integer type, which otherwise can hold any fixed bit pattern.
Also, the Rust book says that Behavior considered undefined includes:
Producing an invalid value, even in private fields and locals. "Producing" a value happens any time a value is assigned to or read from a place, passed to a function/primitive operation or returned from a function/primitive operation. The following values are invalid (at their respective type):
... An integer (i*/u*) or ... obtained from uninitialized memory.
On the other hand, it doesn't seem possible to write to the more field before calling assume_init. Later on the same page:
There is currently no supported way to create a raw pointer or reference to a field of a struct
inside MaybeUninit. That means it is not possible to create a struct by calling
MaybeUninit::uninit::() and then writing to its fields.
If what I'm doing in the above code example does trigger undefined behavior, what would solutions be?
I'd like to avoid boxing the A value (that is, I'd like to have it be directly included in MoreA).
I'd hope also to avoid having to create one A to pass to mock_ffi and then having to copy the results into MoreA. A in my real application is a large structure.
I guess if there's no sound way to get what I'm after, though, I'd have to choose one of those two fallbacks.
If struct A is of a type that can hold the bit-pattern 0 as a valid value, then I guess a third fallback would be:
Start with MaybeUninit::zeroed() rather than MaybeUninit::uninit().

Currently, the only sound way to refer to uninitialized memory—of any type—is MaybeUninit. In practice, it is probably safe to read or write to uninitialized integers, but that is not officially documented. It is definitely not safe to read or write to an uninitialized bool or most other types.
In general, as the documentation states, you cannot initialize a struct field by field. However, it is sound to do so as long as:
the struct has repr(C). This is necessary because it prevents Rust from doing clever layout tricks, so that the layout of a field of type MaybeUninit<T> remains identical to the layout of a field of type T, regardless of its adjacent fields.
every field is MaybeUninit. This lets us assume_init() for the entire struct, and then later initialise each field individually.
Given that your struct is already repr(C), you can use an intermediate representation which uses MaybeIninit for every field. The repr(C) also means that we can transmute between the types once it is initialised, provided that the two structs have the same fields in the same order.
use std::mem::{self, MaybeUninit};
#[repr(C)]
struct MoreAConstruct {
head: MaybeUninit<A>,
more: MaybeUninit<i32>,
}
let b: MoreA = unsafe {
// It's OK to assume a struct is initialized when all of its fields are MaybeUninit
let mut b_construct = MaybeUninit::<MoreAConstruct>::uninit().assume_init();
mock_ffi(b_construct.head.as_mut_ptr());
b_construct.more = MaybeUninit::new(3);
mem::transmute(b_construct)
};

It is now possible (since Rust 1.51) to initialize fields of any uninitialized struct using the std::ptr::addr_of_mut macro. This example is from the documentation:
You can use MaybeUninit, and the std::ptr::addr_of_mut macro, to
initialize structs field by field:
#[derive(Debug, PartialEq)] pub struct Foo {
name: String,
list: Vec<u8>, }
let foo = {
let mut uninit: MaybeUninit<Foo> = MaybeUninit::uninit();
let ptr = uninit.as_mut_ptr();
// Initializing the `name` field
unsafe { addr_of_mut!((*ptr).name).write("Bob".to_string()); }
// Initializing the `list` field
// If there is a panic here, then the `String` in the `name` field leaks.
unsafe { addr_of_mut!((*ptr).list).write(vec![0, 1, 2]); }
// All the fields are initialized, so we call `assume_init` to get an initialized Foo.
unsafe { uninit.assume_init() } };
assert_eq!(
foo,
Foo {
name: "Bob".to_string(),
list: vec![0, 1, 2]
}
);

Related

Why does Default derived on an enum not apply to references to the enum?

I have an enum that has Default derived on it, say for example:
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum Enum<T> {
#[default] None,
One(T),
Two(T),
}
Vec has methods like last() which return an Option<&T>. But when I call unwrap_or_default() on the Option<&T>, I get the following error:
22 | println!("{:?}", v.last().unwrap_or_default());
| ^^^^^^^^ ----------------- required by a bound introduced by this call
| |
| the trait `Default` is not implemented for `&Enum<i8>`
|
= help: the trait `Default` is implemented for `Enum<T>`
note: required by a bound in `Option::<T>::unwrap_or_default`
I can work around this in one of two ways:
Use unwrap_or() and provide a reference to the default element manually:
v.last().unwrap_or(&Enum::None)
Implement Default for &Enum.
impl<'a> Default for &'a Enum2 {
fn default() -> &'a Enum2 { &Enum2::None }
}
But I don't realize why I should have to. Is there a separate trait I should derive to use the tagged default element correctly here?
Playground
Context: the goal was to output "None" if the list was empty and Enum::Display(val) from Some(val) for the last element, within a sequence of format arguments.
match self.history.last() { None => "None", Some(val) => val }
is illegal: the types of the match arms must match. Many other options don't work due to val being borrowed and self not being Copy. Defining a separate default for Enum was an easily-thought-of option to avoid extra string formatting/allocation.
What should Default return for &Enum?
Remember, a reference must point to some valid data, and does not keep said data alive.
So even if the Default implementation would allocate a new object, there is nowhere to keep it alive, because the returning reference wouldn't keep it alive. The only way is to store the default element somewhere globally; and then it would be shared for everyone. Many objects are more complicated and cannot be simply stored globally, because they are not const-initializable. What should happen then?
The fact that a reference does not implement Default automatically is intentional and with good reasons. unwrap_or_default is simply the wrong function to call if your contained value is a reference. Use match or if let to extract the value without having to create a fallback value. unwrap_or is also a valid choice if you must absolutely have a fallback value, although I don't see how this is easily possible to implement with a reference without falling back to global const values. Which is also fine, if it fits your usecase.
In your case, it's of course fine to derive Default manually and is probably what I would do if I absolutely had to. But to me, requiring this in the first place is most likely a code smell in the first place.
For enums that are that simple, it's actually an anti-pattern to use a non-mutable reference instead of a value. References are of size 64 in a 64bit system, while an enum of with three simple values is most likely a 8 bits large. So using a reference is an overhead.
So in this case, you can use copied() to query the object by-value. This is most likely even faster than querying it by reference.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum Enum<T> {
#[default]
None,
One(T),
Two(T),
}
fn main() {
let v: Vec<Enum<u32>> = vec![];
println!("{:?}", v.last().copied().unwrap_or_default());
}
None
Either way, I think the fact that you have this question in the first place indicates that there is an architectural issue in your code.
There are two cases:
Your enum members all contain valid data; in that case it is always important to distinguish in .last() between "one element with some data" and "no element", and falling back to a default wouldn't be viable.
Your enum has a None member, in which case having the None element at .last() would be identical to having no members. (which is what I suspect your code does). In that case it's pointless to manually define a None member, though, simply use the Option enum that already exists. That would make your problem trivially solvable; Option already implements Default (which is None) and can propagate the reference to its member via .as_ref():
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum EnumContent<T> {
One(T),
Two(T),
}
// Option already implements `Default`.
pub type Enum<T> = Option<EnumContent<T>>;
fn main() {
let mut v: Vec<Enum<u32>> = vec![];
println!("{:?} -> {:?}", v, v.last().and_then(Option::as_ref));
v.push(Some(EnumContent::One(42)));
println!("{:?} -> {:?}", v, v.last().and_then(Option::as_ref));
v.push(None);
println!("{:?} -> {:?}", v, v.last().and_then(Option::as_ref));
}
[] -> None
[Some(One(42))] -> Some(One(42))
[Some(One(42)), None] -> None

Implement Borrow on something behind a RefCell?

I have the structs Value and RefValue in my project. RefValue is a reference-counted, dynamically borrowable Value. Now Value may contain a HashMap of RefValue, where both the key and the value is a RefValue.
type ValueMap = HashMap<RefValue, RefValue>;
#[derive(Debug, PartialEq, Eq)]
enum Value {
Integer(i64),
String(String),
Map(ValueMap),
}
#[derive(Debug, PartialEq, Eq)]
struct RefValue {
value: Rc<RefCell<Value>>,
}
I've implemented Hash on RefValue on my own, and some From-traits separately in this playground.
What I want to achieve is something like this main program:
fn main() {
// Simple values
let x = RefValue::from(42);
let y = RefValue::from("Hello");
// Make a map from these values
let mut z = ValueMap::new();
z.insert(RefValue::from("x"), x);
z.insert(RefValue::from("y"), y);
// Make a value from the map
let z = RefValue::from(z);
println!("z = {:?}", z);
// Try to access "x"
if let Value::Map(m) = &*z.borrow() {
println!("m[x] = {:?}", m["x"]); // <- here I want to access by &str
};
}
Unfortunately I'm getting strange results, as you can find in the playground comments. I'm also quite unsure if there's not a better implementation of the entire problem, as the RefCell cannot return a borrowed value of its contained element.
Can anybody give me a hint?
When you implement Borrow<T>, your Hash implementation must return the same hash value as T's for when the underlying value is equivalent. That is, if x.hash() must be equal to x.borrow().hash(). HashMap relies on this property when you index into it: it requires Idx: Borrow<Key> and then uses this rule to ensure it can find the value.
Your impl Borrow<str> for RefValue does not follow this rule. RefValue::hash() for RefValue::String calls write_u8(2) before hashing the string. Since you broke the contract, the hashmap is allowed to do anything (excluding undefined behavior), like panicking, aborting the process, or not finding your key, which is what it does in this case.
To fix that, you should just not hash the discriminant (removed it from the others too, for consistency):
impl Hash for RefValue {
fn hash<H: Hasher>(&self, state: &mut H) {
match &*self.borrow() {
Value::Integer(i) => {
i.hash(state);
}
Value::String(s) => {
s.hash(state);
}
Value::Map(m) => {
(m as *const ValueMap as usize).hash(state); // Object address
}
}
}
}
Now it panics in your Borrow implementation, like you expected (playground).
But you should not implement Borrow, since implementing it means your value is a reflection of the borrowed value. RefValue is by no means str. It can be integers, or maps, too. Thus, you should not implement Borrow for any of those. You could implement Borrow<Value>, but this is impossible because you use RefCell and thus need to return Ref but Borrow mandates returning a reference. You're out of luck. Your only option is to index with RefValues.
Lastly, you should avoid interior mutability in keys. Once change them, and it's easy to change them by mistake, and your hash/equality change, you broke your contract with the map once again.

Ensuring value lives for its entire scope

I have a type (specifically CFData from core-foundation), whose memory is managed by C APIs and that I need to pass and receive from C functions as a *c_void. For simplicity, consider the following struct:
struct Data {
ptr: *mut ffi::c_void,
}
impl Data {
pub fn new() -> Self {
// allocate memory via an unsafe C-API
Self {
ptr: std::ptr::null(), // Just so this compiles.
}
}
pub fn to_raw(&self) -> *const ffi::c_void {
self.ptr
}
}
impl Drop for Data {
fn drop(&mut self) {
unsafe {
// Free memory via a C-API
}
}
}
Its interface is safe, including to_raw(), since it only returns a raw pointer. It doesn't dereference it. And the caller doesn't dereference it. It's just used in a callback.
pub extern "C" fn called_from_C_ok(on_complete: extern "C" fn(*const ffi::c_void)) {
let data = Data::new();
// Do things to compute Data.
// This is actually async code, which is why there's a completion handler.
on_complete(data.to_raw()); // data survives through the function call
}
This is fine. Data is safe to manipulate, and (I strongly believe) Rust promises that data will live until the end of the on_complete function call.
On the other hand, this is not ok:
pub extern "C" fn called_from_C_broken(on_complete: extern "C" fn(*const ffi::c_void)) {
let data = Data::new();
// ...
let ptr = data.to_raw(); // data can be dropped at this point, so ptr is dangling.
on_complete(ptr); // This may crash when the receiver dereferences it.
}
In my code, I made this mistake and it started crashing. It's easy to see why and it's easy to fix. But it's subtle, and it's easy for a future developer (me) to modify the ok version into the broken version without realizing the problem (and it may not always crash).
What I'd like to do is to ensure data lives as long as ptr. In Swift, I'd do this with:
withExtendedLifetime(&data) { data in
// ...data cannot be dropped until this closure ends...
}
Is there a similar construct in Rust that explicitly marks the minimum lifetime for a variable to a scope (that the optimizer may not reorder), even if it's not directly accessed? (I'm sure it's trivial to build a custom with_extended_lifetime in Rust, but I'm looking for a more standard solution so that it will be obvious to other developers what's going on).
Playground
I do believe the following "works" but I'm not sure how flexible it is, or if it's just replacing a more standard solution:
fn with_extended_lifetime<T, U, F>(value: &T, f: F) -> U
where
F: Fn(&T) -> U,
{
f(value)
}
with_extended_lifetime(&data, |data| {
let ptr = data.to_raw();
on_complete(ptr)
});
The optimizer is not allowed to change when a value is dropped. If you assign a value to a variable (and that value is not then moved elsewhere or overwritten by assignment), it will always be dropped at the end of the block, not earlier.
You say that this code is incorrect:
pub extern "C" fn called_from_C_broken(on_complete: extern "C" fn(*const ffi::c_void)) {
let data = Data::new();
// ...
let ptr = data.to_raw(); // data can be dropped at this point, so ptr is dangling.
on_complete(ptr); // This may crash when the receiver dereferences it.
}
but in fact data may not be dropped at that point, and this code is sound. What you may be confusing this with is the mistake of not assigning the value to a variable:
let ptr = Data::new().to_raw();
on_complete(ptr);
In this case, the pointer is dangling, because the result of Data::new() is stored in a temporary variable within the statement, which is dropped at the end of the statement, not a local variable, which is dropped at the end of the block.
If you want to adopt a programming style which makes explicit when values are dropped, the usual pattern is to use the standard drop() function to mark the exact time of drop:
let data = Data::new();
...
on_complete(data.to_raw());
drop(data); // We have stopped using the raw pointer now
(Note that drop() is not magic: it is simply a function which takes one argument and does nothing with it other than dropping. Its presence in the standard library is to give a name to this pattern, not to provide special functionality.)
However, if you want to, there isn't anything wrong with using your with_extended_lifetime (other than nonstandard style and arguably a misleading name) if you want to make the code structure even more strongly indicate the scope of the value. One nitpick: the function parameter should be FnOnce, not Fn, for maximum generality (this allows it to be passed functions that can't be called more than once).
Other than explicitly dropping as the other answer mentions, there is another way to help prevent these types of accidental drops: use a wrapper around a raw pointer that has lifetime information.
use std::marker::PhantomData;
#[repr(transparent)]
struct PtrWithLifetime<'a>{
ptr: *mut ffi::c_void,
_data: PhantomData<&'a ffi::c_void>,
}
impl Data {
fn to_raw(&self) -> PtrWithLife<'_>{
PtrWithLifetime{
ptr: self.ptr,
_data: PhantomData,
}
}
}
The #[repr(transparent)] guarantees that PtrWithLife is stored in memory the same as *const ffi::c_void is, so you can adjust the declaration of on_complete to
fn called_from_c(on_complete: extern "C" fn(PtrWithLifetime<'_>)){
//stuff
}
without causing any major inconvenience to any downstream users, especially since the ffi bindings can be adjusted in a similar fashion.

Initializing a struct field-by-field. Is it possible to know if all the fields were initialized?

I'm following the example from the official documentation. I'll copy the code here for simplicity:
#[derive(Debug, PartialEq)]
pub struct Foo {
name: String,
list: Vec<u8>,
}
let foo = {
let mut uninit: MaybeUninit<Foo> = MaybeUninit::uninit();
let ptr = uninit.as_mut_ptr();
// Initializing the `name` field
// Using `write` instead of assignment via `=` to not call `drop` on the
// old, uninitialized value.
unsafe { addr_of_mut!((*ptr).name).write("Bob".to_string()); }
// Initializing the `list` field
// If there is a panic here, then the `String` in the `name` field leaks.
unsafe { addr_of_mut!((*ptr).list).write(vec![0, 1, 2]); }
// All the fields are initialized, so we call `assume_init` to get an initialized Foo.
unsafe { uninit.assume_init() }
};
What bothers me is the second unsafe comment: If there is a panic here, then the String in the name field leaks. This is exactly what I want to avoid. I modified the example so now it reflects my concerns:
use std::mem::MaybeUninit;
use std::ptr::addr_of_mut;
#[derive(Debug, PartialEq)]
pub struct Foo {
name: String,
list: Vec<u8>,
}
#[allow(dead_code)]
fn main() {
let mut uninit: MaybeUninit<Foo> = MaybeUninit::uninit();
let ptr = uninit.as_mut_ptr();
init_foo(ptr);
// this is wrong because it tries to read the uninitialized field
// I could avoid this call if the function `init_foo` returns a `Result`
// but I'd like to know which fields are initialized so I can cleanup
let _foo = unsafe { uninit.assume_init() };
}
fn init_foo(foo_ptr: *mut Foo) {
unsafe { addr_of_mut!((*foo_ptr).name).write("Bob".to_string()); }
// something happened and `list` field left uninitialized
return;
}
The code builds and runs. But using MIRI I see the error:
Undefined Behavior: type validation failed at .value.list.buf.ptr.pointer: encountered uninitialized raw pointer
The question is how I can figure out which fields are initialized and which are not? Sure, I could return a result with the list of field names or similar, for example. But I don't want to do it - my struct can have dozens of fields, it changes over time and I'm too lazy to maintain an enum that should reflect the fields. Ideally I'd like to have something like this:
if addr_initialized!((*ptr).name) {
clean(addr_of_mut!((*ptr).name));
}
Update: Here's an example of what I want to achieve. I'm doing some Vulkan programming (with ash crate, but that's not important). I want to create a struct that holds all the necessary objects, like Device, Instance, Surface, etc.:
struct VulkanData {
pub instance: Instance,
pub device: Device,
pub surface: Surface,
// 100500 other fields
}
fn init() -> Result<VulkanData, Error> {
// let vulkan_data = VulkanData{}; // can't do that because some fields are not default constructible.
let instance = create_instance(); // can fail
let device = create_device(instance); // can fail, in this case instance have to be destroyed
let surface = create_surface(device); // can fail, in this case instance and device have to be destroyed
//other initialization routines
VulkanData{instance, device, surface, ...}
}
As you can see, for every such object, there's a corresponding create_x function, which can fail. Obviously, if I fail in the middle of the process, I don't want to proceed. But I want to clear already created objects. As you mentioned, I could create a wrapper. But it's very tedious work to create wrappers for hundreds of types, I absolutely want to avoid this (btw, ash is already a wrapper over C-types). Moreover, because of the asynchronous nature of CPU-GPU communication, sometimes it makes no sense to drop an object, it can lead to errors. Instead, some form of a signal should come from the GPU that indicates that an object is safe to destroy. That's the main reason why I can't implement Drop for the wrappers.
But as soon as the struct is successfully initialized I know that it's safe to read any of its fields. That's why don't want to use an Option - it adds some overhead and makes no sense in my particular example.
All that is trivially achievable in C++ - create an uninitialized struct (well, by default all Vulkan objects are initialized with VK_NULL_HANDLE), start to fill it field-by-field, if something went wrong just destroy the objects that are not null.
There is no general purpose way to tell if something is initialized or not. Miri can detect this because it adds a lot of instrumentation and overhead to track memory operations.
All that is trivially achievable in C++ - create an uninitialized struct (well, by default all Vulkan objects are initialized with VK_NULL_HANDLE), start to fill it field-by-field, if something went wrong just destroy the objects that are not null.
You could theoretically do the same in Rust, however this is quite unsafe and makes a lot of assumptions about the construction of the ash types.
If the functions didn't depend on each other, I might suggest something like this:
let instance = create_instance();
let device = create_device();
let surface = create_surface();
match (instance, device, surface) {
(Ok(instance), Ok(device), Ok(surface)) => {
Ok(VulkanData{
instance,
device,
surface,
})
}
instance, device, surface {
// clean up the `Ok` ones and return some error
}
}
However, your functions are dependent on others succeeding (e.g. need the Instance to create a Device) and this also has the disadvantage that it would keep creating values when one already failed.
Creating wrappers with custom drop behavior is the most robust way to accomplish this. There is the vulkano crate that is built on top of ash that does this among other things. But if that's not to your liking you can use something like scopeguard to encapsulate drop logic on the fly.
use scopeguard::{guard, ScopeGuard}; // 1.1.0
fn init() -> Result<VulkanData, Error> {
let instance = guard(create_instance()?, destroy_instance);
let device = guard(create_device(&instance)?, destroy_device);
let surface = guard(create_surface(&device)?, destroy_surface);
Ok(VulkanData {
// use `into_inner` to escape the drop behavior
instance: ScopeGuard::into_inner(instance),
device: ScopeGuard::into_inner(device),
surface: ScopeGuard::into_inner(surface),
})
}
See a full example on the playground. No unsafe required.
I believe MaybeUninit is designed for the cases when you have all the information about its contents and can make the code safe "by hand".
If you need to figure out in runtime if a field has a value, then use Option<T>.
Per the documentation:
You can think of MaybeUninit<T> as being a bit like Option<T> but without any of the run-time tracking and without any of the safety checks.

How to define a function with two enum parameters where the 2nd type is determined by the 1st?

I'm working on the rust-efl project, which is a binding to the Enlightenment EFL C library.
There is a function that takes two C-style enums to configure the library. I want to restrict the second argument to a certain enum based on the value of the first argument.
I'll use the actual code for this example since it makes it a bit easier to understand the use case.
Example call:
elementary::policy_set(ElmPolicy::Quit, ElmPolicyQuit::LastWindowClosed as i32);
Library Rust code:
#[repr(C)]
pub enum ElmPolicy {
/// under which circumstances the application should quit automatically. See ElmPolicyQuit
Quit = 0,
/// defines elm_exit() behaviour. See ElmPolicyExit
Exit,
/// defines how throttling should work. See ElmPolicyThrottle
Throttle
}
#[repr(C)]
pub enum ElmPolicyQuit {
/// never quit the application automatically
None = 0,
/// quit when the application's last window is closed
LastWindowClosed,
/// quit when the application's last window is hidden
LastWindowHidden
}
pub fn policy_set(policy: ElmPolicy, value: i32) {
There is an enum similar to ElmPolicyQuit for each value of ElmPolicy. The second argument to policy_set should be of the corresponding enum type.
I would like to modify policy_set so that the caller does not have to cast the value to an i32 by defining a type for value. Ideally, I would like Rust to check that the second argument is of the correct type for the given policy argument.
Implementation Attempt
I'm new to Rust, so this may be way off, but this is my current attempt:
pub fn policy_set<P: ElmPolicy, V: ElmPolicyValue<P>>(policy: P, value: V) {
unsafe { elm_policy_set(policy as c_int, value as c_int) }
}
trait ElmPolicyValue<P> {}
impl ElmPolicyValue<ElmPolicy::Quit> for ElmPolicyQuit {}
But I get this error:
src/elementary.rs:246:22: 246:31 error: ElmPolicy is not a trait
[E0404] src/elementary.rs:246 pub fn policy_set>(policy: P, value: V) {
(arrow points to first argument's type)
I made a dummy trait for ElmPolicy, but then I get
src/elementary.rs:111:21: 111:36 error: found value
elementary::ElmPolicy::Quit used as a type [E0248]
src/elementary.rs:111 impl ElmPolicyValue for
ElmPolicyQuit {}
So it seems like I can't use enums for generic types in this way.
What is the correct way to implement this?
I suppose I don't need these to actually be enums. I just need the values to be convertible to a c_int that corresponds with the EFL library's C enum.
Also, I thought about a single argument instead.
elementary::policy_set(elementary::Policy::Quit::LastWindowClosed);
But nested C-like enums don't seem to work despite documentation I found and I'm uncertain about using #[repr(C)] with nested enums.
Here's how I would do it: instead of defining ElmPolicy as an enum, I'd define it as a trait with an associated type that specifies the parameter type for the policy's value. Each policy would define a unit struct that implements ElmPolicy.
pub trait ElmPolicy {
type Value;
}
pub struct ElmPolicyQuit;
impl ElmPolicy for ElmPolicyQuit {
type Value = ElmPolicyQuitValue;
}
// TODO: ElmPolicyExit and ElmPolicyThrottle
#[repr(C)]
pub enum ElmPolicyQuitValue {
/// never quit the application automatically
None = 0,
/// quit when the application's last window is closed
LastWindowClosed,
/// quit when the application's last window is hidden
LastWindowHidden
}
pub fn policy_set<P: ElmPolicy>(policy: P, value: P::Value) {
// TODO
}
fn main() {
policy_set(ElmPolicyQuit, ElmPolicyQuitValue::LastWindowClosed);
//policy_set(ElmPolicyQuit, ElmPolicyQuitValue::LastWindowClosed as i32); // does not compile
}
You can add methods or supertraits to ElmPolicy and contraints to ElmPolicy::Value as necessary. For example, you might want to add Into<i32> as a constraint on ElmPolicy::Value, so that you can use value.into() in policy_set to convert the value to an i32.
I wouldn't try to solve both problems in one swoop. Create a straight-forward Rust binding to the C library that exposes the API as is (a *-sys package). Then provide a more idiomatic Rust API on top:
enum ElmPolicy {
Quit(QuitDetail),
Exit(ExitDetail),
Throttle(ThrottleDetail),
}
enum QuitDetail {
None,
LastWindowClosed,
LastWindowHidden,
}
Then you can create methods on ElmPolicy that produce a tuple of the C enums, or directly calls the appropriate C function, as desired.
Note that there is likely to be a bit of boilerplate as your enums in the *-sys crate will mirror those in the idiomatic API and you will want to convert between them. Macros may help reduce that.

Resources