Skip to content

Commit

Permalink
Add support for enums with associated data. (#381)
Browse files Browse the repository at this point in the history
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, ...);
}
```
  • Loading branch information
rfk authored Feb 22, 2021
1 parent 9590a36 commit f7c8ccd
Show file tree
Hide file tree
Showing 31 changed files with 919 additions and 166 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Things that are implemented so far:

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

Things that are not implemented yet:

* Enums with associated data.
* Union types.
* Efficient access to binary data (like Rust's `Vec<u8>`).
* Passing object references to functions or methods.
Expand Down
4 changes: 2 additions & 2 deletions docs/manual/src/internals/lifting_and_lowering.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Calling this function from foreign language code involves the following steps:
| `T?` | `RustBuffer` struct pointing to serialized bytes |
| `sequence<T>` | `RustBuffer` struct pointing to serialized bytes |
| `record<DOMString, T>` | `RustBuffer` struct pointing to serialized bytes |
| `enum` | `uint32_t` indicating variant, numbered in declaration order starting from 1 |
| `enum` and `[Enum] interface` | `RustBuffer` struct pointing to serialized bytes |
| `dictionary` | `RustBuffer` struct pointing to serialized bytes |
| `interface` | `uint64_t` opaque integer handle |

Expand All @@ -84,7 +84,7 @@ The details of this format are internal only and may change between versions of
| `T?` | If null, serialized `boolean` false; if non-null, serialized `boolean` true followed by serialized `T` |
| `sequence<T>` | Serialized `i32` item count followed by serialized items; each item is a serialized `T` |
| `record<DOMString, T>` | Serialized `i32` item count followed by serialized items; each item is a serialized `string` followed by a serialized `T` |
| `enum` | Serialized `u32` indicating variant, numbered in declaration order starting from 1 |
| `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 |
| `dictionary` | The serialized value of each field, in declaration order |
| `interface` | *Cannot currently be serialized* |

Expand Down
24 changes: 23 additions & 1 deletion docs/manual/src/udl/enumerations.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Enumerations

An enumeration defined in Rust code as

```rust
enum Animal {
Dog,
Expand All @@ -17,4 +18,25 @@ enum Animal {
};
```

Note that enumerations with associated data are not yet supported.
Enumerations with associated data require a different syntax,
due to the limitations of using WebIDL as the basis for UniFFI's interface language.
An enum like this in Rust:

```rust
enum IpAddr {
V4 {q1: u8, q2: u8, q3: u8, q4: u8},
V6 {addr: string},
}
```

Can be exposed in the UDL file with:

```idl
[Enum]
interface IpAddr {
V4(u8 q1, u8 q2, u8 q3, u8 q4);
V6(string addr);
};
```

Only enums with named fields are supported by this syntax.
11 changes: 10 additions & 1 deletion examples/rondpoint/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ pub enum Enumeration {
Trois,
}

#[derive(Debug, Clone)]
pub enum EnumerationAvecDonnees {
Zero,
Un { premier: u32 },
Deux { premier: u32, second: String },
}

