Skip to content

Commit

Permalink
Clean up python bindings generator, and add support for sequences.
Browse files Browse the repository at this point in the history
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 sequence types.

The key idea here is that, for any given `ComponentInterface`, we can
finitely enumerate all the possible types used in that interface
(including recursive types like sequences) and can give each such type
a unique name. This lets us define per-type helper methods on the internal
helper objects like `RustBuffer`, rather than putting these as "hidden"
methods on the public classes.

For example, for each type that lowers into a `RustBuffer`, there is
a corresponding `RustBuffer.allocFrom{{ type_name }}` classmethod for
lowering it and a `RustBuffer.consumeInto{{ type_name }}` for lifting it.

If you squint, this is a little bit like defining private traits and
implementing them for each type, except within the constraints of python's
much looser type system.
  • Loading branch information
rfk committed Aug 12, 2020
1 parent 04e7bf6 commit d054e7a
Show file tree
Hide file tree
Showing 11 changed files with 604 additions and 172 deletions.
10 changes: 10 additions & 0 deletions examples/rondpoint/tests/bindings/test_rondpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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 switcheroo(False) is True
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",
]
);
130 changes: 59 additions & 71 deletions uniffi_bindgen/src/bindings/python/gen_python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ mod filters {
FFIType::Float32 => "ctypes.c_float".to_string(),
FFIType::Float64 => "ctypes.c_double".to_string(),
FFIType::RustBuffer => "RustBuffer".to_string(),
FFIType::RustError => "RustError".to_string(),
FFIType::RustError => "POINTER(RustError)".to_string(),
// We use a c_void_p instead of a c_char_p since python seems to
// create it's own string if we use c_char_p, and that prevents us
// from freeing. I could be wrong, but that's what I got from this:
Expand Down Expand Up @@ -87,94 +87,82 @@ 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),
})
}

pub fn lower_py(nm: &dyn fmt::Display, type_: &Type) -> Result<String, askama::Error> {
Ok(match type_ {
Type::UInt32 | 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::Int8
| Type::UInt8
| Type::Int16
| Type::UInt16
| Type::Int32
| Type::UInt32
| Type::Int64
| Type::UInt64
| Type::Float32
| Type::Float64 => nm.to_string(),
Type::Boolean => format!("(1 if {} else 0)", nm),
Type::String => format!("RustString.allocFromString({})", nm),
Type::Enum(_) => format!("({}.value)", nm),
Type::Object(_) => format!("({}._uniffi_handle)", nm),
Type::Error(_) => panic!("No support for lowering errors, yet"),
Type::Record(_) | Type::Optional(_) | Type::Sequence(_) => format!(
"RustBuffer.allocFrom{}({})",
class_name_py(&type_.unique_name())?,
nm
),
_ => panic!("[TODO: lower_py({:?})]", type_),
})
}

pub fn lowers_into_size_py(
nm: &dyn fmt::Display,
type_: &Type,
) -> Result<String, askama::Error> {
let nm = var_name_py(nm)?;
Ok(match type_ {
Type::UInt32 => "4".to_string(),
Type::Float64 => "8".to_string(),
Type::String => format!("4 + len({}.encode('utf-8'))", nm),
Type::Record(type_name) => format!("{}._lowersIntoSize({})", type_name, nm),
_ => panic!("[TODO: lowers_into_size_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::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::Int8
| Type::UInt8
| Type::Int16
| Type::UInt16
| Type::Int32
| Type::UInt32
| 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(_) => format!(
"{}.consumeInto{}()",
nm,
lift_from_py(&"buf", type_)?
class_name_py(&type_.unique_name())?
),
_ => panic!("[TODO: lift_py({:?})]", type_),
})
}

pub fn lift_from_py(nm: &dyn fmt::Display, type_: &Type) -> Result<String, askama::Error> {
pub fn calculate_write_size(
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_),
Type::Int8 | Type::UInt8 | Type::Boolean => "1".into(),
Type::Int16 | Type::UInt16 => "2".into(),
Type::Int32 | Type::UInt32 | Type::Float32 | Type::Enum(_) => "4".into(),
Type::Int64 | Type::UInt64 | Type::Float64 => "8".into(),
Type::String => format!("len({}.encode('utf-8'))", nm),
Type::Object(_) => panic!("No support for writing objects, yet"),
Type::Error(_) => panic!("No support for writing errors, yet"),
Type::Record(_) | Type::Optional(_) | Type::Sequence(_) => format!(
"RustBuffer.calculateWriteSizeOf{}({})",
class_name_py(&type_.unique_name())?,
nm
),
})
}
}
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 %}
14 changes: 9 additions & 5 deletions uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,28 @@ class {{ obj.name()|class_name_py }}(object):
{%- for cons in obj.constructors() %}
def __init__(self, {% call py::arg_list_decl(cons) -%}):
{%- call py::coerce_args_extra_indent(cons) %}
self._handle = {% call py::to_ffi_call(cons) %}
self._uniffi_handle = {% call py::to_ffi_call(cons) %}
{%- endfor %}

def __del__(self):
_UniFFILib.{{ obj.ffi_object_free().name() }}(self._handle)
# XXX TODO: if we have multiple instances of the python-side class
# that somehow share the same underlying handle, then the rust side
# of *all* of them will be poisoned whenever the first one is GC'd.
# Maybe we need a python-side map of handles to instances?
_UniFFILib.{{ obj.ffi_object_free().name() }}(self._uniffi_handle)

{% for meth in obj.methods() -%}
{%- match meth.return_type() -%}

{%- when Some with (return_type) -%}
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) %}
_retval = {% call py::to_ffi_call_with_prefix("self._uniffi_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) %}
{% call py::to_ffi_call_with_prefix("self._handle", meth) %}
{% call py::to_ffi_call_with_prefix("self._uniffi_handle", meth) %}
{% endmatch %}
{% endfor %}
43 changes: 3 additions & 40 deletions uniffi_bindgen/src/bindings/python/templates/RecordTemplate.py
Original file line number Diff line number Diff line change
@@ -1,52 +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):
rbuf = RustBuffer.alloc(cls._lowersIntoSize(v))
cls._lowerInto(v, RustBufferStream(rbuf))
return rbuf

@classmethod
def _lowersIntoSize(cls, v):
return 0 + \
{%- for field in rec.fields() %}
{{ "(v.{})"|format(field.name())|lowers_into_size_py(field.type_()) }}{% if loop.last %}{% else %} + \{% endif %}
{%- endfor %}

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

0 comments on commit d054e7a

Please sign in to comment.