Mats Kindahl
Mats Kindahl

Reputation: 2075

Is it possible to use a type to access a specific field of a Rust union?

As part of mapping a C interface to Rust, I want to handle a union that stored a few native types directly and have a pointer to an allocated type for all others.

How can I implement a parameterized wrapper type around the union that can select and use the appropriate field based on the wrapper type parameter?

In this case, I want to add a Rust wrapper that reads the data structure directly and not a solution that converts it to a Rust-native type first. Adding other support types to "trick" the compiler is fine though. The end goal is to be able to write code similar to this:

let the_list: List<i32> = get_a_list();
for i in the_list {
    println!("value")
}

I am skipping the definitions of IntoIterator and friends since that boils down to accessing the correct field based on the type.

The code is highly unsafe, but we can assume that the user provides the correct type for the type parameter.

There are other solutions to this problem such as having explicit reader functions for each type, but this is focused on understanding if there are ways to make it work without those and without introducing an undue overhead.

Code that does not work, but illustrates what I want to accomplish:

#![feature(specialization)]
use std::convert::From;

union Data<T> {
    int_value: i64,
    ptr_value: *const T,
}

default impl<T> From<&Data<T>> for &T {
    fn from(data: &Data<T>) -> &T {
        &*data.ptr_value
    }
}

impl From<&Data<i64>> for &i64 {
    fn from(data: &Data<i64>) -> &i64 {
        &*data.int_value
    }
}

fn show<T>(value: &T) {
    println!("value: {}", value);
}

fn main() {
    let value = String::from("magic");
    let data: Data<&str> = Data {
        ptr_value: value.as_ptr(),
    };
    show(data.into());
}

I have minimized the example to avoid discussions about other aspects. This gives the following error:

error[E0210]: type parameter `T` must be used as the type parameter for some local type (e.g., `MyStruct<T>`)
  --> examples/union_convert.rs:10:14
   |
10 | default impl<T> From<&Data<T>> for &T {
   |              ^ type parameter `T` must be used as the type parameter for some local type
   |
   = note: implementing a foreign trait is only possible if at least one of the types for which is it implemented is local
   = note: only traits defined in the current crate can be implemented for a type parameter

I have tried adding a wrapper around the union to handle the type-punning, but that seems to just push the error message around. Returning some other type than &T would also be possible, but I do not understand how to make it behave correctly. Using a different return type is also an option, but it still boils down to selecting the correct field based on a type.

Looking at the implementation of std::vec::Vec it does a similar thing, but in this case it always map the memory representation of the type to a real type. In this case, I want to select the correct union field based on the type that was used when writing the value.

Other similar questions that do not really answer this questions:

Update: Provided a more distinct example of using into() to convert the union type one of the fields.

Upvotes: 2

Views: 459

Answers (1)

Shepmaster
Shepmaster

Reputation: 431689

I would define my own trait instead of From to avoid conflicting with the standard library implementations. I'd also define a newtype wrapper / marker type. This removes the possibility of conflict when storing one of the specific types in the generic spot.

struct Other<T>(T);

union Data<T> {
    bool_value: bool,
    int_value: i64,
    ptr_value: *const T,
}

trait Access<T> {
    type Output;

    unsafe fn access(&self) -> &Self::Output;
}

impl<T> Access<Other<T>> for Data<T> {
    type Output = T;

    unsafe fn access(&self) -> &Self::Output {
        &*self.ptr_value
    }
}

impl<T> Access<bool> for Data<T> {
    type Output = bool;

    unsafe fn access(&self) -> &Self::Output {
        &self.bool_value
    }
}

impl<T> Access<i64> for Data<T> {
    type Output = i64;

    unsafe fn access(&self) -> &Self::Output {
        &self.int_value
    }
}

fn main() {
    let value = 123_f64;
    let data: Data<f64> = Data { ptr_value: &value };

    // This is safe because we just created this with
    // a `f64` and nothing happened in-between.
    unsafe {
        println!("{}", Access::<Other<f64>>::access(&data));
    }
}

See also:

Upvotes: 3

Related Questions