Direct answer to cheap fields

#1

If we don’t have inheritance, then indeed all we can do to share x,y,z,alpha properties within a view hierarchy is using 2 heap objects plus enum or using traits, where all access is virtual (e.g. accessing x() field will cause unneccessary lookup)?

Just tell me and I’ll have to accept the Rust’s overhead style.

0 Likes

#2

If it’s still not clear, I mean…

Rc wrapping (x, y, z, alpha) + Box<Instance>, where Instance = enum Instance { TextArea { ... }, Bitmap { ... } }.

traits option: fn x(&self) -> f64; fn y(&self) -> f64; fn z(&self) -> f64; fn alpha(&self) -> f64;. Clearly, all of these clear choiches have overhead. One allocates one more on object heap for instance kind, while the another one has virtual accesses…

0 Likes

#3

Why do you need to box the enum? Is this self-referential in some way?

struct Foo {
    x: f32,
    y: f32,
    z: f32,
    a: f32,
    inst: Instance
}

enum Instance {
    TextArea { ... },
    BitMap { ... },
    NeedsFoo {
        foo: Box<Foo>,
        ...
    }
}

This should work if you don’t need to reference Foo inside of Instance. If you do you could box those up instead like above.

1 Like

#4

If I keep stuck to Instance variant fields, fine; but and when I wanna cast to the sub-type (e.g., TextArea)? That’s where comes the need for Box.

Hmm, that’d make container/canvas types slower at most cases, in view elements. They’re usually stored like this:

struct ToolBar {
    root: Rc<Sprite>,
}

/// toolbar.append(TextArea::new("Hi"))

Normally I can’t do this on Rust, unless I use unsafe and do some inheritance internals…

So, yeah, Box is indeed the solution. It doubles heap compared to C#, C++ and AS3. Idk if virtual calls are performantically worser than memory doubling.

0 Likes

#5

Don’t just settle for having your cake; let’s eat it, too!

You see, Rust has a neat little feature which allows the final field in a struct to be ?Sized.

pub struct Sprite<I: ?Sized> {
    x: f64,
    y: f64,
    z: f64,
    alpha: f64,
    inst: I,
}

This allows you to create some limited form of custom DSTs. Basically, you can have the type &Sprite<dyn Trait> (or Box<Sprite<dyn Trait>>, or *const Sprite<dyn Trait>…); these are fat pointers that point to a Sprite, but whose metadata is a Trait vtable.

Let’s define some things to be the I type.

pub struct TextAreaData {}
pub struct BitmapData {}

We’ll also need a trait for dynamic polymorphism. You implied earlier that you want to be able to access the x, y etc. fields on the subtypes, so I’ll make the trait methods take a &Sprite<Self> receiver.

pub trait Instance {
    // a simple example method
    fn name(_: &Sprite<Self>) -> &'static str;
}

…no wait, that turns them into static methods, which aren’t object safe. Okay, so we’ll make one small concession: collect the shared fields into one struct to make them easy to pass around.

pub struct SpriteData {
    x: f64,
    y: f64,
    z: f64,
    alpha: f64,
}

pub struct Sprite<I: ?Sized> {
    data: SpriteData,
    inst: I,
}

pub trait Instance {
    // a simple example method
    fn name(&self, data: &SpriteData) -> &'static str;
}
impl Instance for TextAreaData {
    // Because this is such a simple example we don't
    // actually use SpriteData, it was just here to show
    // how you could write a typical method on Instance.
    fn name(&self, _: &SpriteData) -> &'static str { "a text area" }
}

impl Instance for BitmapData {
    fn name(&self, _: &SpriteData) -> &'static str { "a bitmap" }
}

You can also just implement some shared functionality on Sprite itself:

impl<I: ?Sized + Instance> Sprite<I> {
    fn awesome_method(&self) {
        println!(
            "called awesome_method on {} at ({},{})",
            self.inst.name(&self.data), self.data.x, self.data.y,
        );
    }
}

…and as one final bit of polish, I’d define some type aliases:

type DynSprite = Sprite<dyn Instance>;
type TextArea = Sprite<TextAreaData>;
type Bitmap = Sprite<BitmapData>;

(but for now I won’t use them, to make the coercions in the below code more obvious)


Now let’s see what we can do with it:

// Here's a TextArea on the stack.
let sprite: Sprite<TextAreaData> = Sprite {
    data: SpriteData { x: 10.0, y: 15.0, z: 20.0, alpha: 1.0 },
    inst: TextAreaData {},
};
println!("x is {}", sprite.data.x); // direct field access
sprite.awesome_method(); // trait method access

Now, to go dynamic, you can take advantage of the T: Unsize<U> clause described in the nomicon page on coercions:

  • Box<Sprite<TextArea>> coerces to Box<Sprite<dyn Instance>>
  • &Sprite<TextArea> coerces to &Sprite<dyn Instance>
  • Rc<Sprite<TextArea>> coerces to Rc<Sprite<dyn Instance>>
  • …basically, you can do this coercion behind any standard library pointer type.

So here’s a dynamic sprite with zero allocations!

// Do a custom unsizing coercion via:
//   - (stdlib)   impl CoerceUnsized<&U> for &T
//   - (implicit) impl Unsize<Sprite<dyn Instance>> for Sprite<TextArea>
let ref_sprite: &Sprite<dyn Instance> = &sprite;

println!("x is {}", ref_sprite.data.x); // direct field access still works
ref_sprite.awesome_method(); // trait method access still works

and if you need freedom from lifetimes, you can allocate at your leisure:

let box_sprite: Box<Sprite<dyn Instance>> = Box::new(sprite);

println!("x is {}", box_sprite.data.x); // direct field access still works
box_sprite.awesome_method(); // trait method access still works
7 Likes