flutter_rust_bridge icon indicating copy to clipboard operation
flutter_rust_bridge copied to clipboard

What about constructors?

Open Dampfwalze opened this issue 1 year ago • 4 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,
        }
    }
}

Dampfwalze avatar Feb 16 '24 18:02 Dampfwalze

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)

fzyzcjy avatar Feb 16 '24 23:02 fzyzcjy

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.

Dampfwalze avatar Feb 17 '24 00:02 Dampfwalze

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!

fzyzcjy avatar Feb 17 '24 01:02 fzyzcjy

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)

fzyzcjy avatar Feb 17 '24 04:02 fzyzcjy

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.

github-actions[bot] avatar Mar 21 '24 00:03 github-actions[bot]