Skip to content

Commit 751a7d4

Browse files
committed
Add support for factory functions and static methods.
It's a fairly common pattern for object-oriented interfaces to have named functions or 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 factories by: * Allowing owned object instances to be returned from functions and methods. * Allowing interface methods to be marked as `static`. The "owned object instances" part here is really key, since it allows us to bypass a lot of the general concerns about returning *references* to object instances that are outlined in #197 (and that this commit does not at all attempt to tackle).
1 parent a5b5a49 commit 751a7d4

File tree

19 files changed

+263
-34
lines changed

19 files changed

+263
-34
lines changed

docs/manual/src/udl/interfaces.md

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

77+
## Static Methods
78+
79+
Interface methods can be marked with the `static` keyword to mark them as belonging to the interface
80+
itself rather than to a particular instance. Static methods are commonly used to make named alternatives to
81+
the default constructor, like this:
82+
83+
```idl
84+
interface TodoList {
85+
// The default constructor makes an empty list.
86+
constructor();
87+
// This static method is a shortcut for making a new TodoList from a list of items.
88+
static TodoList new_from_items(sequence<string>)
89+
...
90+
```
91+
92+
UniFFI will expose an appropriate static-method, class-method or similar in the foreign language binding,
93+
and will connect it to the Rust method of the same name on the underlying Rust struct.
94+
95+
7796
## Concurrent Access
7897

7998
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+
static Sprite new_relative_to(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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ already_AddRefed<{{ obj.name()|class_name_cpp(context) }}> {{ obj.name()|class_n
6464
{%- endfor %}
6565

6666
{%- for meth in obj.methods() %}
67+
{% if meth.is_static() %}
68+
MOZ_STATIC_ASSERT(false, "Sorry the gecko-js backend does not yet support static methods");
69+
{% endif %}
6770

6871
{% 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 }}(
6972
{%- for arg in meth.cpp_arguments() %}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ interface {{ obj.name()|class_name_webidl(context) }} {
6666
{%- if meth.throws().is_some() %}
6767
[Throws]
6868
{% endif %}
69-
{%- match meth.webidl_return_type() -%}{%- when Some with (type_) %}{{ type_|type_webidl(context) }}{% when None %}void{% endmatch %} {{ meth.name()|fn_name_webidl }}(
69+
{%- match meth.webidl_return_type() -%}{%- when Some with (type_) %}{{ type_|type_webidl(context) }}{% when None %}void{% endmatch %} {% if meth.is_static() %}static {% endif -%} {{ meth.name()|fn_name_webidl }}(
7070
{%- for arg in meth.arguments() %}
7171
{% if arg.optional() -%}optional{%- else -%}{%- endif %} {{ arg.webidl_type()|type_webidl(context) }} {{ arg.name() }}
7272
{%- match arg.webidl_default_value() %}

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

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
public interface {{ obj.name()|class_name_kt }}Interface {
22
{% for meth in obj.methods() -%}
3+
{%- if ! meth.is_static() -%}
34
fun {{ meth.name()|fn_name_kt }}({% call kt::arg_list_decl(meth) %})
45
{%- match meth.return_type() -%}
56
{%- when Some with (return_type) %}: {{ return_type|type_kt -}}
67
{%- else -%}
7-
{%- endmatch %}
8+
{%- endmatch -%}
9+
{%- endif %}
810
{% endfor %}
911
}
1012

@@ -38,7 +40,9 @@ class {{ obj.name()|class_name_kt }}(
3840
}
3941
}
4042

43+
{# // Instance methods #}
4144
{% for meth in obj.methods() -%}
45+
{%- if ! meth.is_static() -%}
4246
{%- match meth.return_type() -%}
4347

4448
{%- when Some with (return_type) -%}
@@ -48,12 +52,37 @@ class {{ obj.name()|class_name_kt }}(
4852
}.let {
4953
{{ "it"|lift_kt(return_type) }}
5054
}
51-
55+
5256
{%- when None -%}
5357
override fun {{ meth.name()|fn_name_kt }}({% call kt::arg_list_protocol(meth) %}) =
5458
callWithHandle {
55-
{%- call kt::to_ffi_call_with_prefix("it", meth) %}
59+
{%- call kt::to_ffi_call_with_prefix("it", meth) %}
5660
}
5761
{% endmatch %}
62+
{%- endif -%}
5863
{% endfor %}
64+
65+
companion object {
66+
internal fun lift(handle: Long): {{ obj.name()|class_name_kt }} {
67+
return {{ obj.name()|class_name_kt }}(handle)
68+
}
69+
70+
{# // Static methods, if any #}
71+
{% for meth in obj.methods() -%}
72+
{%- if meth.is_static() -%}
73+
{%- match meth.return_type() -%}
74+
75+
{%- when Some with (return_type) -%}
76+
fun {{ meth.name()|fn_name_kt }}({% call kt::arg_list_decl(meth) %}): {{ return_type|type_kt }} {
77+
val _retval = {% call kt::to_ffi_call(meth) %}
78+
return {{ "_retval"|lift_kt(return_type) }}
79+
}
80+
81+
{%- when None -%}
82+
fun {{ meth.name()|fn_name_kt }}({% call kt::arg_list_decl(meth) %}) =
83+
{% call kt::to_ffi_call(meth) %}
84+
{% endmatch %}
85+
{%- endif -%}
86+
{% endfor %}
87+
}
5988
}

uniffi_bindgen/src/bindings/python/gen_python.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ mod filters {
181181
Type::Boolean => format!("(True if {} else False)", nm),
182182
Type::String => format!("{}.consumeIntoString()", nm),
183183
Type::Enum(name) => format!("{}({})", class_name_py(name)?, nm),
184-
Type::Object(_) => panic!("No support for lifting objects, yet"),
184+
Type::Object(name) => format!("liftObject({}, {})", class_name_py(name)?, nm),
185185
Type::CallbackInterface(_) => panic!("No support for lifting callback interfaces, yet"),
186186
Type::Error(_) => panic!("No support for lowering errors, yet"),
187187
Type::Record(_) | Type::Optional(_) | Type::Sequence(_) | Type::Map(_) => format!(
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Miscellaneous "private" helpers.
2+
#
3+
# These are things that we need to have available for internal use,
4+
# but that we don't want to expose as part of the public API classes,
5+
# even as "hidden" methods.
6+
7+
{% if ci.iter_object_definitions().len() > 0 %}
8+
def liftObject(cls, handle):
9+
"""Helper to create an object instace from a handle.
10+
11+
This bypasses the usual __init__() logic for the given class
12+
and just directly creates an instance with the given handle.
13+
It's used to support factory functions and methods, which need
14+
to return instances without invoking a (Python-level) constructor.
15+
"""
16+
obj = cls.__new__(cls)
17+
obj._handle = handle
18+
return obj
19+
{% endif %}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,24 @@ def __del__(self):
1414
)
1515

1616
{% for meth in obj.methods() -%}
17+
{%- if meth.is_static() -%}
18+
{%- match meth.return_type() -%}
19+
20+
{%- when Some with (return_type) -%}
21+
@staticmethod
22+
def {{ meth.name()|fn_name_py }}({% call py::arg_list_decl(meth) %}):
23+
{%- call py::coerce_args_extra_indent(meth) %}
24+
_retval = {% call py::to_ffi_call(meth) %}
25+
return {{ "_retval"|lift_py(return_type) }}
26+
27+
{%- when None -%}
28+
@staticmethod
29+
def {{ meth.name()|fn_name_py }}({% call py::arg_list_decl(meth) %}):
30+
{%- call py::coerce_args_extra_indent(meth) %}
31+
{% call py::to_ffi_call(meth) %}
32+
{% endmatch %}
33+
34+
{%- else -%}
1735
{%- match meth.return_type() -%}
1836

1937
{%- when Some with (return_type) -%}
@@ -27,4 +45,5 @@ def {{ meth.name()|fn_name_py }}(self, {% call py::arg_list_decl(meth) %}):
2745
{%- call py::coerce_args_extra_indent(meth) %}
2846
{% call py::to_ffi_call_with_prefix("self._handle", meth) %}
2947
{% endmatch %}
48+
{% endif %}
3049
{% endfor %}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
{% include "RustBufferTemplate.py" %}
2323
{% include "RustBufferStream.py" %}
2424
{% include "RustBufferBuilder.py" %}
25+
{% include "Helpers.py" %}
2526

2627
# Error definitions
2728
{% include "ErrorTemplate.py" %}

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11

22
public protocol {{ obj.name() }}Protocol {
33
{% for meth in obj.methods() -%}
4+
{%- if ! meth.is_static() %}
45
func {{ meth.name()|fn_name_swift }}({% call swift::arg_list_protocol(meth) %}) {% call swift::throws(meth) -%}
56
{%- match meth.return_type() -%}
67
{%- when Some with (return_type) %} -> {{ return_type|type_swift -}}
78
{%- else -%}
89
{%- endmatch %}
10+
{%- endif %}
911
{% endfor %}
1012
}
1113

1214
public class {{ obj.name() }}: {{ obj.name() }}Protocol {
1315
private let handle: UInt64
1416

17+
init(fromRawHandle handle: UInt64) {
18+
self.handle = handle
19+
}
20+
1521
{%- for cons in obj.constructors() %}
1622
public init({% call swift::arg_list_decl(cons) -%}) {% call swift::throws(cons) %} {
1723
self.handle = {% call swift::to_ffi_call(cons) %}
@@ -24,8 +30,27 @@ public class {{ obj.name() }}: {{ obj.name() }}Protocol {
2430
}
2531
}
2632

27-
// TODO: Maybe merge the two templates (i.e the one with a return type and the one without)
33+
static func lift(_ handle: UInt64) throws -> {{ obj.name()|class_name_swift }} {
34+
{{ obj.name()|class_name_swift }}(fromRawHandle: handle)
35+
}
36+
37+
{# // TODO: Maybe merge the two templates (i.e the one with a return type and the one without) #}
2838
{% for meth in obj.methods() -%}
39+
{%- if meth.is_static() %}
40+
{%- match meth.return_type() -%}
41+
42+
{%- when Some with (return_type) -%}
43+
public static func {{ meth.name()|fn_name_swift }}({% call swift::arg_list_decl(meth) %}) {% call swift::throws(meth) %} -> {{ return_type|type_swift }} {
44+
let _retval = {% call swift::to_ffi_call(meth) %}
45+
return {% call swift::try(meth) %} {{ "_retval"|lift_swift(return_type) }}
46+
}
47+
48+
{%- when None -%}
49+
public static func {{ meth.name()|fn_name_swift }}({% call swift::arg_list_decl(meth) %}) {% call swift::throws(meth) %} {
50+
{% call swift::to_ffi_call(meth) %}
51+
}
52+
{%- endmatch %}
53+
{%- else -%}
2954
{%- match meth.return_type() -%}
3055

3156
{%- when Some with (return_type) -%}
@@ -39,5 +64,6 @@ public class {{ obj.name() }}: {{ obj.name() }}Protocol {
3964
{% call swift::to_ffi_call_with_prefix("self.handle", meth) %}
4065
}
4166
{%- endmatch %}
67+
{%- endif %}
4268
{% endfor %}
4369
}

uniffi_bindgen/src/interface/function.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,6 @@ impl APIConverter<Function> for weedle::namespace::NamespaceMember<'_> {
112112
impl APIConverter<Function> for weedle::namespace::OperationNamespaceMember<'_> {
113113
fn convert(&self, ci: &mut ComponentInterface) -> Result<Function> {
114114
let return_type = ci.resolve_return_type_expression(&self.return_type)?;
115-
if let Some(Type::Object(_)) = return_type {
116-
bail!("Objects cannot currently be returned from functions");
117-
}
118115
Ok(Function {
119116
name: match self.identifier {
120117
None => bail!("anonymous functions are not supported {:?}", self),

0 commit comments

Comments
 (0)