Skip to content

Commit

Permalink
Add support for enums with associated data.
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 committed Feb 18, 2021
1 parent 4d61601 commit 61ccf63
Show file tree
Hide file tree
Showing 30 changed files with 873 additions and 158 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
10 changes: 9 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,15 @@ 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")
))

assert(switcheroo(false))

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

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

Expand All @@ -19,9 +27,9 @@
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 +95,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
10 changes: 9 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,15 @@ 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(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.has_associated_data() %}
MOZ_STATIC_ASSERT(false, "Sorry the gecko-js backend does not yet support enums with associated data");
{% 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,11 +14,15 @@ dictionary {{ rec.name()|class_name_webidl(context) }} {
{% endfor %}

{%- for e in ci.iter_enum_definitions() %}
{% if e.has_associated_data() %}
MOZ_STATIC_ASSERT(false, "Sorry the gecko-js backend does not yet support enums with associated data");
{% else %}
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 %}
};
{% endif %}
{% endfor %}

{%- let functions = ci.iter_function_definitions() %}
Expand Down
4 changes: 3 additions & 1 deletion uniffi_bindgen/src/bindings/gecko_js/webidl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ impl WebIDLType {
/// the C++ implementation; `false` if by value.
pub fn needs_out_param(&self) -> bool {
match self {
WebIDLType::Flat(Type::String) | WebIDLType::Flat(Type::Record(_)) => true,
WebIDLType::Flat(Type::String)
| WebIDLType::Flat(Type::Record(_))
| WebIDLType::Flat(Type::Enum(_)) => true,
WebIDLType::Map(_) | WebIDLType::Sequence(_) => true,
WebIDLType::Optional(inner)
| WebIDLType::OptionalWithDefaultValue(inner)
Expand Down
93 changes: 84 additions & 9 deletions uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,94 @@
enum class {{ e.name()|class_name_kt }} {
{% for variant in e.variants() %}
{{ variant|enum_variant_kt }}{% if loop.last %};{% else %},{% endif %}
{#
// 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.has_associated_data() %}

sealed class {{ e.name()|class_name_kt }} {
{% for variant in e.variants() -%}
class {{ variant.name()|class_name_kt }}({% if variant.has_fields() %}
{% for field in variant.fields() -%}
val {{ field.name()|var_name_kt }}: {{ field.type_()|type_kt}}{% if loop.last %}{% else %}, {% endif %}
{% endfor -%}
{%- endif %}) : {{ e.name()|class_name_kt }}() {
override fun write(buf: RustBufferBuilder) {
buf.putInt({{ loop.index }})
{% for field in variant.fields() -%}
{{ "(this.{})"|format(field.name())|write_kt("buf", field.type_()) }}
{% endfor -%}
}
override fun equals(other: Any?) : Boolean =
if (other is {{ e.name()|class_name_kt }}.{{ variant.name()|class_name_kt }}) {
{% if variant.has_fields() -%}
{% for field in variant.fields() -%}
{{ field.name()|var_name_kt }} == other.{{ field.name()|var_name_kt }}{% if loop.last %}{% else %} && {% endif -%}
{% endfor -%}
{% else -%}
true
{%- endif %}
} else {
false
}
}
{% 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): {{ 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(): RustBuffer.ByValue {
return lowerIntoRustBuffer(this, {v, buf -> v.write(buf)})
}

internal open fun write(buf: RustBufferBuilder) {
throw RuntimeException("enum variant should have overridden `write` method, something is very wrong!!")
}
}

{% else %}

enum class {{ e.name()|class_name_kt }} {
{% for variant in e.variants() -%}
{{ variant.name()|enum_variant_kt }}{% if loop.last %};{% else %},{% 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) =
try { values()[buf.getInt() - 1] }
catch (e: IndexOutOfBoundsException) {
throw RuntimeException("invalid enum value, something is very wrong!!", e)
}

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

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) {
buf.putInt(this.ordinal + 1)
}
}

{% endif %}
Loading

0 comments on commit 61ccf63

Please sign in to comment.