Skip to content

Commit 5aed68d

Browse files
committed
Add support for named alternate constructors.
It's a fairly common pattern for object-oriented interfaces to have static methods that act as named "factories" for producing object instances in ways that differ from the default constructor. This commit adds basic support for such named alternate constructors: * In the UDL, declare additional constructors with a [Name=Value] attribute to tell the tool what name should be exposed under. * In the Rust code, implement a corresponding method on the struct that acts as a constructor (no `self` parameter, `Self` return type). * In the foreign-language code, expect a static method on the resulting class with name matching the one provided in Rust. We continue to reserve the name "new" to refer to the default constructor. A followup could consider annotating it with an additional attribute like [DefaultConstructor] in order to let the default constructor have a different name on the Rust side.
1 parent a5b5a49 commit 5aed68d

File tree

15 files changed

+370
-57
lines changed

15 files changed

+370
-57
lines changed

docs/manual/src/udl/interfaces.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,27 @@ func display(list: TodoListProtocol) {
7474
}
7575
```
7676

77+
## Alternate Named Constructors
78+
79+
In addition to the default constructor connected to the `::new()` method, you can specify
80+
alternate named constructors to create object instances in different ways. Each such constructor
81+
must be given an explicit name, provided in the UDL with the `[Name]` attribute like so:
82+
83+
```idl
84+
interface TodoList {
85+
// The default constructor makes an empty list.
86+
constructor();
87+
// This alternate constructor makes a new TodoList from a list of string items.
88+
[Name=new_from_items]
89+
constructor(sequence<string> items)
90+
...
91+
```
92+
93+
For each alternate constructor, UniFFI will expose an appropriate static-method, class-method or similar
94+
in the foreign language binding, and will connect it to the Rust method of the same name on the underlying
95+
Rust struct.
96+
97+
7798
## Concurrent Access
7899

79100
Since interfaces represent mutable data, uniffi has to take extra care

examples/sprites/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ impl Sprite {
3939
}
4040
}
4141

42+
fn new_relative_to(reference: Point, direction: Vector) -> Sprite {
43+
Sprite {
44+
current_position: translate(&reference, direction),
45+
}
46+
}
47+
4248
fn get_position(&self) -> Point {
4349
self.current_position.clone()
4450
}

examples/sprites/src/sprites.udl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ dictionary Vector {
1414
};
1515

1616
interface Sprite {
17-
// Should be an optional, but I had to test nullable args :)
1817
constructor(Point? initial_position);
18+
[Name=new_relative_to] constructor(Point reference, Vector direction);
1919
Point get_position();
2020
void move_to(Point position);
2121
void move_by(Vector direction);

examples/sprites/tests/bindings/test_sprites.kts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@ s.moveBy(Vector(-4.0, 2.0))
1313
assert( s.getPosition() == Point(-3.0, 4.0) )
1414

1515
s.destroy()
16-
try {
16+
try {
1717
s.moveBy(Vector(0.0, 0.0))
1818
assert(false) { "Should not be able to call anything after `destroy`" }
1919
} catch(e: IllegalStateException) {
2020
assert(true)
21-
}
21+
}
22+
23+
val srel = Sprite.newRelativeTo(Point(0.0, 1.0), Vector(1.0, 1.5))
24+
assert( srel.getPosition() == Point(1.0, 2.5) )
25+

examples/sprites/tests/bindings/test_sprites.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@
1111

1212
s.move_by(Vector(-4, 2))
1313
assert s.get_position() == Point(-3, 4)
14+
15+
srel = Sprite.new_relative_to(Point(0, 1), Vector(1, 1.5))
16+
assert srel.get_position() == Point(1, 2.5)
17+

examples/sprites/tests/bindings/test_sprites.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ assert( s.getPosition() == Point(x: 1, y: 2))
1212
s.moveBy(direction: Vector(dx: -4, dy: 2))
1313
assert( s.getPosition() == Point(x: -3, y: 4))
1414

15-
15+
let srel = Sprite.newRelativeTo(reference: Point(x: 0.0, y: 1.0), direction: Vector(dx: 1, dy: 1.5))
16+
assert( srel.getPosition() == Point(x: 1.0, y: 2.5) )

examples/sprites/tests/test_generated_bindings.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
uniffi_macros::build_foreign_language_testcases!(
22
"src/sprites.udl",
33
[
4-
// "tests/bindings/test_sprites.py",
4+
"tests/bindings/test_sprites.py",
55
"tests/bindings/test_sprites.kts",
66
"tests/bindings/test_sprites.swift",
77
]

uniffi_bindgen/src/bindings/gecko_js/templates/InterfaceTemplate.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ JSObject* {{ obj.name()|class_name_cpp(context) }}::WrapObject(
3939
return dom::{{ obj.name()|class_name_cpp(context) }}_Binding::Wrap(aCx, this, aGivenProto);
4040
}
4141

42-
{%- for cons in obj.constructors() %}
42+
{%- match obj.primary_constructor() %}
43+
{%- when Some with (cons) %}
4344

4445
/* static */
4546
already_AddRefed<{{ obj.name()|class_name_cpp(context) }}> {{ obj.name()|class_name_cpp(context) }}::Constructor(
@@ -61,10 +62,14 @@ already_AddRefed<{{ obj.name()|class_name_cpp(context) }}> {{ obj.name()|class_n
6162
auto result = MakeRefPtr<{{ obj.name()|class_name_cpp(context) }}>(global, handle);
6263
return result.forget();
6364
}
65+
{%- when None %}
66+
{%- endmatch %}
67+
68+
{%- for cons in obj.alternate_constructors() %}
69+
MOZ_STATIC_ASSERT(false, "Sorry the gecko-js backend does not yet support alternate constructors");
6470
{%- endfor %}
6571

6672
{%- for meth in obj.methods() %}
67-
6873
{% match meth.cpp_return_type() %}{% when Some with (type_) %}{{ type_|ret_type_cpp(context) }}{% else %}void{% endmatch %} {{ obj.name()|class_name_cpp(context) }}::{{ meth.name()|fn_name_cpp }}(
6974
{%- for arg in meth.cpp_arguments() %}
7075
{{ arg|arg_type_cpp(context) }} {{ arg.name() }}{%- if !loop.last %},{% endif %}

uniffi_bindgen/src/bindings/gecko_js/templates/WebIDLTemplate.webidl

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ namespace {{ context.namespace()|class_name_webidl(context) }} {
4646
{%- for obj in ci.iter_object_definitions() %}
4747
[ChromeOnly, Exposed=Window]
4848
interface {{ obj.name()|class_name_webidl(context) }} {
49-
{%- for cons in obj.constructors() %}
49+
50+
{%- match obj.primary_constructor() %}
51+
{%- when Some with (cons) %}
5052
{%- if cons.throws().is_some() %}
5153
[Throws]
5254
{% endif %}
@@ -60,6 +62,23 @@ interface {{ obj.name()|class_name_webidl(context) }} {
6062
{%- if !loop.last %}, {% endif %}
6163
{%- endfor %}
6264
);
65+
{%- when None %}
66+
{%- endmatch %}
67+
68+
{%- for cons in obj.alternate_constructors() %}
69+
{%- if cons.throws().is_some() %}
70+
[Throws]
71+
{% endif %}
72+
{{ obj.name()|class_name_webidl(context) }} {{ cons.name()|fn_name_webidl }}(
73+
{%- for arg in cons.arguments() %}
74+
{% if arg.optional() -%}optional{%- else -%}{%- endif %} {{ arg.webidl_type()|type_webidl(context) }} {{ arg.name() }}
75+
{%- match arg.webidl_default_value() %}
76+
{%- when Some with (literal) %} = {{ literal|literal_webidl }}
77+
{%- else %}
78+
{%- endmatch %}
79+
{%- if !loop.last %}, {% endif %}
80+
{%- endfor %}
81+
);
6382
{%- endfor %}
6483

6584
{% for meth in obj.methods() -%}

uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ class {{ obj.name()|class_name_kt }}(
1212
handle: Long
1313
) : FFIObject(AtomicLong(handle)), {{ obj.name()|class_name_kt }}Interface {
1414

15-
{%- for cons in obj.constructors() %}
15+
{%- match obj.primary_constructor() %}
16+
{%- when Some with (cons) %}
1617
constructor({% call kt::arg_list_decl(cons) -%}) :
1718
this({% call kt::to_ffi_call(cons) %})
18-
{%- endfor %}
19+
{%- when None %}
20+
{%- endmatch %}
1921

2022
/**
2123
* Disconnect the object from the underlying Rust object.
@@ -48,12 +50,21 @@ class {{ obj.name()|class_name_kt }}(
4850
}.let {
4951
{{ "it"|lift_kt(return_type) }}
5052
}
51-
53+
5254
{%- when None -%}
5355
override fun {{ meth.name()|fn_name_kt }}({% call kt::arg_list_protocol(meth) %}) =
5456
callWithHandle {
55-
{%- call kt::to_ffi_call_with_prefix("it", meth) %}
57+
{%- call kt::to_ffi_call_with_prefix("it", meth) %}
5658
}
5759
{% endmatch %}
5860
{% endfor %}
61+
62+
{% if obj.constructors().len() > 1 -%}
63+
companion object {
64+
{% for cons in obj.alternate_constructors() -%}
65+
fun {{ cons.name()|fn_name_kt }}({% call kt::arg_list_decl(cons) %}): {{ obj.name()|class_name_kt }} =
66+
{{ obj.name()|class_name_kt }}({% call kt::to_ffi_call(cons) %})
67+
{% endfor %}
68+
}
69+
{%- endif %}
5970
}

uniffi_bindgen/src/bindings/python/gen_python.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ mod filters {
183183
Type::Enum(name) => format!("{}({})", class_name_py(name)?, nm),
184184
Type::Object(_) => panic!("No support for lifting objects, yet"),
185185
Type::CallbackInterface(_) => panic!("No support for lifting callback interfaces, yet"),
186-
Type::Error(_) => panic!("No support for lowering errors, yet"),
186+
Type::Error(_) => panic!("No support for lifting errors, yet"),
187187
Type::Record(_) | Type::Optional(_) | Type::Sequence(_) | Type::Map(_) => format!(
188188
"{}.consumeInto{}()",
189189
nm,

uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
class {{ obj.name()|class_name_py }}(object):
2-
# XXX TODO: support for multiple constructors...
3-
{%- for cons in obj.constructors() %}
2+
{%- match obj.primary_constructor() %}
3+
{%- when Some with (cons) %}
44
def __init__(self, {% call py::arg_list_decl(cons) -%}):
55
{%- call py::coerce_args_extra_indent(cons) %}
6-
self._handle = {% call py::to_ffi_call(cons) %}
7-
{%- endfor %}
6+
self._handle = {% call py::to_ffi_call(cons) %}\
7+
{%- when None %}
8+
{%- endmatch %}
89

910
def __del__(self):
1011
rust_call_with_error(
@@ -13,6 +14,19 @@ def __del__(self):
1314
self._handle
1415
)
1516

17+
{% for cons in obj.alternate_constructors() -%}
18+
@classmethod
19+
def {{ cons.name()|fn_name_py }}(cls, {% call py::arg_list_decl(cons) %}):
20+
{%- call py::coerce_args_extra_indent(cons) %}
21+
# Call the (fallible) function before creating any half-baked object instances.
22+
handle = {% call py::to_ffi_call(cons) %}
23+
# Lightly yucky way to bypass the usual __init__ logic
24+
# and just create a new instance with the required handle.
25+
inst = cls.__new__(cls)
26+
inst._handle = handle
27+
return inst
28+
{% endfor %}
29+
1630
{% for meth in obj.methods() -%}
1731
{%- match meth.return_type() -%}
1832

uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,34 @@ public protocol {{ obj.name() }}Protocol {
99
{% endfor %}
1010
}
1111

12-
public class {{ obj.name() }}: {{ obj.name() }}Protocol {
12+
public class {{ obj.name()|class_name_swift }}: {{ obj.name() }}Protocol {
1313
private let handle: UInt64
1414

15-
{%- for cons in obj.constructors() %}
16-
public init({% call swift::arg_list_decl(cons) -%}) {% call swift::throws(cons) %} {
17-
self.handle = {% call swift::to_ffi_call(cons) %}
15+
private init(fromRawHandle handle: UInt64) {
16+
self.handle = handle
1817
}
19-
{%- endfor %}
18+
19+
{%- match obj.primary_constructor() %}
20+
{%- when Some with (cons) %}
21+
public convenience init({% call swift::arg_list_decl(cons) -%}) {% call swift::throws(cons) %} {
22+
self.init(fromRawHandle: {% call swift::to_ffi_call(cons) %})
23+
}
24+
{%- when None %}
25+
{%- endmatch %}
2026

2127
deinit {
2228
try! rustCall(InternalError.unknown()) { err in
2329
{{ obj.ffi_object_free().name() }}(handle, err)
2430
}
2531
}
2632

27-
// TODO: Maybe merge the two templates (i.e the one with a return type and the one without)
33+
{% for cons in obj.alternate_constructors() %}
34+
public static func {{ cons.name()|fn_name_swift }}({% call swift::arg_list_decl(cons) %}) {% call swift::throws(cons) %} -> {{ obj.name()|class_name_swift }} {
35+
return {{ obj.name()|class_name_swift }}(fromRawHandle: {% call swift::to_ffi_call(cons) %})
36+
}
37+
{% endfor %}
38+
39+
{# // TODO: Maybe merge the two templates (i.e the one with a return type and the one without) #}
2840
{% for meth in obj.methods() -%}
2941
{%- match meth.return_type() -%}
3042

0 commit comments

Comments
 (0)