Work on const field offset crate to give type safe offsets

This commit is contained in:
Olivier Goffart 2020-05-13 11:02:14 +02:00
parent aff3d7e14b
commit 751a3fbe59
9 changed files with 399 additions and 107 deletions

View file

@ -2,7 +2,7 @@ pub use sixtyfps_rs_macro::sixtyfps;
/// internal re_exports used by the macro generated /// internal re_exports used by the macro generated
pub mod re_exports { pub mod re_exports {
pub use const_field_offset::FieldOffsets; pub use const_field_offset::{self, FieldOffsets};
pub use corelib::abi::datastructures::{Component, ComponentTO, ComponentVTable, ItemTreeNode}; pub use corelib::abi::datastructures::{Component, ComponentTO, ComponentVTable, ItemTreeNode};
pub use corelib::abi::primitives::{Image, ImageVTable, Rectangle, RectangleVTable}; pub use corelib::abi::primitives::{Image, ImageVTable, Rectangle, RectangleVTable};
pub use corelib::ComponentVTable_static; pub use corelib::ComponentVTable_static;

View file

@ -113,7 +113,7 @@ pub fn sixtyfps(stream: TokenStream) -> TokenStream {
let children_count = item.children.len() as u32; let children_count = item.children.len() as u32;
item_tree_array.push(quote!( item_tree_array.push(quote!(
sixtyfps::re_exports::ItemTreeNode::Item{ sixtyfps::re_exports::ItemTreeNode::Item{
offset: #component_id::field_offsets().#field_name as isize, offset: #component_id::field_offsets().#field_name.get_byte_offset() as isize,
vtable: &#vtable as *const _, vtable: &#vtable as *const _,
chilren_count: #children_count, chilren_count: #children_count,
children_index: #children_index, children_index: #children_index,
@ -141,6 +141,7 @@ pub fn sixtyfps(stream: TokenStream) -> TokenStream {
let item_tree_array_len = item_tree_array.len(); let item_tree_array_len = item_tree_array.len();
quote!( quote!(
use sixtyfps::re_exports::const_field_offset;
#[derive(sixtyfps::re_exports::FieldOffsets)] #[derive(sixtyfps::re_exports::FieldOffsets)]
#[repr(C)] #[repr(C)]
struct #component_id { struct #component_id {

View file

@ -4,14 +4,8 @@ version = "0.1.0"
authors = ["Sixty FPS <info@sixtyfps.io>"] authors = ["Sixty FPS <info@sixtyfps.io>"]
edition = "2018" edition = "2018"
[lib]
proc-macro = true
[dependencies] [dependencies]
syn = { version = "1.0", features = ["derive"] } const-field-offset-macro = { path = "./macro" }
quote = "1.0"
proc-macro2 = "1.0"
[dev-dependencies] [dev-dependencies]
memoffset = "0.5.4" memoffset = "0.5.4"

View file

@ -0,0 +1,18 @@
[package]
name = "const-field-offset-macro"
version = "0.1.0"
authors = ["Sixty FPS <info@sixtyfps.io>"]
edition = "2018"
[lib]
path = "macro.rs"
proc-macro = true
[dependencies]
syn = { version = "1.0", features = ["derive"] }
quote = "1.0"
proc-macro2 = "1.0"
[dev-dependencies]
memoffset = "0.5.4"
const-field-offset = { path="../" }

View file

@ -0,0 +1,94 @@
/*!
This crate allow to get the offset of a field of a structure in a const or static context.
```rust
use const_field_offset::FieldOffsets;
#[repr(C)]
#[derive(FieldOffsets)]
struct Foo {
field_1 : u8,
field_2 : u32,
}
const FOO : usize = Foo::field_offsets().field_2.get_byte_offset();
assert_eq!(FOO, 4);
// This would not work on stable rust at the moment (rust 1.43)
// const FOO : usize = memofsets::offsetof!(Foo, field_2);
```
The macro FieldOffsets adds a `const fn field_offsets()` associated function to the struct, that
returns an object which has a bunch of usize fields with the same name as the fields of the
original struct.
## limitations
Only work with named #[repr(C)] structures.
*/
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(FieldOffsets)]
pub fn const_field_offset(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
if !input.attrs.iter().any(|a| {
if let Some(i) = a.path.get_ident() {
i == "repr" && a.tokens.to_string() == "(C)"
} else {
false
}
}) {
return TokenStream::from(quote! {compile_error!{"Only work if repr(C)"}});
};
let struct_name = input.ident;
let field_struct_name = quote::format_ident!("{}FieldsOffsets", struct_name);
let (fields, types, vis) = if let syn::Data::Struct(s) = &input.data {
if let syn::Fields::Named(n) = &s.fields {
let (f, tv): (Vec<_>, Vec<_>) =
n.named.iter().map(|f| (&f.ident, (&f.ty, &f.vis))).unzip();
let (t, v): (Vec<_>, Vec<_>) = tv.into_iter().unzip();
(f, t, v)
} else {
return TokenStream::from(quote! {compile_error!{"Only work for named fields"}});
}
} else {
return TokenStream::from(quote! {compile_error!("Only work for struct")});
};
let crate_ = quote!(const_field_offset);
// Build the output, possibly using quasi-quotation
let expanded = quote! {
pub struct #field_struct_name {
#(#vis #fields : #crate_::FieldOffset<#struct_name, #types>,)*
}
impl #struct_name {
pub const fn field_offsets() -> #field_struct_name {
let mut len = 0usize;
#field_struct_name {
#( #fields : {
let align = ::core::mem::align_of::<#types>();
// from Layout::padding_needed_for which is not yet stable
let len_rounded_up = len.wrapping_add(align).wrapping_sub(1) & !align.wrapping_sub(1);
len = len_rounded_up + ::core::mem::size_of::<#types>();
/// Safety: According to the rules of repr(C), this is the right offset
unsafe { #crate_::FieldOffset::<#struct_name, #types>::new_from_offset(len_rounded_up) }
}, )*
}
}
}
};
// Hand the output tokens back to the compiler
TokenStream::from(expanded)
}

View file

@ -1,91 +1,274 @@
/*! /*
The FieldOffster structure is forked from https://docs.rs/field-offset/0.3.0/src/field_offset/lib.rs.html
This crate allow to get the offset of a field of a structure in a const or static context. The changes include:
- Only the FieldOffset structure was imported, not the macros
- re-export of the FieldOffsets derive macro
- add const in most method
```rust (there is a `//###` comment in front of each change)
use const_field_offset::FieldOffsets;
#[repr(C)]
#[derive(FieldOffsets)]
struct Foo {
field_1 : u8,
field_2 : u32,
}
const FOO : usize = Foo::field_offsets().field_2;
assert_eq!(FOO, 4);
// This would not work on stable rust at the moment (rust 1.43)
// const FOO : usize = memofsets::offsetof!(Foo, field_2);
```
The macro FieldOffsets adds a `const fn field_offsets()` associated function to the struct, that
returns an object which has a bunch of usize fields with the same name as the fields of the
original struct.
## limitations
Only work with named #[repr(C)] structures.
*/ */
extern crate proc_macro; use std::fmt;
use std::marker::PhantomData;
use std::mem;
use std::ops::Add;
use proc_macro::TokenStream; #[doc(inline)]
use quote::quote; pub use const_field_offset_macro::FieldOffsets;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(FieldOffsets)] /// Represents a pointer to a field of type `U` within the type `T`
pub fn const_field_offset(input: TokenStream) -> TokenStream { #[repr(transparent)]
let input = parse_macro_input!(input as DeriveInput); pub struct FieldOffset<T, U>(
/// Offset in bytes of the field within the struct
usize,
// ### Changed from Fn to fn to allow const
// it is fine to be fariant
PhantomData<(*const T, *const U)>,
);
if !input.attrs.iter().any(|a| { impl<T, U> FieldOffset<T, U> {
if let Some(i) = a.path.get_ident() { // Use MaybeUninit to get a fake T
i == "repr" && a.tokens.to_string() == "(C)" #[cfg(fieldoffset_maybe_uninit)]
} else { #[inline]
false fn with_uninit_ptr<R, F: FnOnce(*const T) -> R>(f: F) -> R {
} let uninit = mem::MaybeUninit::<T>::uninit();
}) { f(uninit.as_ptr())
return TokenStream::from(quote! {compile_error!{"Only work if repr(C)"}});
};
let struct_name = input.ident;
let field_struct_name = quote::format_ident!("{}FieldsOffsets", struct_name);
let (fields, types) = if let syn::Data::Struct(s) = input.data {
if let syn::Fields::Named(n) = s.fields {
(
n.named.iter().map(|f| f.ident.clone()).collect::<Vec<_>>(),
n.named.iter().map(|f| f.ty.clone()).collect::<Vec<_>>(),
)
} else {
return TokenStream::from(quote! {compile_error!{"Only work for named fields"}});
}
} else {
return TokenStream::from(quote! {compile_error!("Only work for struct")});
};
// Build the output, possibly using quasi-quotation
let expanded = quote! {
pub struct #field_struct_name {
#(pub #fields : usize,)*
} }
impl #struct_name { // Use a dangling pointer to get a fake T
pub const fn field_offsets() -> #field_struct_name { #[cfg(not(fieldoffset_maybe_uninit))]
let mut len = 0usize; #[inline]
#field_struct_name { fn with_uninit_ptr<R, F: FnOnce(*const T) -> R>(f: F) -> R {
#( #fields : { f(mem::align_of::<T>() as *const T)
let align = ::core::mem::align_of::<#types>();
// from Layout::padding_needed_for whiwh was not yet stable
let len_rounded_up = len.wrapping_add(align).wrapping_sub(1) & !align.wrapping_sub(1);
len = len_rounded_up + ::core::mem::size_of::<#types>();
len_rounded_up
}, )*
} }
}
}
};
// Hand the output tokens back to the compiler /// Construct a field offset via a lambda which returns a reference
TokenStream::from(expanded) /// to the field in question.
///
/// # Safety
///
/// The lambda *must not* dereference the provided pointer or access the
/// inner value in any way as it may point to uninitialized memory.
///
/// For the returned `FieldOffset` to be safe to use, the returned pointer
/// must be valid for *any* instance of `T`. For example, returning a pointer
/// to a field from an enum with multiple variants will produce a `FieldOffset`
/// which is unsafe to use.
pub unsafe fn new<F: for<'a> FnOnce(*const T) -> *const U>(f: F) -> Self {
let offset = Self::with_uninit_ptr(|base_ptr| {
let field_ptr = f(base_ptr);
(field_ptr as usize).wrapping_sub(base_ptr as usize)
});
// Construct an instance using the offset
Self::new_from_offset(offset)
}
/// Construct a field offset directly from a byte offset.
///
/// # Safety
///
/// For the returned `FieldOffset` to be safe to use, the field offset
/// must be valid for *any* instance of `T`. For example, returning the offset
/// to a field from an enum with multiple variants will produce a `FieldOffset`
/// which is unsafe to use.
#[inline]
pub const unsafe fn new_from_offset(offset: usize) -> Self {
// ### made const so assert is not allowed
// Sanity check: ensure that the field offset plus the field size
// is no greater than the size of the containing struct. This is
// not sufficient to make the function *safe*, but it does catch
// obvious errors like returning a reference to a boxed value,
// which is owned by `T` and so has the correct lifetime, but is not
// actually a field.
//assert!(offset + mem::size_of::<U>() <= mem::size_of::<T>());
FieldOffset(offset, PhantomData)
}
// Methods for applying the pointer to member
/// Apply the field offset to a native pointer.
#[inline]
pub fn apply_ptr(self, x: *const T) -> *const U {
((x as usize) + self.0) as *const U
}
/// Apply the field offset to a native mutable pointer.
#[inline]
pub fn apply_ptr_mut(self, x: *mut T) -> *mut U {
((x as usize) + self.0) as *mut U
}
/// Apply the field offset to a reference.
#[inline]
pub fn apply<'a>(self, x: &'a T) -> &'a U {
unsafe { &*self.apply_ptr(x) }
}
/// Apply the field offset to a mutable reference.
#[inline]
pub fn apply_mut<'a>(self, x: &'a mut T) -> &'a mut U {
unsafe { &mut *self.apply_ptr_mut(x) }
}
/// Get the raw byte offset for this field offset.
#[inline]
pub const fn get_byte_offset(self) -> usize {
self.0
}
// Methods for unapplying the pointer to member
/// Unapply the field offset to a native pointer.
///
/// # Safety
///
/// *Warning: very unsafe!*
///
/// This applies a negative offset to a pointer. If the safety
/// implications of this are not already clear to you, then *do
/// not* use this method. Also be aware that Rust has stronger
/// aliasing rules than other languages, so it may be UB to
/// dereference the resulting pointer even if it points to a valid
/// location, due to the presence of other live references.
#[inline]
pub unsafe fn unapply_ptr(self, x: *const U) -> *const T {
((x as usize) - self.0) as *const T
}
/// Unapply the field offset to a native mutable pointer.
///
/// # Safety
///
/// *Warning: very unsafe!*
///
/// This applies a negative offset to a pointer. If the safety
/// implications of this are not already clear to you, then *do
/// not* use this method. Also be aware that Rust has stronger
/// aliasing rules than other languages, so it may be UB to
/// dereference the resulting pointer even if it points to a valid
/// location, due to the presence of other live references.
#[inline]
pub unsafe fn unapply_ptr_mut(self, x: *mut U) -> *mut T {
((x as usize) - self.0) as *mut T
}
/// Unapply the field offset to a reference.
///
/// # Safety
///
/// *Warning: very unsafe!*
///
/// This applies a negative offset to a reference. If the safety
/// implications of this are not already clear to you, then *do
/// not* use this method. Also be aware that Rust has stronger
/// aliasing rules than other languages, so this method may cause UB
/// even if the resulting reference points to a valid location, due
/// to the presence of other live references.
#[inline]
pub unsafe fn unapply<'a>(self, x: &'a U) -> &'a T {
&*self.unapply_ptr(x)
}
/// Unapply the field offset to a mutable reference.
///
/// # Safety
///
/// *Warning: very unsafe!*
///
/// This applies a negative offset to a reference. If the safety
/// implications of this are not already clear to you, then *do
/// not* use this method. Also be aware that Rust has stronger
/// aliasing rules than other languages, so this method may cause UB
/// even if the resulting reference points to a valid location, due
/// to the presence of other live references.
#[inline]
pub unsafe fn unapply_mut<'a>(self, x: &'a mut U) -> &'a mut T {
&mut *self.unapply_ptr_mut(x)
}
}
/// Allow chaining pointer-to-members.
///
/// Applying the resulting field offset is equivalent to applying the first
/// field offset, then applying the second field offset.
///
/// The requirements on the generic type parameters ensure this is a safe operation.
impl<T, U, V> Add<FieldOffset<U, V>> for FieldOffset<T, U> {
type Output = FieldOffset<T, V>;
#[inline]
fn add(self, other: FieldOffset<U, V>) -> FieldOffset<T, V> {
FieldOffset(self.0 + other.0, PhantomData)
}
}
/// The debug implementation prints the byte offset of the field in hexadecimal.
impl<T, U> fmt::Debug for FieldOffset<T, U> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "FieldOffset({:#x})", self.0)
}
}
impl<T, U> Copy for FieldOffset<T, U> {}
impl<T, U> Clone for FieldOffset<T, U> {
fn clone(&self) -> Self {
*self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate as const_field_offset;
// ### Structures were change to repr(c) and to inherit FieldOffsets
// Example structs
#[derive(Debug, FieldOffsets)]
#[repr(C)]
struct Foo {
a: u32,
b: f64,
c: bool,
}
#[derive(Debug, FieldOffsets)]
#[repr(C)]
struct Bar {
x: u32,
y: Foo,
}
#[test]
fn test_simple() {
// Get a pointer to `b` within `Foo`
let foo_b = Foo::field_offsets().b;
// Construct an example `Foo`
let mut x = Foo { a: 1, b: 2.0, c: false };
// Apply the pointer to get at `b` and read it
{
let y = foo_b.apply(&x);
assert!(*y == 2.0);
}
// Apply the pointer to get at `b` and mutate it
{
let y = foo_b.apply_mut(&mut x);
*y = 42.0;
}
assert!(x.b == 42.0);
}
#[test]
fn test_nested() {
// Construct an example `Foo`
let mut x = Bar { x: 0, y: Foo { a: 1, b: 2.0, c: false } };
// Combine the pointer-to-members
let bar_y_b = Bar::field_offsets().y + Foo::field_offsets().b;
// Apply the pointer to get at `b` and mutate it
{
let y = bar_y_b.apply_mut(&mut x);
*y = 42.0;
}
assert!(x.y.b == 42.0);
}
} }