#[allow(non_camel_case_types)]
#[allow(non_snake_case)]
pub struct minusculeMAJUSCULEDict {
Expand All @@ -54,7 +61,9 @@ fn copie_enumerations(e: Vec<Enumeration>) -> Vec<Enumeration> {
e
}

fn copie_carte(e: HashMap<String, Enumeration>) -> HashMap<String, Enumeration> {
fn copie_carte(
e: HashMap<String, EnumerationAvecDonnees>,
) -> HashMap<String, EnumerationAvecDonnees> {
e
}

Expand Down
9 changes: 8 additions & 1 deletion examples/rondpoint/src/rondpoint.udl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ namespace rondpoint {
Dictionnaire copie_dictionnaire(Dictionnaire d);
Enumeration copie_enumeration(Enumeration e);
sequence<Enumeration> copie_enumerations(sequence<Enumeration> e);
record<DOMString, Enumeration> copie_carte(record<DOMString, Enumeration> c);
record<DOMString, EnumerationAvecDonnees> copie_carte(record<DOMString, EnumerationAvecDonnees> c);
boolean switcheroo(boolean b);
};

Expand All @@ -20,6 +20,13 @@ enum Enumeration {
"Trois",
};

[Enum]
interface EnumerationAvecDonnees {
Zero();
Un(u32 premier);
Deux(u32 premier, string second);
};

dictionary Dictionnaire {
Enumeration un;
boolean deux;
Expand Down
19 changes: 18 additions & 1 deletion examples/rondpoint/tests/bindings/test_rondpoint.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,24 @@ assert(dico == copyDico)

assert(copieEnumeration(Enumeration.DEUX) == Enumeration.DEUX)
assert(copieEnumerations(listOf(Enumeration.UN, Enumeration.DEUX)) == listOf(Enumeration.UN, Enumeration.DEUX))
assert(copieCarte(mapOf("1" to Enumeration.UN, "2" to Enumeration.DEUX)) == mapOf("1" to Enumeration.UN, "2" to Enumeration.DEUX))
assert(copieCarte(mapOf(
"0" to EnumerationAvecDonnees.Zero,
"1" to EnumerationAvecDonnees.Un(1u),
"2" to EnumerationAvecDonnees.Deux(2u, "deux")
)) == mapOf(
"0" to EnumerationAvecDonnees.Zero,
"1" to EnumerationAvecDonnees.Un(1u),
"2" to EnumerationAvecDonnees.Deux(2u, "deux")
))

val var1: EnumerationAvecDonnees = EnumerationAvecDonnees.Zero
val var2: EnumerationAvecDonnees = EnumerationAvecDonnees.Un(1u)
val var3: EnumerationAvecDonnees = EnumerationAvecDonnees.Un(2u)
assert(var1 != var2)
assert(var2 != var3)
assert(var1 == EnumerationAvecDonnees.Zero)
assert(var1 != EnumerationAvecDonnees.Un(1u))
assert(var2 == EnumerationAvecDonnees.Un(1u))

assert(switcheroo(false))

Expand Down
24 changes: 18 additions & 6 deletions examples/rondpoint/tests/bindings/test_rondpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,32 @@

assert copie_enumeration(Enumeration.DEUX) == Enumeration.DEUX
assert copie_enumerations([Enumeration.UN, Enumeration.DEUX]) == [Enumeration.UN, Enumeration.DEUX]
assert copie_carte({"1": Enumeration.UN, "2": Enumeration.DEUX}) == {"1": Enumeration.UN, "2": Enumeration.DEUX}
assert copie_carte({
"0": EnumerationAvecDonnees.ZERO(),
"1": EnumerationAvecDonnees.UN(1),
"2": EnumerationAvecDonnees.DEUX(2, "deux"),
}) == {
"0": EnumerationAvecDonnees.ZERO(),
"1": EnumerationAvecDonnees.UN(1),
"2": EnumerationAvecDonnees.DEUX(2, "deux"),
}

assert switcheroo(False) is True

assert EnumerationAvecDonnees.ZERO() != EnumerationAvecDonnees.UN(1)
assert EnumerationAvecDonnees.UN(1) == EnumerationAvecDonnees.UN(1)
assert EnumerationAvecDonnees.UN(1) != EnumerationAvecDonnees.UN(2)

# Test the roundtrip across the FFI.
# This shows that the values we send come back in exactly the same state as we sent them.
# i.e. it shows that lowering from python and lifting into rust is symmetrical with
# lowering from rust and lifting into python.
rt = Retourneur()

def affirmAllerRetour(vals, identique):
for v in vals:
id_v = identique(v)
assert id_v == v, f"Round-trip failure: {v} => {id_v}"
for v in vals:
id_v = identique(v)
assert id_v == v, f"Round-trip failure: {v} => {id_v}"

MIN_I8 = -1 * 2**7
MAX_I8 = 2**7 - 1
Expand Down Expand Up @@ -87,8 +99,8 @@ def affirmAllerRetour(vals, identique):

def affirmEnchaine(vals, toString, rustyStringify=lambda v: str(v).lower()):
for v in vals:
str_v = toString(v)
assert rustyStringify(v) == str_v, f"String compare error {v} => {str_v}"
str_v = toString(v)
assert rustyStringify(v) == str_v, f"String compare error {v} => {str_v}"

# Test the efficacy of the string transport from rust. If this fails, but everything else
# works, then things are very weird.
Expand Down
15 changes: 14 additions & 1 deletion examples/rondpoint/tests/bindings/test_rondpoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,20 @@ assert(dico == copyDico)

assert(copieEnumeration(e: .deux) == .deux)
assert(copieEnumerations(e: [.un, .deux]) == [.un, .deux])
assert(copieCarte(c: ["1": .un, "2": .deux]) == ["1": .un, "2": .deux])
assert(copieCarte(c:
["0": .zero,
"1": .un(premier: 1),
"2": .deux(premier: 2, second: "deux")
]) == [
"0": .zero,
"1": .un(premier: 1),
"2": .deux(premier: 2, second: "deux")
])

assert(EnumerationAvecDonnees.zero != EnumerationAvecDonnees.un(premier: 1))
assert(EnumerationAvecDonnees.un(premier: 1) == EnumerationAvecDonnees.un(premier: 1))
assert(EnumerationAvecDonnees.un(premier: 1) != EnumerationAvecDonnees.un(premier: 2))


assert(switcheroo(b: false))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,17 @@ namespace dom {
namespace {{ context.detail_name() }} {

{% for e in ci.iter_enum_definitions() %}
{% if !e.is_flat() %}
MOZ_STATIC_ASSERT(false, "Sorry the gecko-js backend does not yet support enums with associated data: {{ e.name() }}");
{% else %}
template <>
struct ViaFfi<{{ e.name()|class_name_cpp(context) }}, uint32_t> {
[[nodiscard]] static bool Lift(const uint32_t& aLowered, {{ e.name()|class_name_cpp(context) }}& aLifted) {
switch (aLowered) {
struct Serializable<{{ e.name()|class_name_cpp(context) }}> {
[[nodiscard]] static bool ReadFrom(Reader& aReader, {{ e.name()|class_name_cpp(context) }}& aValue) {
auto variant = aReader.ReadInt32();
switch (variant) {
{% for variant in e.variants() -%}
case {{ loop.index }}:
aLifted = {{ e.name()|class_name_cpp(context) }}::{{ variant|enum_variant_cpp }};
aValue = {{ e.name()|class_name_cpp(context) }}::{{ variant.name()|enum_variant_cpp }};
break;
{% endfor -%}
default:
Expand All @@ -45,30 +49,18 @@ struct ViaFfi<{{ e.name()|class_name_cpp(context) }}, uint32_t> {
return true;
}

[[nodiscard]] static uint32_t Lower(const {{ e.name()|class_name_cpp(context) }}& aLifted) {
switch (aLifted) {
static void WriteInto(Writer& aWriter, const {{ e.name()|class_name_cpp(context) }}& aValue) {
switch (aValue) {
{% for variant in e.variants() -%}
case {{ e.name()|class_name_cpp(context) }}::{{ variant|enum_variant_cpp }}:
return {{ loop.index }};
case {{ e.name()|class_name_cpp(context) }}::{{ variant.name()|enum_variant_cpp }}:
aWriter.WriteInt32({{ loop.index }});
{% endfor -%}
default:
MOZ_ASSERT(false, "Unknown raw enum value");
}
return 0;
}
};

template <>
struct Serializable<{{ e.name()|class_name_cpp(context) }}> {
[[nodiscard]] static bool ReadFrom(Reader& aReader, {{ e.name()|class_name_cpp(context) }}& aValue) {
auto rawValue = aReader.ReadUInt32();
return ViaFfi<{{ e.name()|class_name_cpp(context) }}, uint32_t>::Lift(rawValue, aValue);
}

static void WriteInto(Writer& aWriter, const {{ e.name()|class_name_cpp(context) }}& aValue) {
aWriter.WriteUInt32(ViaFfi<{{ e.name()|class_name_cpp(context) }}, uint32_t>::Lower(aValue));
}
};
{% endif %}
{% endfor %}

{% for rec in ci.iter_record_definitions() -%}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ dictionary {{ rec.name()|class_name_webidl(context) }} {
{% endfor %}

{%- for e in ci.iter_enum_definitions() %}
{% if ! e.is_flat() %}
// Sorry the gecko-js backend does not yet support enums with associated data,
// so this probably isn't going to compile just yet...
{% endif %}
enum {{ e.name()|class_name_webidl(context) }} {
{% for variant in e.variants() %}
"{{ variant|enum_variant_webidl }}"{%- if !loop.last %}, {% endif %}
"{{ variant.name()|enum_variant_webidl }}"{%- if !loop.last %}, {% endif %}
{% endfor %}
};
{% endfor %}
Expand Down
85 changes: 77 additions & 8 deletions uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,88 @@
{#
// Kotlin's `enum class` constuct doesn't support variants with associated data,
// but is a little nicer for consumers than its `sealed class` enum pattern.
// So, we switch here, using `enum class` for enums with no associated data
// and `sealed class` for the general case.
#}

{% if e.is_flat() %}

enum class {{ e.name()|class_name_kt }} {
{% for variant in e.variants() %}
{{ variant|enum_variant_kt }}{% if loop.last %};{% else %},{% endif %}
{% endfor %}
{% for variant in e.variants() -%}
{{ variant.name()|enum_variant_kt }}{% if loop.last %};{% else %},{% endif %}
{%- endfor %}

companion object {
internal fun lift(n: Int) =
try { values()[n - 1] }
internal fun lift(rbuf: RustBuffer.ByValue): {{ e.name()|class_name_kt }} {
return liftFromRustBuffer(rbuf) { buf -> {{ e.name()|class_name_kt }}.read(buf) }
}

internal fun read(buf: ByteBuffer) =
try { values()[buf.getInt() - 1] }
catch (e: IndexOutOfBoundsException) {
throw RuntimeException("invalid enum value, something is very wrong!!", e)
}
}

internal fun lower(): RustBuffer.ByValue {
return lowerIntoRustBuffer(this, {v, buf -> v.write(buf)})
}

internal fun write(buf: RustBufferBuilder) {
buf.putInt(this.ordinal + 1)
}
}

internal fun read(buf: ByteBuffer) = lift(buf.getInt())
{% else %}

sealed class {{ e.name()|class_name_kt }} {
{% for variant in e.variants() -%}
{% if !variant.has_fields() -%}
object {{ variant.name()|class_name_kt }} : {{ e.name()|class_name_kt }}()
{% else -%}
data class {{ variant.name()|class_name_kt }}(
{% for field in variant.fields() -%}
val {{ field.name()|var_name_kt }}: {{ field.type_()|type_kt}}{% if loop.last %}{% else %}, {% endif %}
{% endfor -%}
) : {{ e.name()|class_name_kt }}()
{%- endif %}
{% endfor %}

companion object {
internal fun lift(rbuf: RustBuffer.ByValue): {{ e.name()|class_name_kt }} {
return liftFromRustBuffer(rbuf) { buf -> {{ e.name()|class_name_kt }}.read(buf) }
}

internal fun read(buf: ByteBuffer): {{ e.name()|class_name_kt }} {
return when(buf.getInt()) {
{%- for variant in e.variants() %}
{{ loop.index }} -> {{ e.name()|class_name_kt }}.{{ variant.name()|class_name_kt }}{% if variant.has_fields() %}(
{% for field in variant.fields() -%}
{{ "buf"|read_kt(field.type_()) }}{% if loop.last %}{% else %},{% endif %}
{% endfor -%}
){%- endif -%}
{%- endfor %}
else -> throw RuntimeException("invalid enum value, something is very wrong!!")
}
}
}

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

internal fun write(buf: RustBufferBuilder) = buf.putInt(this.lower())
internal fun write(buf: RustBufferBuilder) {
when(this) {
{%- for variant in e.variants() %}
is {{ e.name()|class_name_kt }}.{{ variant.name()|class_name_kt }} -> {
buf.putInt({{ loop.index }})
{% for field in variant.fields() -%}
{{ "(this.{})"|format(field.name())|write_kt("buf", field.type_()) }}
{% endfor -%}
}
{%- endfor %}
}.let { /* this makes the `when` an expression, which ensures it is exhaustive */ }
}
}

{% endif %}
Loading

0 comments on commit f7c8ccd

Please sign in to comment.