Skip to content

Commit

Permalink
Clean up python bindings generator, add support for remaining data ty…
Browse files Browse the repository at this point in the history
…pes.

This is a significant refactor of the python bindings generator, with
the aim of making the generated code a bit cleaner and of better hiding
many of our implementation details from the public API. To help prove
out the approach, it adds previously-missing support for data types like
sequences and maps.

We use the new `TypeUniverse` structure to iterate over all types used
in the interface, and generate various internal helper functions as
methods on our utility classes. This keeps them out of the public API
as seen by consumers.

For example, for each type that lowers into a `RustBuffer`, there is
a corresponding `RustBuffer.allocFrom{{ type_name }}` staticmethod for
lowering it and a `RustBuffer.consumeInto{{ type_name }}` for lifting it.
  • Loading branch information
rfk committed Sep 29, 2020
1 parent 576ec1a commit 4d662d5
Show file tree
Hide file tree
Showing 14 changed files with 696 additions and 246 deletions.
5 changes: 0 additions & 5 deletions examples/rondpoint/tests/bindings/test_rondpoint.kts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,6 @@ fun <T> List<T>.affirmEnchaine(
val wellKnown = st.wellKnownString("kotlin")
assert("uniffi 💚 kotlin!" == wellKnown) { "wellKnownString 'uniffi 💚 kotlin!' == '$wellKnown'" }

// NB. Numbers are all signed in kotlin. This makes roundtripping of unsigned numbers tricky to show.
// Uniffi does not generate unsigned types for kotlin, but the work tracked is
// in https://github.com/mozilla/uniffi-rs/issues/249. Tests using unsigned types are
// commented out for now.

// Booleans
listOf(true, false).affirmEnchaine(st::toStringBoolean)

Expand Down
134 changes: 134 additions & 0 deletions examples/rondpoint/tests/bindings/test_rondpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import sys
import ctypes
from rondpoint import *

dico = Dictionnaire(Enumeration.DEUX, True, 0, 123456789)
copyDico = copie_dictionnaire(dico)
assert dico == copyDico

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 switcheroo(False) is True

# 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}"

MIN_I8 = -1 * 2**7
MAX_I8 = 2**7 - 1
MIN_I16 = -1 * 2**15
MAX_I16 = 2**15 - 1
MIN_I32 = -1 * 2**31
MAX_I32 = 2**31 - 1
MIN_I64 = -1 * 2**31
MAX_I64 = 2**31 - 1

# Python floats are always doubles, so won't round-trip through f32 correctly.
# This truncates them appropriately.
F32_ONE_THIRD = ctypes.c_float(1.0 / 3).value

# Booleans
affirmAllerRetour([True, False], rt.identique_boolean)

# Bytes.
affirmAllerRetour([MIN_I8, -1, 0, 1, MAX_I8], rt.identique_i8)
affirmAllerRetour([0x00, 0x12, 0xFF], rt.identique_u8)

# Shorts
affirmAllerRetour([MIN_I16, -1, 0, 1, MAX_I16], rt.identique_i16)
affirmAllerRetour([0x0000, 0x1234, 0xFFFF], rt.identique_u16)

# Ints
affirmAllerRetour([MIN_I32, -1, 0, 1, MAX_I32], rt.identique_i32)
affirmAllerRetour([0x00000000, 0x12345678, 0xFFFFFFFF], rt.identique_u32)

# Longs
affirmAllerRetour([MIN_I64, -1, 0, 1, MAX_I64], rt.identique_i64)
affirmAllerRetour([0x0000000000000000, 0x1234567890ABCDEF, 0xFFFFFFFFFFFFFFFF], rt.identique_u64)

# Floats
affirmAllerRetour([0.0, 0.5, 0.25, 1.0, F32_ONE_THIRD], rt.identique_float)

# Doubles
affirmAllerRetour(
[0.0, 0.5, 0.25, 1.0, 1.0 / 3, sys.float_info.max, sys.float_info.min],
rt.identique_double
)

# Strings
affirmAllerRetour(
["", "abc", "été", "ښي لاس ته لوستلو لوستل", "😻emoji 👨‍👧‍👦multi-emoji, 🇨🇭a flag, a canal, panama"],
rt.identique_string
)

# Test one way across the FFI.
#
# We send one representation of a value to lib.rs, and it transforms it into another, a string.
# lib.rs sends the string back, and then we compare here in python.
#
# This shows that the values are transformed into strings the same way in both python and rust.
# i.e. if we assume that the string return works (we test this assumption elsewhere)
# we show that lowering from python and lifting into rust has values that both python and rust
# both stringify in the same way. i.e. the same values.
#
# If we roundtripping proves the symmetry of our lowering/lifting from here to rust, and lowering/lifting from rust to here,
# and this convinces us that lowering/lifting from here to rust is correct, then
# together, we've shown the correctness of the return leg.
st = Stringifier()

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}"

# Test the efficacy of the string transport from rust. If this fails, but everything else
# works, then things are very weird.
wellKnown = st.well_known_string("python")
assert "uniffi 💚 python!" == wellKnown

# Booleans
affirmEnchaine([True, False], st.to_string_boolean)

# Bytes.
affirmEnchaine([MIN_I8, -1, 0, 1, MAX_I8], st.to_string_i8)
affirmEnchaine([0x00, 0x12, 0xFF], st.to_string_u8)

# Shorts
affirmEnchaine([MIN_I16, -1, 0, 1, MAX_I16], st.to_string_i16)
affirmEnchaine([0x0000, 0x1234, 0xFFFF], st.to_string_u16)

# Ints
affirmEnchaine([MIN_I32, -1, 0, 1, MAX_I32], st.to_string_i32)
affirmEnchaine([0x00000000, 0x12345678, 0xFFFFFFFF], st.to_string_u32)

# Longs
affirmEnchaine([MIN_I64, -1, 0, 1, MAX_I64], st.to_string_i64)
affirmEnchaine([0x0000000000000000, 0x1234567890ABCDEF, 0xFFFFFFFFFFFFFFFF], st.to_string_u64)

# Floats
def rustyFloatToStr(v):
"""Stringify a float in the same way that rust seems to."""
# Rust doesn't include the decimal part of whole enumber floats when stringifying.
if int(v) == v:
return str(int(v))
return str(v)

affirmEnchaine([0.0, 0.5, 0.25, 1.0], st.to_string_float, rustyFloatToStr)
assert st.to_string_float(F32_ONE_THIRD) == "0.33333334" # annoyingly different string repr

