Skip to content

Rust Interop - Emitting type decorators in a target language. #2887

Closed
@alexswan10k

Description

@alexswan10k

Following on from #2875, I wanted to discuss possible approaches to creating bindings for languages specifically which need type information. The approach will probably be relevant for any typed target language where there is asymmetry between source and target types, otherwise import would probably suffice.

So currently you can do simple operations by using emit to bridge the impedance mismatch between the F# domain and the underlying Rust domain. Import alone is not sufficient because you effecrively have to transform the parameters and return types as they are called.

    module Vec =
        [<Erase>]
        type VecT =
            [<Emit("$0.get_mut().push($1)")>]
            abstract Push: 'a -> unit
        [<Emit("MutCell::from(std::vec::Vec::new())")>]
        let create (): VecT = jsNative
        [<Emit("$1.get_mut().push($0)")>]
        let push item (vec: VecT) = jsNative
        [<Emit("{ $1.get_mut().append(&mut vec![$0]); $1 }")>]
        let append item (vec: VecT): VecT = jsNative

This lets you use Vec as if it was a struct with static methods (which is basically how Rust models instances).

let a = Subs.Vec.create()
a |> Vec.push 3 |> Vec.push 4

This is fine for simple cases, but Rust does not have the level of type inference that F# does, and at some point, you are probably going to want to pass a Vec (or anything you are abstracting) over a boundary (function parameter or add to record etc), which will require type annotation. Currently, this will break because there is no way to define how the type is represented in the target type system. It will output the dotnet type, which is nonsense.

pub fn (v: &IrrelevantNamespace.Vec<?>)  {
   ...
}

Now notice this is a particularly problematic example because not only is there no definition, but there is also no way to define generic parameters.

Currently, Emit does not work on instances I believe, but presumably, it could be extended to emit on constructor calls, and any instances that need type decorators could perhaps use a new attribute EmitType to instruct Fable on how to generate the type decorator

[<Erase, Emit("Rc::new(MutCell::new(std::vec::Vec::new()))"), EmitType("Rc<MutCell<std::vec::Vec<$0>>>")>]
type Vec<'a>() =
    [<Emit("(*$0).get_mut().push($1)")>]
    member x.Push a = jsNative
    [<Emit("$0.clone()")>]
    member x.Clone (): Vec<'a> = jsNative
...etc

My generated code might then look like this

pub fn (v: &Rc<MutCell<std::vec::Vec<i32>>>)  {
   ...
}

let v: Rc<MutCell<std::vec::Vec<i32>>> = Rc::new(MutCell::new(std::vec::Vec::new())) // type annotation not actually needed here but for illustration purposes

In the same way that Emit allows placeholders to exist for parameter variables, perhaps EmitType would be able to do the same for generic parameters. This would allow modelling of generic collections, among other things.

I imagine this is going to require changes to the core Fable AST as the Emit concept is baked right in. Is this feasible?

One final caveat. In rust a constructor is rrally just a function called new. This means it does not have to return the same type, it might be wrapped in a result, or there might be multiple constructors. With all this in mind, perhaps static method are the way to go. The type decorator problem remains though!

Previous discussions covering dart etc #2779

#2774 (comment)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions