Skip to content

Commit 3a63c2b

Browse files
committed
Add support for enums with associated data.
This commit adds support for enums variants having named data fields, in a style similar to records. It also attempts to expose both "plain" enums and the new data-bearing enums to target foreign languages in an idiomatic way. Unfortunately for us, WebIDL doesn't have native syntax for this kind of data. Fortunately for us, we can fake it by using anonymous special interface methods via a syntax like: ``` [Enum] interface EnumWithData { VariantName(type1 name1, type2 name2, ...); } ```
1 parent 4d61601 commit 3a63c2b

30 files changed

+871
-158
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ Things that are implemented so far:
8888

8989
* Primitive numeric types, equivalents to those offered by Rust (`u32`, `f64`, etc).
9090
* Strings (which are always UTF-8, like Rust's `String`).
91-
* C-style enums (just the discriminant, no associated data).
91+
* Enums, including enums with associated data (aka "tagged unions" or "sum types").
9292
* C-style structs containing named fields (we call these *records*).
9393
* Sequences of all of the above (like Rust's `Vec<T>`).
9494
* Optional instances of all of the above (like Rust's `Option<T>`).
@@ -98,7 +98,6 @@ Things that are implemented so far:
9898

9999
Things that are not implemented yet:
100100

101-
* Enums with associated data.
102101
* Union types.
103102
* Efficient access to binary data (like Rust's `Vec<u8>`).
104103
* Passing object references to functions or methods.

docs/manual/src/internals/lifting_and_lowering.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Calling this function from foreign language code involves the following steps:
6262
| `T?` | `RustBuffer` struct pointing to serialized bytes |
6363
| `sequence<T>` | `RustBuffer` struct pointing to serialized bytes |
6464
| `record<DOMString, T>` | `RustBuffer` struct pointing to serialized bytes |
65-
| `enum` | `uint32_t` indicating variant, numbered in declaration order starting from 1 |
65+
| `enum` and `[Enum] interface` | `RustBuffer` struct pointing to serialized bytes |
6666
| `dictionary` | `RustBuffer` struct pointing to serialized bytes |
6767
| `interface` | `uint64_t` opaque integer handle |
6868

@@ -84,7 +84,7 @@ The details of this format are internal only and may change between versions of
8484
| `T?` | If null, serialized `boolean` false; if non-null, serialized `boolean` true followed by serialized `T` |
8585
| `sequence<T>` | Serialized `i32` item count followed by serialized items; each item is a serialized `T` |
8686
| `record<DOMString, T>` | Serialized `i32` item count followed by serialized items; each item is a serialized `string` followed by a serialized `T` |
87-
| `enum` | Serialized `u32` indicating variant, numbered in declaration order starting from 1 |
87+
| `enum` and `[Enum] interface` | Serialized `i32` indicating variant, numbered in declaration order starting from 1, followed by the serialized values of the variant's fields in declaration order |
8888
| `dictionary` | The serialized value of each field, in declaration order |
8989
| `interface` | *Cannot currently be serialized* |
9090

docs/manual/src/udl/enumerations.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Enumerations
22

33
An enumeration defined in Rust code as
4+
45
```rust
56
enum Animal {
67
Dog,
@@ -17,4 +18,25 @@ enum Animal {
1718
};
1819
```
1920

20-
Note that enumerations with associated data are not yet supported.
21+
Enumerations with associated data require a different syntax,
22+
due to the limitations of using WebIDL as the basis for UniFFI's interface language.
23+
An enum like this in Rust:
24+
25+
```rust
26+
enum IpAddr {
27+
V4 {q1: u8, q2: u8, q3: u8, q4: u8},
28+
V6 {addr: string},
29+
}
30+
```
31+
32+
Can be exposed in the UDL file with:
33+
34+
```idl
35+
[Enum]
36+
interface IpAddr {
37+
V4(u8 q1, u8 q2, u8 q3, u8 q4);
38+
V6(string addr);
39+
};
40+
```
41+
42+
Only enums with named fields are supported by this syntax.

examples/rondpoint/src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ pub enum Enumeration {
3535
Trois,
3636
}
3737

38+
#[derive(Debug, Clone)]
39+
pub enum EnumerationAvecDonnees {
40+
Zero,
41+
Un { premier: u32 },
42+
Deux { premier: u32, second: String },
43+
}
44+
3845
#[allow(non_camel_case_types)]
3946
#[allow(non_snake_case)]
4047
pub struct minusculeMAJUSCULEDict {
@@ -54,7 +61,9 @@ fn copie_enumerations(e: Vec<Enumeration>) -> Vec<Enumeration> {
5461
e
5562
}
5663

57-
fn copie_carte(e: HashMap<String, Enumeration>) -> HashMap<String, Enumeration> {
64+
fn copie_carte(
65+
e: HashMap<String, EnumerationAvecDonnees>,
66+
) -> HashMap<String, EnumerationAvecDonnees> {
5867
e
5968
}
6069

examples/rondpoint/src/rondpoint.udl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ namespace rondpoint {
22
Dictionnaire copie_dictionnaire(Dictionnaire d);
33
Enumeration copie_enumeration(Enumeration e);
44
sequence<Enumeration> copie_enumerations(sequence<Enumeration> e);
5-
record<DOMString, Enumeration> copie_carte(record<DOMString, Enumeration> c);
5+
record<DOMString, EnumerationAvecDonnees> copie_carte(record<DOMString, EnumerationAvecDonnees> c);
66
boolean switcheroo(boolean b);
77
};
88

@@ -20,6 +20,13 @@ enum Enumeration {
2020
"Trois",
2121
};
2222

23+
[Enum]
24+
interface EnumerationAvecDonnees {
25+
Zero();
26+
Un(u32 premier);
27+
Deux(u32 premier, string second);
28+
};
29+
2330
dictionary Dictionnaire {
2431
Enumeration un;
2532
boolean deux;

examples/rondpoint/tests/bindings/test_rondpoint.kts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ assert(dico == copyDico)
66

77
assert(copieEnumeration(Enumeration.DEUX) == Enumeration.DEUX)
88
assert(copieEnumerations(listOf(Enumeration.UN, Enumeration.DEUX)) == listOf(Enumeration.UN, Enumeration.DEUX))
9-
assert(copieCarte(mapOf("1" to Enumeration.UN, "2" to Enumeration.DEUX)) == mapOf("1" to Enumeration.UN, "2" to Enumeration.DEUX))
9+
assert(copieCarte(mapOf(
10+
"0" to EnumerationAvecDonnees.Zero(),
11+
"1" to EnumerationAvecDonnees.Un(1u),
12+
"2" to EnumerationAvecDonnees.Deux(2u, "deux")
13+
)) == mapOf(
14+
"0" to EnumerationAvecDonnees.Zero(),
15+
"1" to EnumerationAvecDonnees.Un(1u),
16+
"2" to EnumerationAvecDonnees.Deux(2u, "deux")
17+
))
1018

1119
assert(switcheroo(false))
1220

examples/rondpoint/tests/bindings/test_rondpoint.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@
88

99
assert copie_enumeration(Enumeration.DEUX) == Enumeration.DEUX
1010
assert copie_enumerations([Enumeration.UN, Enumeration.DEUX]) == [Enumeration.UN, Enumeration.DEUX]
11-
assert copie_carte({"1": Enumeration.UN, "2": Enumeration.DEUX}) == {"1": Enumeration.UN, "2": Enumeration.DEUX}
11+
assert copie_carte({
12+
"0": EnumerationAvecDonnees.ZERO(),
13+
"1": EnumerationAvecDonnees.UN(1),
14+
"2": EnumerationAvecDonnees.DEUX(2, "deux"),
15+
}) == {
16+
"0": EnumerationAvecDonnees.ZERO(),
17+
"1": EnumerationAvecDonnees.UN(1),
18+
"2": EnumerationAvecDonnees.DEUX(2, "deux"),
19+
}
1220

1321
assert switcheroo(False) is True
1422

@@ -19,9 +27,9 @@
1927
rt = Retourneur()
2028

2129
def affirmAllerRetour(vals, identique):
22-
for v in vals:
23-
id_v = identique(v)
24-
assert id_v == v, f"Round-trip failure: {v} => {id_v}"
30+
for v in vals:
31+
id_v = identique(v)
32+
assert id_v == v, f"Round-trip failure: {v} => {id_v}"
2533

2634
MIN_I8 = -1 * 2**7
2735
MAX_I8 = 2**7 - 1
@@ -87,8 +95,8 @@ def affirmAllerRetour(vals, identique):
8795

8896
def affirmEnchaine(vals, toString, rustyStringify=lambda v: str(v).lower()):
8997
for v in vals:
90-
str_v = toString(v)
91-
assert rustyStringify(v) == str_v, f"String compare error {v} => {str_v}"
98+
str_v = toString(v)
99+
assert rustyStringify(v) == str_v, f"String compare error {v} => {str_v}"
92100

93101
# Test the efficacy of the string transport from rust. If this fails, but everything else
94102
# works, then things are very weird.

examples/rondpoint/tests/bindings/test_rondpoint.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ assert(dico == copyDico)
66

77
assert(copieEnumeration(e: .deux) == .deux)
88
assert(copieEnumerations(e: [.un, .deux]) == [.un, .deux])
9-
assert(copieCarte(c: ["1": .un, "2": .deux]) == ["1": .un, "2": .deux])
9+
assert(copieCarte(c:
10+
["0": .zero,
11+
"1": .un(premier: 1),
12+
"2": .deux(premier: 2, second: "deux")
13+
]) == [
14+
"0": .zero,
15+
"1": .un(premier: 1),
16+
"2": .deux(premier: 2, second: "deux")
17+
])
1018

1119
assert(switcheroo(b: false))
1220

uniffi_bindgen/src/bindings/gecko_js/templates/SharedHeaderTemplate.h

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,17 @@ namespace dom {
2929
namespace {{ context.detail_name() }} {
3030

3131
{% for e in ci.iter_enum_definitions() %}
32+
{% if e.has_associated_data() %}
33+
MOZ_STATIC_ASSERT(false, "Sorry the gecko-js backend does not yet support enums with associated data");
34+
{% else %}
3235
template <>
33-
struct ViaFfi<{{ e.name()|class_name_cpp(context) }}, uint32_t> {
34-
[[nodiscard]] static bool Lift(const uint32_t& aLowered, {{ e.name()|class_name_cpp(context) }}& aLifted) {
35-
switch (aLowered) {
36+
struct Serializable<{{ e.name()|class_name_cpp(context) }}> {
37+
[[nodiscard]] static bool ReadFrom(Reader& aReader, {{ e.name()|class_name_cpp(context) }}& aValue) {
38+
auto variant = aReader.ReadInt32();
39+
switch (variant) {
3640
{% for variant in e.variants() -%}
3741
case {{ loop.index }}:
38-
aLifted = {{ e.name()|class_name_cpp(context) }}::{{ variant|enum_variant_cpp }};
42+
aValue = {{ e.name()|class_name_cpp(context) }}::{{ variant.name()|enum_variant_cpp }};
3943
break;
4044
{% endfor -%}
4145
default:
@@ -45,30 +49,18 @@ struct ViaFfi<{{ e.name()|class_name_cpp(context) }}, uint32_t> {
4549
return true;
4650
}
4751

48-
[[nodiscard]] static uint32_t Lower(const {{ e.name()|class_name_cpp(context) }}& aLifted) {
49-
switch (aLifted) {
52+
static void WriteInto(Writer& aWriter, const {{ e.name()|class_name_cpp(context) }}& aValue) {
53+
switch (aValue) {
5054
{% for variant in e.variants() -%}
51-
case {{ e.name()|class_name_cpp(context) }}::{{ variant|enum_variant_cpp }}:
52-
return {{ loop.index }};
55+
case {{ e.name()|class_name_cpp(context) }}::{{ variant.name()|enum_variant_cpp }}:
56+
aWriter.WriteInt32({{ loop.index }});
5357
{% endfor -%}
5458
default:
5559
MOZ_ASSERT(false, "Unknown raw enum value");
5660
}
57-
return 0;
58-
}
59-
};
60-
61-
template <>
62-
struct Serializable<{{ e.name()|class_name_cpp(context) }}> {
63-
[[nodiscard]] static bool ReadFrom(Reader& aReader, {{ e.name()|class_name_cpp(context) }}& aValue) {
64-
auto rawValue = aReader.ReadUInt32();
65-
return ViaFfi<{{ e.name()|class_name_cpp(context) }}, uint32_t>::Lift(rawValue, aValue);
66-
}
67-
68-
static void WriteInto(Writer& aWriter, const {{ e.name()|class_name_cpp(context) }}& aValue) {
69-
aWriter.WriteUInt32(ViaFfi<{{ e.name()|class_name_cpp(context) }}, uint32_t>::Lower(aValue));
7061
}
7162
};
63+
{% endif %}
7264
{% endfor %}
7365

7466
{% for rec in ci.iter_record_definitions() -%}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@ dictionary {{ rec.name()|class_name_webidl(context) }} {
1414
{% endfor %}
1515

1616
{%- for e in ci.iter_enum_definitions() %}
17+
{% if e.has_associated_data() %}
18+
MOZ_STATIC_ASSERT(false, "Sorry the gecko-js backend does not yet support enums with associated data");
19+
{% else %}
1720
enum {{ e.name()|class_name_webidl(context) }} {
1821
{% for variant in e.variants() %}
19-
"{{ variant|enum_variant_webidl }}"{%- if !loop.last %}, {% endif %}
22+
"{{ variant.name()|enum_variant_webidl }}"{%- if !loop.last %}, {% endif %}
2023
{% endfor %}
2124
};
25+
{% endif %}
2226
{% endfor %}
2327

2428
{%- let functions = ci.iter_function_definitions() %}

uniffi_bindgen/src/bindings/gecko_js/webidl.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ impl WebIDLType {
6969
/// the C++ implementation; `false` if by value.
7070
pub fn needs_out_param(&self) -> bool {
7171
match self {
72-
WebIDLType::Flat(Type::String) | WebIDLType::Flat(Type::Record(_)) => true,
72+
WebIDLType::Flat(Type::String) | WebIDLType::Flat(Type::Record(_)) | WebIDLType::Flat(Type::Enum(_)) => true,
7373
WebIDLType::Map(_) | WebIDLType::Sequence(_) => true,
7474
WebIDLType::Optional(inner)
7575
| WebIDLType::OptionalWithDefaultValue(inner)
Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,94 @@
1-
enum class {{ e.name()|class_name_kt }} {
2-
{% for variant in e.variants() %}
3-
{{ variant|enum_variant_kt }}{% if loop.last %};{% else %},{% endif %}
1+
{#
2+
// Kotlin's `enum class` constuct doesn't support variants with associated data,
3+
// but is a little nicer for consumers than its `sealed class` enum pattern.
4+
// So, we switch here, using `enum class` for enums with no associated data
5+
// and `sealed class` for the general case.
6+
#}
7+
8+
{% if e.has_associated_data() %}
9+
10+
sealed class {{ e.name()|class_name_kt }} {
11+
{% for variant in e.variants() -%}
12+
class {{ variant.name()|class_name_kt }}({% if variant.has_fields() %}
13+
{% for field in variant.fields() -%}
14+
val {{ field.name()|var_name_kt }}: {{ field.type_()|type_kt}}{% if loop.last %}{% else %}, {% endif %}
15+
{% endfor -%}
16+
{%- endif %}) : {{ e.name()|class_name_kt }}() {
17+
override fun write(buf: RustBufferBuilder) {
18+
buf.putInt({{ loop.index }})
19+
{% for field in variant.fields() -%}
20+
{{ "(this.{})"|format(field.name())|write_kt("buf", field.type_()) }}
21+
{% endfor -%}
22+
}
23+
override fun equals(other: Any?) : Boolean =
24+
if (other is {{ e.name()|class_name_kt }}.{{ variant.name()|class_name_kt }}) {
25+
{% if variant.has_fields() -%}
26+
{% for field in variant.fields() -%}
27+
{{ field.name()|var_name_kt }} == other.{{ field.name()|var_name_kt }}{% if loop.last %}{% else %} && {% endif -%}
28+
{% endfor -%}
29+
{% else -%}
30+
true
31+
{%- endif %}
32+
} else {
33+
false
34+
}
35+
}
436
{% endfor %}
537

638
companion object {
7-
internal fun lift(n: Int) =
8-
try { values()[n - 1] }
39+
internal fun lift(rbuf: RustBuffer.ByValue): {{ e.name()|class_name_kt }} {
40+
return liftFromRustBuffer(rbuf) { buf -> {{ e.name()|class_name_kt }}.read(buf) }
41+
}
42+
43+
internal fun read(buf: ByteBuffer): {{ e.name()|class_name_kt }} {
44+
return when(buf.getInt()) {
45+
{%- for variant in e.variants() %}
46+
{{ loop.index }} -> {{ e.name()|class_name_kt }}.{{ variant.name()|class_name_kt }}({% if variant.has_fields() %}
47+
{% for field in variant.fields() -%}
48+
{{ "buf"|read_kt(field.type_()) }}{% if loop.last %}{% else %},{% endif %}
49+
{% endfor -%}
50+
{%- endif -%})
51+
{%- endfor %}
52+
else -> throw RuntimeException("invalid enum value, something is very wrong!!")
53+
}
54+
}
55+
}
56+
57+
internal fun lower(): RustBuffer.ByValue {
58+
return lowerIntoRustBuffer(this, {v, buf -> v.write(buf)})
59+
}
60+
61+
internal open fun write(buf: RustBufferBuilder) {
62+
throw RuntimeException("enum variant should have overridden `write` method, something is very wrong!!")
63+
}
64+
}
65+
66+
{% else %}
67+
68+
enum class {{ e.name()|class_name_kt }} {
69+
{% for variant in e.variants() -%}
70+
{{ variant.name()|enum_variant_kt }}{% if loop.last %};{% else %},{% endif %}
71+
{%- endfor %}
72+
73+
companion object {
74+
internal fun lift(rbuf: RustBuffer.ByValue): {{ e.name()|class_name_kt }} {
75+
return liftFromRustBuffer(rbuf) { buf -> {{ e.name()|class_name_kt }}.read(buf) }
76+
}
77+
78+
internal fun read(buf: ByteBuffer) =
79+
try { values()[buf.getInt() - 1] }
980
catch (e: IndexOutOfBoundsException) {
1081
throw RuntimeException("invalid enum value, something is very wrong!!", e)
1182
}
12-
13-
internal fun read(buf: ByteBuffer) = lift(buf.getInt())
1483
}
1584

16-
internal fun lower() = this.ordinal + 1
85+
internal fun lower(): RustBuffer.ByValue {
86+
return lowerIntoRustBuffer(this, {v, buf -> v.write(buf)})
87+
}
1788

18-
internal fun write(buf: RustBufferBuilder) = buf.putInt(this.lower())
89+
internal fun write(buf: RustBufferBuilder) {
90+
buf.putInt(this.ordinal + 1)
91+
}
1992
}
93+
94+
{% endif %}

0 commit comments

Comments
 (0)