# Doubles
# TODO: float_info.max/float_info.min don't stringify-roundtrip properly yet, TBD.
affirmEnchaine(
[0.0, 0.5, 0.25, 1.0, 1.0 / 3],
st.to_string_double,
rustyFloatToStr,
)
2 changes: 1 addition & 1 deletion examples/rondpoint/tests/test_generated_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ uniffi_macros::build_foreign_language_testcases!(
[
"tests/bindings/test_rondpoint.kts",
"tests/bindings/test_rondpoint.swift",
// "tests/bindings/test_rondpoint.py",
"tests/bindings/test_rondpoint.py",
]
);
105 changes: 35 additions & 70 deletions uniffi_bindgen/src/bindings/python/gen_python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ mod filters {
FFIType::Float64 => "ctypes.c_double".to_string(),
FFIType::RustCString => "ctypes.c_voidp".to_string(),
FFIType::RustBuffer => "RustBuffer".to_string(),
FFIType::RustError => "RustError".to_string(),
FFIType::RustError => "ctypes.POINTER(RustError)".to_string(),
FFIType::ForeignBytes => "ForeignBytes".to_string(),
})
}
Expand Down Expand Up @@ -83,20 +83,16 @@ mod filters {
| Type::Int32
| Type::UInt32
| Type::Int64
| Type::UInt64
| Type::Float32
| Type::Float64
| Type::String
| Type::Boolean
| Type::Object(_)
| Type::Error(_) => format!("{} = {}", nm, nm),
Type::Enum(type_name) => format!("{} = {}({})", nm, type_name, nm),
Type::Record(type_name) => format!("{} = {}._coerce({})", nm, type_name, nm),
| Type::UInt64 => format!("int({})", nm), // TODO: check max/min value
Type::Float32 | Type::Float64 => format!("float({})", nm),
Type::Boolean => format!("bool({})", nm),
Type::String | Type::Object(_) | Type::Error(_) | Type::Record(_) => nm.to_string(),
Type::Enum(name) => format!("{}({})", class_name_py(name)?, nm),
Type::Optional(t) => format!("(None if {} is None else {})", nm, coerce_py(nm, t)?),
Type::Sequence(t) => format!("({} for x in {})", coerce_py(&"x", t)?, nm), // TODO: name hygiene,
Type::Sequence(t) => format!("list({} for x in {})", coerce_py(&"x", t)?, nm),
Type::Map(t) => format!(
"({}:{} for (k, v) in {}.items())",
coerce_py(&"k", t)?,
"dict(({},{}) for (k, v) in {}.items())",
coerce_py(&"k", &Type::String)?,
coerce_py(&"v", t)?,
nm
),
Expand All @@ -106,80 +102,49 @@ mod filters {
pub fn lower_py(nm: &dyn fmt::Display, type_: &Type) -> Result<String, askama::Error> {
Ok(match type_ {
Type::Int8
| Type::Int16
| Type::Int32
| Type::Int64
| Type::UInt8
| Type::Int16
| Type::UInt16
| Type::Int32
| Type::UInt32
| Type::Int64
| Type::UInt64
| Type::Float32
| Type::Float64
| Type::Boolean => nm.to_string(),
Type::Enum(_) => format!("{}.value", nm),
Type::String => format!("{}.encode('utf-8')", nm),
Type::Record(type_name) => format!("{}._lower({})", type_name, nm),
Type::Optional(_type) => format!(
"lowerOptional({}, lambda buf, v: {})",
nm,
lower_into_py(&"buf", &"v", type_)?
| Type::Float64 => nm.to_string(),
Type::Boolean => format!("(1 if {} else 0)", nm),
Type::String => format!("RustBuffer.allocFromString({})", nm),
Type::Enum(_) => format!("({}.value)", nm),
Type::Object(_) => format!("({}._handle)", nm),
Type::Error(_) => panic!("No support for lowering errors, yet"),
Type::Record(_) | Type::Optional(_) | Type::Sequence(_) | Type::Map(_) => format!(
"RustBuffer.allocFrom{}({})",
class_name_py(&type_.canonical_name())?,
nm
),
_ => panic!("[TODO: lower_py({:?})]", type_),
})
}

pub fn lower_into_py(
nm: &dyn fmt::Display,
target: &dyn fmt::Display,
type_: &Type,
) -> Result<String, askama::Error> {
let nm = var_name_py(nm)?;
Ok(match type_ {
Type::Float64 => format!("{}.putDouble({})", target, nm),
Type::UInt32 => format!("{}.putInt({})", target, nm),
Type::String => format!("{}.putString({})", target, nm),
Type::Record(type_name) => format!("{}._lowerInto({}, {})", type_name, nm, target),
_ => panic!("[TODO: lower_into_py({:?})]", type_),
})
}

pub fn lift_py(nm: &dyn fmt::Display, type_: &Type) -> Result<String, askama::Error> {
Ok(match type_ {
Type::Int8
| Type::Int16
| Type::Int32
| Type::Int64
| Type::UInt8
| Type::Int16
| Type::UInt16
| Type::Int32
| Type::UInt32
| Type::UInt64
| Type::Float32
| Type::Float64
| Type::Boolean => format!("{}", nm),
Type::Enum(type_name) => format!("{}({})", type_name, nm),
Type::String => format!("liftString({})", nm),
Type::Record(type_name) => format!("{}._lift({})", type_name, nm),
Type::Optional(type_) => format!(
"liftOptional({}, lambda buf: {})",
nm,
lift_from_py(&"buf", type_)?
),
Type::Sequence(type_) => format!(
"liftSequence({}, lambda buf: {})",
| Type::Int64
| Type::UInt64 => format!("int({})", nm),
Type::Float32 | Type::Float64 => format!("float({})", nm),
Type::Boolean => format!("(True if {} else False)", nm),
Type::String => format!("{}.consumeIntoString()", nm),
Type::Enum(name) => format!("{}({})", class_name_py(name)?, nm),
Type::Object(_) => panic!("No support for lifting objects, yet"),
Type::Error(_) => panic!("No support for lowering errors, yet"),
Type::Record(_) | Type::Optional(_) | Type::Sequence(_) | Type::Map(_) => format!(
"{}.consumeInto{}()",
nm,
lift_from_py(&"buf", type_)?
class_name_py(&type_.canonical_name())?
),
_ => panic!("[TODO: lift_py({:?})]", type_),
})
}

pub fn lift_from_py(nm: &dyn fmt::Display, type_: &Type) -> Result<String, askama::Error> {
Ok(match type_ {
Type::UInt32 => format!("{}.getInt()", nm),
Type::Float64 => format!("{}.getDouble()", nm),
Type::Record(type_name) => format!("{}._liftFrom({})", type_name, nm),
Type::String => format!("{}.getString()", nm),
_ => panic!("[TODO: lift_from_py({:?})]", type_),
})
}
}
4 changes: 2 additions & 2 deletions uniffi_bindgen/src/bindings/python/templates/EnumTemplate.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class {{ e.name() }}(enum.Enum):
class {{ e.name()|class_name_py }}(enum.Enum):
{% for variant in e.variants() -%}
{{ variant|enum_name_py }} = {{ loop.index }}
{% endfor -%}
{% endfor %}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def {{ meth.name()|fn_name_py }}(self, {% call py::arg_list_decl(meth) %}):
{%- call py::coerce_args_extra_indent(meth) %}
_retval = {% call py::to_ffi_call_with_prefix("self._handle", meth) %}
return {{ "_retval"|lift_py(return_type) }}

{%- when None -%}
def {{ meth.name()|fn_name_py }}(self, {% call py::arg_list_decl(meth) %}):
{%- call py::coerce_args_extra_indent(meth) %}
Expand Down
38 changes: 2 additions & 36 deletions uniffi_bindgen/src/bindings/python/templates/RecordTemplate.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,15 @@
class {{ rec.name() }}(object):
class {{ rec.name()|class_name_py }}(object):
def __init__(self,{% for field in rec.fields() %}{{ field.name()|var_name_py }}{% if loop.last %}{% else %}, {% endif %}{% endfor %}):
{%- for field in rec.fields() %}
self.{{ field.name()|var_name_py }} = {{ field.name()|var_name_py }}
{%- endfor %}

def __str__(self):
return "{{ rec.name() }}({% for field in rec.fields() %}{{ field.name() }}={}{% if loop.last %}{% else %}, {% endif %}{% endfor %})".format({% for field in rec.fields() %}self.{{ field.name() }}{% if loop.last %}{% else %}, {% endif %}{% endfor %})
return "{{ rec.name()|class_name_py }}({% for field in rec.fields() %}{{ field.name() }}={}{% if loop.last %}{% else %}, {% endif %}{% endfor %})".format({% for field in rec.fields() %}self.{{ field.name() }}{% if loop.last %}{% else %}, {% endif %}{% endfor %})

def __eq__(self, other):
{%- for field in rec.fields() %}
if self.{{ field.name()|var_name_py }} != other.{{ field.name()|var_name_py }}:
return False
return True
{%- endfor %}

@classmethod
def _coerce(cls, v):
# TODO: maybe we could do a bit of duck-typing here, details TBD
assert isinstance(v, {{ rec.name() }})
return v

@classmethod
def _lift(cls, rbuf):
return cls._liftFrom(RustBufferStream(rbuf))

@classmethod
def _liftFrom(cls, buf):
return cls(
{%- for field in rec.fields() %}
{{ "buf"|lift_from_py(field.type_()) }}{% if loop.last %}{% else %},{% endif %}
{%- endfor %}
)

@classmethod
def _lower(cls, v):
buf = RustBufferBuilder()
try:
cls._lowerInto(v, buf)
return buf.finalize()
except Exception:
buf.discard()
raise

@classmethod
def _lowerInto(cls, v, buf):
{%- for field in rec.fields() %}
{{ "(v.{})"|format(field.name())|lower_into_py("buf", field.type_()) }}
{%- endfor %}
Loading

0 comments on commit 4d662d5

Please sign in to comment.