Transpiling to Rust

I'm wanting to transpile Shockscript to Rust. Any opinions? Could I miss something?

External crates

Auto boxing

Primitive classes are represented as structs and union classes are represented as unions. Besides a struct/union generated in Rust, another struct may be generated for the boxed form of primitive and union classes.

Class inheriting

Inheritance is anti-pattern in Rust, of course, but when transpiling to it, inheritance must be efficiently expressed. Rc won't be used for Object references, this is just an example.

use std::rc::Rc;

fn main() {
    let b = B::new(90);
    let a = b.as_a();
    println!("a.x = {}", a.as_b().x());
}

#[repr(C)]
struct A {
}

impl A {
    fn as_b(self:&Rc<Self>) -> Rc<B>
    {
        unsafe { Rc::from_raw(Rc::into_raw(self.clone()).cast::<B>()) }
    }
}

#[repr(C)]
struct B {
    _x: u32
}

impl B {
    fn new(x: u32) -> Rc<Self>
    {
        Rc::new(B { _x: x })
    }

    fn as_a(self:&Rc<Self>) -> Rc<A>
    {
        unsafe { Rc::from_raw(Rc::into_raw(self.clone()).cast::<A>()) }
    }

    fn x(self:&Rc<Self>) -> u32 {
        self._x
    }
}

Debugging

Lines and columns won't retain, as I guess there's no way to manipulate generated Rust spans, but I'll attempt to retain qualified definition names.

You as_* functions are unsound, you can't just cast the pointers and be on your way. Allocators require layouts to match exactly, so if even if you aren't using Rc, you can't just cast away to a different type.

I thought using #[repr(C)] would make the layouts compatible. I forgot to indicate the cast from A to B in the above example is incomplete (it should return Option and check if the self object is B really). B would also contain any fields from A (in same order).

Unless the types A and B are identical, you can't just cast away allocated memory like this. You can safely model inheritance as B holding an Rc<A>, (or whatever smart pointer you are using)

1 Like

In general, Rust simply doesn't have any way of creating user-defined types X and Y such that there is a Y directly contained within the memory layout of X without any indirection or heap allocation, unless you manually specify layouts for them like #[repr(C)], #[repr(transparent)], etc. You really do need a Box or Rc or Arc or something to make this work without UB today.

There's been much discussion in the past on what layout guarantees would be needed to make this feasible, and how we may or may not want to expose these features. However, for the forseeable future, there seems to be a silent but near-universal consensus that they're just not a huge priority for Rust right now (I suspect because everyone who needs highly optimized layouts can and does implement those layouts by hand, and inheritance is far from the only example of this). Regardless, here are some relevant links:

1 Like

To extend on this point, it's only sound to do a pointer cast from Child * to Parent * when both are #[repr(C)] and the first field in Child is a Parent.

This works because in C, a pointer to a struct is the same as a pointer to its first element. This is how "inheritance" is implemented in C, and you'll see it used in frameworks like COM and GTK.

#[repr(C)]
struct Parent {
    ...
}

#[repr(C)]
struct Child {
  parent: Parent,
  ...
}

impl Child {
    fn as_parent(self: &Rc<Child>) -> Rc<Parent> {
        unsafe { Rc::from_raw(Rc::into_raw(self.clone()).cast()) }
    }
}
2 Likes

This is still unsound because when Rc<Parent> drops, it may try and deallocate with a different Layout than it used to allocate, which is UB.
Note: it's fine to cast references in this way, only because references don't own their pointee.

1 Like

Thanks @RustyYato. I hadn't thought about the soundness issues of Rc because I only ever use this pattern for interop with C, where you're using raw pointers and free() doesn't care about layout.