Skip to content

Commit f7c8ccd

Browse files
authored
Add support for enums with associated data. (#381)
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 9590a36 commit f7c8ccd

31 files changed

+919
-166
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: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,24 @@ 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+
))
18+
19+
val var1: EnumerationAvecDonnees = EnumerationAvecDonnees.Zero
20+
val var2: EnumerationAvecDonnees = EnumerationAvecDonnees.Un(1u)
21+
val var3: EnumerationAvecDonnees = EnumerationAvecDonnees.Un(2u)
22+
assert(var1 != var2)
23+
assert(var2 != var3)
24+
assert(var1 == EnumerationAvecDonnees.Zero)
25+
assert(var1 != EnumerationAvecDonnees.Un(1u))
26+
assert(var2 == EnumerationAvecDonnees.Un(1u))
1027

1128
assert(switcheroo(false))
1229

examples/rondpoint/tests/bindings/test_rondpoint.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,32 @@
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

23+
assert EnumerationAvecDonnees.ZERO() != EnumerationAvecDonnees.UN(1)
24+
assert EnumerationAvecDonnees.UN(1) == EnumerationAvecDonnees.UN(1)
25+
assert EnumerationAvecDonnees.UN(1) != EnumerationAvecDonnees.UN(2)
26+
1527
# Test the roundtrip across the FFI.
1628
# This shows that the values we send come back in exactly the same state as we sent them.
1729
# i.e. it shows that lowering from python and lifting into rust is symmetrical with
1830
# lowering from rust and lifting into python.
1931
rt = Retourneur()
2032

2133
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}"
34+
for v in vals:
35+
id_v = identique(v)
36+
assert id_v == v, f"Round-trip failure: {v} => {id_v}"
2537

2638
MIN_I8 = -1 * 2**7
2739
MAX_I8 = 2**7 - 1
@@ -87,8 +99,8 @@ def affirmAllerRetour(vals, identique):
8799

88100
def affirmEnchaine(vals, toString, rustyStringify=lambda v: str(v).lower()):
89101
for v in vals:
90-
str_v = toString(v)
91-
assert rustyStringify(v) == str_v, f"String compare error {v} => {str_v}"
102+
str_v = toString(v)
103+
assert rustyStringify(v) == str_v, f"String compare error {v} => {str_v}"
92104

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

examples/rondpoint/tests/bindings/test_rondpoint.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,20 @@ 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+
])
18+
19+
assert(EnumerationAvecDonnees.zero != EnumerationAvecDonnees.un(premier: 1))
20+
assert(EnumerationAvecDonnees.un(premier: 1) == EnumerationAvecDonnees.un(premier: 1))
21+
assert(EnumerationAvecDonnees.un(premier: 1) != EnumerationAvecDonnees.un(premier: 2))
22+
1023

1124
assert(switcheroo(b: false))
1225

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.is_flat() %}
33+
MOZ_STATIC_ASSERT(false, "Sorry the gecko-js backend does not yet support enums with associated data: {{ e.name() }}");
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,9 +14,13 @@ dictionary {{ rec.name()|class_name_webidl(context) }} {
1414
{% endfor %}
1515

1616
{%- for e in ci.iter_enum_definitions() %}
17+
{% if ! e.is_flat() %}
18+
// Sorry the gecko-js backend does not yet support enums with associated data,
19+
// so this probably isn't going to compile just yet...
20+
{% endif %}
1721
enum {{ e.name()|class_name_webidl(context) }} {
1822
{% for variant in e.variants() %}
19-
"{{ variant|enum_variant_webidl }}"{%- if !loop.last %}, {% endif %}
23+
"{{ variant.name()|enum_variant_webidl }}"{%- if !loop.last %}, {% endif %}
2024
{% endfor %}
2125
};
2226
{% endfor %}
Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,88 @@
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.is_flat() %}
9+
110
enum class {{ e.name()|class_name_kt }} {
2-
{% for variant in e.variants() %}
3-
{{ variant|enum_variant_kt }}{% if loop.last %};{% else %},{% endif %}
4-
{% endfor %}
11+
{% for variant in e.variants() -%}
12+
{{ variant.name()|enum_variant_kt }}{% if loop.last %};{% else %},{% endif %}
13+
{%- endfor %}
514

615
companion object {
7-
internal fun lift(n: Int) =
8-
try { values()[n - 1] }
16+
internal fun lift(rbuf: RustBuffer.ByValue): {{ e.name()|class_name_kt }} {
17+
return liftFromRustBuffer(rbuf) { buf -> {{ e.name()|class_name_kt }}.read(buf) }
18+
}
19+
20+
internal fun read(buf: ByteBuffer) =
21+
try { values()[buf.getInt() - 1] }
922
catch (e: IndexOutOfBoundsException) {
1023
throw RuntimeException("invalid enum value, something is very wrong!!", e)
1124
}
25+
}
26+
27+
internal fun lower(): RustBuffer.ByValue {
28+
return lowerIntoRustBuffer(this, {v, buf -> v.write(buf)})
29+
}
30+
31+
internal fun write(buf: RustBufferBuilder) {
32+
buf.putInt(this.ordinal + 1)
33+
}
34+
}
1235

13-
internal fun read(buf: ByteBuffer) = lift(buf.getInt())
36+
{% else %}
37+
38+
sealed class {{ e.name()|class_name_kt }} {
39+
{% for variant in e.variants() -%}
40+
{% if !variant.has_fields() -%}
41+
object {{ variant.name()|class_name_kt }} : {{ e.name()|class_name_kt }}()
42+
{% else -%}
43+
data class {{ variant.name()|class_name_kt }}(
44+
{% for field in variant.fields() -%}
45+
val {{ field.name()|var_name_kt }}: {{ field.type_()|type_kt}}{% if loop.last %}{% else %}, {% endif %}
46+
{% endfor -%}
47+
) : {{ e.name()|class_name_kt }}()
48+
{%- endif %}
49+
{% endfor %}
50+
51+
companion object {
52+
internal fun lift(rbuf: RustBuffer.ByValue): {{ e.name()|class_name_kt }} {
53+
return liftFromRustBuffer(rbuf) { buf -> {{ e.name()|class_name_kt }}.read(buf) }
54+
}
55+
56+
internal fun read(buf: ByteBuffer): {{ e.name()|class_name_kt }} {
57+
return when(buf.getInt()) {
58+
{%- for variant in e.variants() %}
59+
{{ loop.index }} -> {{ e.name()|class_name_kt }}.{{ variant.name()|class_name_kt }}{% if variant.has_fields() %}(
60+
{% for field in variant.fields() -%}
61+
{{ "buf"|read_kt(field.type_()) }}{% if loop.last %}{% else %},{% endif %}
62+
{% endfor -%}
63+
){%- endif -%}
64+
{%- endfor %}
65+
else -> throw RuntimeException("invalid enum value, something is very wrong!!")
66+
}
67+
}
1468
}
1569

16-
internal fun lower() = this.ordinal + 1
70+
internal fun lower(): RustBuffer.ByValue {
71+
return lowerIntoRustBuffer(this, {v, buf -> v.write(buf)})
72+
}
1773

18-
internal fun write(buf: RustBufferBuilder) = buf.putInt(this.lower())
74+
internal fun write(buf: RustBufferBuilder) {
75+
when(this) {
76+
{%- for variant in e.variants() %}
77+
is {{ e.name()|class_name_kt }}.{{ variant.name()|class_name_kt }} -> {
78+
buf.putInt({{ loop.index }})
79+
{% for field in variant.fields() -%}
80+
{{ "(this.{})"|format(field.name())|write_kt("buf", field.type_()) }}
81+
{% endfor -%}
82+
}
83+
{%- endfor %}
84+
}.let { /* this makes the `when` an expression, which ensures it is exhaustive */ }
85+
}
1986
}
87+
88+
{% endif %}

0 commit comments

Comments
 (0)