fzyzcjy / flutter_rust_bridge

Flutter/Dart <-> Rust binding generator, feature-rich, but seamless and simple.

Home Page:https://fzyzcjy.github.io/flutter_rust_bridge/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

What about constructors?

Dampfwalze opened this issue · comments

Rust does not have a built-in feature for constructors, but the norm is to define an associated function new(...) for that type.

Dart has constructors as a feature. One can define up to one constructor and multiple named constructors. (A factory acts like a static method, returning an instance of a class, but has the same Syntax as a constructor to the class user)

Currently, on the Dart side, there is a constructor being generated that initializes all fields on the Dart side. But sometimes, the constructor on the Rust side is mandatory.

Solution

If a constructor on the Rust side should be used, generate a factory constructor that calls that constructor and do not expose the default constructor to the public API, if the struct does not have attribute #[frb(default_constructor)].

What should be considered a constructor?

  • An associated function should be considered a constructor,
    • if it does not have attribute #[frb(no_constructor)] and
    • if its signature is pub fn new(...) -> Self, where Self can also be the struct type, or
    • if it returns Self or the struct type and has attribute #[frb(constructor(default))]
  • An associated function should be considered a named constructor,
    • if it does not have attribute #[frb(no_constructor)] and
    • if it returns Self or the struct type and its name is not new. The name of the constructor should be
      • <name>, if it has attribute #[frb(constructor("<name>"))], or
      • the name of the function, otherwise
    • if its signature is pub fn new(...) -> Self, where Self can also be the struct type and it has attribute #[frb(constructor("<name>"))], where the name of the constructor is <name>.

Alternatives

Try to ignore the Dart constructor and only call MyStruct.newMyStruct(...). This is bad, if you have a public API

Additional context

Example
pub struct Point {
    pub x: f64,
    pub y: f64,
}

impl Point {
    // Constructor
    pub fn new(x: f64, y: f64) -> Point {
        Self { x, y }
    }

    // Named constructor
    pub fn all(v: f64) -> Point {
        Self { x: v, y: v }
    }

    // Renamed named constructor
    #[frb(constructor("with_magnitude"))]
    pub fn with_length(x: f64, y: f64, length: f64) -> Point {
        let scale = length / (x * x + y * y).sqrt();
        Self {
            x: x * scale,
            y: y * scale,
        }
    }

    // Not a constructor
    #[frb(no_constructor)]
    pub fn add(a: &Point, b: &Point) -> Point {
        Self {
            x: a.x + b.x,
            y: a.y + b.y,
        }
    }
}

Totally agree that MyStruct.newMyStruct is quite suboptimal!

However, as for the constructor, there is one problem: Dart requires any dart constructor to be synchronous. Thus, if we have #[frb(sync)] fn new() -> Self, it is easy to be a Dart constructor; but if it is just fn new() -> Self, then we cannot :( What do you think?

Btw, just a small tweak about the conditions you suggest: We may not consider all non-method be constructors, since it may just be a normal static method. But we can allow users to explicitly make it a constructor when using something like frb(constructor) Oh I see your condition requires "return self", which looks reasonable! (I will consider this small detail a bit more later)

However, as for the constructor, there is one problem: Dart requires any dart constructor to be synchronous. Thus, if we have #[frb(sync)] fn new() -> Self, it is easy to be a Dart constructor; but if it is just fn new() -> Self, then we cannot :( What do you think?

Oh yea... I totally overlooked that! 🤦‍♂️ However, I think it would be fine to implicitly mark constructors as sync, since constructors are rarely that complicated that the extra overhead of calling it asynchronously is even worth it. But if a constructor is marked with #[frb(async)], you could generate a static method (acting like a constructor) instead of a factory constructor, that returns a Future<Self> and, in the case of an unnamed constructor, is named new.

Alternatively, if you decide, that this is an antipattern, I think it would also be fine, if there is the added requirement, that a constructor must be marked with #[frb(sync)], otherwise generate a static method.

I am a bit worried about making it sync by default, since that may make users confused. The explicit frb(sync) LGTM, but that is a bit verbose... So yes, I am also hesitate among the two choices!

If we use option 1, we can unify the DX a little bit more by adding:

  • When using getters (#[frb(getter)]), automatically make them sync as well. (Since getters are usually intended to be synchronous and small)

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new issue.