View file

@ -18,18 +18,18 @@ struct MyStruct2 {
v: u32, v: u32,
} }
const XX_CONST: usize = MyStruct2::field_offsets().xx; const XX_CONST: usize = MyStruct2::field_offsets().xx.get_byte_offset();
static D_STATIC: usize = MyStruct::field_offsets().d; static D_STATIC: usize = MyStruct::field_offsets().d.get_byte_offset();
#[test] #[test]
fn test() { fn test() {
assert_eq!(offset_of!(MyStruct, a), MyStruct::field_offsets().a); assert_eq!(offset_of!(MyStruct, a), MyStruct::field_offsets().a.get_byte_offset());
assert_eq!(offset_of!(MyStruct, b), MyStruct::field_offsets().b); assert_eq!(offset_of!(MyStruct, b), MyStruct::field_offsets().b.get_byte_offset());
assert_eq!(offset_of!(MyStruct, c), MyStruct::field_offsets().c); assert_eq!(offset_of!(MyStruct, c), MyStruct::field_offsets().c.get_byte_offset());
assert_eq!(offset_of!(MyStruct, d), MyStruct::field_offsets().d); assert_eq!(offset_of!(MyStruct, d), MyStruct::field_offsets().d.get_byte_offset());
assert_eq!(offset_of!(MyStruct2, xx), MyStruct2::field_offsets().xx); assert_eq!(offset_of!(MyStruct2, xx), MyStruct2::field_offsets().xx.get_byte_offset());
assert_eq!(offset_of!(MyStruct2, v), MyStruct2::field_offsets().v); assert_eq!(offset_of!(MyStruct2, v), MyStruct2::field_offsets().v.get_byte_offset());
assert_eq!(offset_of!(MyStruct2, k), MyStruct2::field_offsets().k); assert_eq!(offset_of!(MyStruct2, k), MyStruct2::field_offsets().k.get_byte_offset());
assert_eq!(XX_CONST, offset_of!(MyStruct2, xx)); assert_eq!(XX_CONST, offset_of!(MyStruct2, xx));
assert_eq!(D_STATIC, offset_of!(MyStruct, d)); assert_eq!(D_STATIC, offset_of!(MyStruct, d));

View file

@ -33,7 +33,7 @@ impl Item for Rectangle {
impl ItemConsts for Rectangle { impl ItemConsts for Rectangle {
const cached_rendering_data_offset: isize = const cached_rendering_data_offset: isize =
Rectangle::field_offsets().cached_rendering_data as isize; Rectangle::field_offsets().cached_rendering_data.get_byte_offset() as isize;
} }
// FIXME: remove (or use the libc one) // FIXME: remove (or use the libc one)
@ -80,7 +80,7 @@ impl Item for Image {
impl ItemConsts for Image { impl ItemConsts for Image {
const cached_rendering_data_offset: isize = const cached_rendering_data_offset: isize =
Image::field_offsets().cached_rendering_data as isize; Image::field_offsets().cached_rendering_data.get_byte_offset() as isize;
} }
#[no_mangle] #[no_mangle]

View file

@ -98,17 +98,18 @@ fn main() -> std::io::Result<()> {
// FIXME: thus obviously is unsafe and not great // FIXME: thus obviously is unsafe and not great
let mut rtti = HashMap::new(); let mut rtti = HashMap::new();
let offsets = Rectangle::field_offsets();
rtti.insert( rtti.insert(
"Rectangle", "Rectangle",
RuntimeTypeInfo { RuntimeTypeInfo {
vtable: &corelib::abi::primitives::RectangleVTable as _, vtable: &corelib::abi::primitives::RectangleVTable as _,
construct: construct::<Rectangle>, construct: construct::<Rectangle>,
properties: [ properties: [
("x", (Rectangle::field_offsets().x, set_property::<f32> as _)), ("x", (offsets.x.get_byte_offset(), set_property::<f32> as _)),
("y", (Rectangle::field_offsets().y, set_property::<f32> as _)), ("y", (offsets.y.get_byte_offset(), set_property::<f32> as _)),
("width", (Rectangle::field_offsets().width, set_property::<f32> as _)), ("width", (offsets.width.get_byte_offset(), set_property::<f32> as _)),
("height", (Rectangle::field_offsets().height, set_property::<f32> as _)), ("height", (offsets.height.get_byte_offset(), set_property::<f32> as _)),
("color", (Rectangle::field_offsets().color, set_property::<u32> as _)), ("color", (offsets.color.get_byte_offset(), set_property::<u32> as _)),
] ]
.iter() .iter()
.cloned() .cloned()
@ -116,17 +117,18 @@ fn main() -> std::io::Result<()> {
size: std::mem::size_of::<Rectangle>(), size: std::mem::size_of::<Rectangle>(),
}, },
); );
let offsets = Image::field_offsets();
rtti.insert( rtti.insert(
"Image", "Image",
RuntimeTypeInfo { RuntimeTypeInfo {
vtable: &corelib::abi::primitives::ImageVTable as _, vtable: &corelib::abi::primitives::ImageVTable as _,
construct: construct::<Image>, construct: construct::<Image>,
properties: [ properties: [
("x", (Image::field_offsets().x, set_property::<f32> as _)), ("x", (offsets.x.get_byte_offset(), set_property::<f32> as _)),
("y", (Image::field_offsets().y, set_property::<f32> as _)), ("y", (offsets.y.get_byte_offset(), set_property::<f32> as _)),
("width", (Image::field_offsets().width, set_property::<f32> as _)), ("width", (offsets.width.get_byte_offset(), set_property::<f32> as _)),
("height", (Image::field_offsets().height, set_property::<f32> as _)), ("height", (offsets.height.get_byte_offset(), set_property::<f32> as _)),
("source", (Image::field_offsets().source, set_property::<*const i8> as _)), ("source", (offsets.source.get_byte_offset(), set_property::<*const i8> as _)),
] ]
.iter() .iter()
.cloned() .cloned()