From 13285df51be6417a23c7aa0edabaf81c816d5ecc Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Sat, 14 Dec 2024 11:30:29 -0500 Subject: [PATCH] Switch Python to BindingsIr The new system is to do a `BindingsIr` specialization pass, which means implementing `VisitMut` and using it to: - Rewrite names, docstrings, etc. - Derive things like FFI converter names and store them in `lang_data` - Generate language-specific things like imports as we walk the tree Along the way I made a couple changes to the Python bindings: Changed `_uniffi_rust_call_async` to not lift the return value. This makes it fit in with the sync logic better and also enables async constructors (which store the pointer in `self` rather than lift it). Added the `CustomTypeConfig::type_name`. While updating the code I noticed the current templates handle this in a slightly buggy way where they assume the type name is the builtin type, even if there's a `from_custom`/`into_custom` that converts to a different type. Removed almost all of the macros, `define_callable` is the only one left. --- CHANGELOG.md | 3 + Cargo.lock | 1 + docs/manual/src/types/custom_types.md | 7 +- examples/custom-types/uniffi.toml | 4 +- .../futures/tests/bindings/test_futures.py | 6 +- uniffi_bindgen/Cargo.toml | 1 + .../python/gen_python/callback_interface.rs | 31 - .../bindings/python/gen_python/compounds.rs | 118 --- .../bindings/python/gen_python/convert_ir.rs | 868 ++++++++++++++++++ .../src/bindings/python/gen_python/custom.rs | 26 - .../bindings/python/gen_python/dataclasses.rs | 92 ++ .../src/bindings/python/gen_python/enum_.rs | 39 - .../bindings/python/gen_python/external.rs | 26 - .../src/bindings/python/gen_python/ffi.rs | 73 ++ .../bindings/python/gen_python/functions.rs | 124 +++ .../bindings/python/gen_python/interfaces.rs | 109 +++ .../bindings/python/gen_python/miscellany.rs | 30 - .../src/bindings/python/gen_python/mod.rs | 770 ++++------------ .../src/bindings/python/gen_python/object.rs | 26 - .../bindings/python/gen_python/primitives.rs | 70 -- .../src/bindings/python/gen_python/record.rs | 26 - .../src/bindings/python/gen_python/types.rs | 101 ++ uniffi_bindgen/src/bindings/python/mod.rs | 3 +- .../src/bindings/python/templates/Async.py | 20 +- .../python/templates/BooleanHelper.py | 2 +- .../bindings/python/templates/BytesHelper.py | 2 +- .../templates/CallbackInterfaceTemplate.py | 13 +- .../bindings/python/templates/CustomType.py | 36 +- .../python/templates/DurationHelper.py | 2 +- .../bindings/python/templates/EnumTemplate.py | 103 +-- .../python/templates/ErrorTemplate.py | 94 +- .../python/templates/ExternalTemplate.py | 9 - .../python/templates/Float32Helper.py | 2 +- .../python/templates/Float64Helper.py | 2 +- .../src/bindings/python/templates/Helpers.py | 4 +- .../bindings/python/templates/Int16Helper.py | 2 +- .../bindings/python/templates/Int32Helper.py | 2 +- .../bindings/python/templates/Int64Helper.py | 2 +- .../bindings/python/templates/Int8Helper.py | 2 +- .../bindings/python/templates/MapTemplate.py | 4 +- .../templates/NamespaceLibraryTemplate.py | 46 +- .../python/templates/ObjectTemplate.py | 173 ++-- .../python/templates/OptionalTemplate.py | 2 +- .../src/bindings/python/templates/Protocol.py | 10 +- .../python/templates/RecordTemplate.py | 53 +- .../python/templates/RustBufferTemplate.py | 6 +- .../python/templates/SequenceTemplate.py | 2 +- .../bindings/python/templates/StringHelper.py | 2 +- .../python/templates/TimestampHelper.py | 2 +- .../templates/TopLevelFunctionTemplate.py | 49 +- .../src/bindings/python/templates/Types.py | 127 ++- .../bindings/python/templates/UInt16Helper.py | 2 +- .../bindings/python/templates/UInt32Helper.py | 2 +- .../bindings/python/templates/UInt64Helper.py | 2 +- .../bindings/python/templates/UInt8Helper.py | 2 +- .../{CallbackInterfaceImpl.py => VTable.py} | 76 +- .../src/bindings/python/templates/macros.py | 215 +---- .../src/bindings/python/templates/wrapper.py | 52 +- uniffi_internal_macros/src/lib.rs | 112 +++ 59 files changed, 2090 insertions(+), 1700 deletions(-) delete mode 100644 uniffi_bindgen/src/bindings/python/gen_python/callback_interface.rs delete mode 100644 uniffi_bindgen/src/bindings/python/gen_python/compounds.rs create mode 100644 uniffi_bindgen/src/bindings/python/gen_python/convert_ir.rs delete mode 100644 uniffi_bindgen/src/bindings/python/gen_python/custom.rs create mode 100644 uniffi_bindgen/src/bindings/python/gen_python/dataclasses.rs delete mode 100644 uniffi_bindgen/src/bindings/python/gen_python/enum_.rs delete mode 100644 uniffi_bindgen/src/bindings/python/gen_python/external.rs create mode 100644 uniffi_bindgen/src/bindings/python/gen_python/ffi.rs create mode 100644 uniffi_bindgen/src/bindings/python/gen_python/functions.rs create mode 100644 uniffi_bindgen/src/bindings/python/gen_python/interfaces.rs delete mode 100644 uniffi_bindgen/src/bindings/python/gen_python/miscellany.rs delete mode 100644 uniffi_bindgen/src/bindings/python/gen_python/object.rs delete mode 100644 uniffi_bindgen/src/bindings/python/gen_python/primitives.rs delete mode 100644 uniffi_bindgen/src/bindings/python/gen_python/record.rs create mode 100644 uniffi_bindgen/src/bindings/python/gen_python/types.rs delete mode 100644 uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py rename uniffi_bindgen/src/bindings/python/templates/{CallbackInterfaceImpl.py => VTable.py} (55%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74aaf65ca3..c2065f0f39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ We have [detailed upgrade notes](https://mozilla.github.io/uniffi-rs/next/Upgrad The `UniffiCustomTypeConverter` trait is no longer used, use the [`custom_type!` macro](https://mozilla.github.io/uniffi-rs/next/types/custom_types.html) instead. +- Python: Added the `type_name` field for custom type configs. This is required when the name of + the custom type differs from the Python type name. + - The [UDL syntax for external types](https://mozilla.github.io/uniffi-rs/next/udl/external_types.html) has changed. `typedef extern MyEnum;` has been replaced with `typedef enum MyEnum;`. Attributes other than `[External = "crate_name"]` have been removed. diff --git a/Cargo.lock b/Cargo.lock index 7dffc9d6b0..eb0ee9168c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2052,6 +2052,7 @@ dependencies = [ "serde", "textwrap", "toml", + "uniffi_internal_macros", "uniffi_meta", "uniffi_testing", "uniffi_udl", diff --git a/docs/manual/src/types/custom_types.md b/docs/manual/src/types/custom_types.md index 02af134e2d..177d599683 100644 --- a/docs/manual/src/types/custom_types.md +++ b/docs/manual/src/types/custom_types.md @@ -219,10 +219,9 @@ lower = "{}.toString()" Here's how the configuration works in `uniffi.toml`. * Create a `[bindings.{language}.custom_types.{CustomTypeName}]` table to enable a custom type on a bindings side. This has several subkeys: - * `type_name` (Optional, Typed languages only): Type/class name for the - custom type. Defaults to the type name used in the UDL. Note: The UDL - type name will still be used in generated function signatures, however it - will be defined as a typealias to this type. + * `type_name` (Optional): Type/class name for the custom type. + Defaults to the type name used in the proc macro. + Note: The UDL type name will still be used in generated function signatures for the foreign bindings, however it will be defined as a typealias to this type. * `lift`: Expression to convert the UDL type to the custom type. `{}` will be replaced with the value of the UDL type. * `lower`: Expression to convert the custom type to the UDL type. `{}` will be replaced with the value of the custom type. * `imports` (Optional) list of modules to import for your `lift`/`lower` functions. diff --git a/examples/custom-types/uniffi.toml b/examples/custom-types/uniffi.toml index 3af45bf952..d8ba782808 100644 --- a/examples/custom-types/uniffi.toml +++ b/examples/custom-types/uniffi.toml @@ -48,8 +48,8 @@ from_custom = "{}.toString()" [bindings.python.custom_types.Url] # We're going to be the urllib.parse.ParseResult class, which is the closest -# thing Python has to a Url class. No need to specify `type_name` though, -# since Python is loosely typed. +# thing Python has to a Url class. +type_name = "urllib.parse.ParseResult" # modules to import imports = ["urllib.parse"] # Functions to convert between strings and the ParsedUrl class diff --git a/fixtures/futures/tests/bindings/test_futures.py b/fixtures/futures/tests/bindings/test_futures.py index 6a3ba92cd1..a25794b5b6 100644 --- a/fixtures/futures/tests/bindings/test_futures.py +++ b/fixtures/futures/tests/bindings/test_futures.py @@ -77,13 +77,13 @@ async def test(): asyncio.run(test()) def test_async_constructors(self): - # Check the default constructor has been disabled. + # Async constructor's can't be called directly with self.assertRaises(ValueError) as e: Megaphone() - self.assertTrue(str(e.exception).startswith("async constructors not supported")) async def test(): - megaphone = await Megaphone.secondary() + # Instead, users should use the `new` classmethod. + megaphone = await Megaphone.new() result_alice = await megaphone.say_after(0, 'Alice') self.assertEqual(result_alice, 'HELLO, ALICE!') diff --git a/uniffi_bindgen/Cargo.toml b/uniffi_bindgen/Cargo.toml index f864e06217..b91e192795 100644 --- a/uniffi_bindgen/Cargo.toml +++ b/uniffi_bindgen/Cargo.toml @@ -29,6 +29,7 @@ once_cell = "1.12" paste = "1.0" serde = { version = "1", features = ["derive"] } toml = "0.5" +uniffi_internal_macros = { path = "../uniffi_internal_macros", version = "=0.28.3" } uniffi_meta = { path = "../uniffi_meta", version = "=0.28.3" } uniffi_testing = { path = "../uniffi_testing", version = "=0.28.3", optional = true } uniffi_udl = { path = "../uniffi_udl", version = "=0.28.3" } diff --git a/uniffi_bindgen/src/bindings/python/gen_python/callback_interface.rs b/uniffi_bindgen/src/bindings/python/gen_python/callback_interface.rs deleted file mode 100644 index 7793fde625..0000000000 --- a/uniffi_bindgen/src/bindings/python/gen_python/callback_interface.rs +++ /dev/null @@ -1,31 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use super::CodeType; -use crate::{backend::Literal, bail, Result}; - -#[derive(Debug)] -pub struct CallbackInterfaceCodeType { - id: String, -} - -impl CallbackInterfaceCodeType { - pub fn new(id: String) -> Self { - Self { id } - } -} - -impl CodeType for CallbackInterfaceCodeType { - fn type_label(&self) -> String { - super::PythonCodeOracle.class_name(&self.id) - } - - fn canonical_name(&self) -> String { - format!("Type{}", self.type_label()) - } - - fn literal(&self, _literal: &Literal) -> Result { - bail!("literals not supported here") - } -} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/compounds.rs b/uniffi_bindgen/src/bindings/python/gen_python/compounds.rs deleted file mode 100644 index b06bfc9974..0000000000 --- a/uniffi_bindgen/src/bindings/python/gen_python/compounds.rs +++ /dev/null @@ -1,118 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use super::CodeType; -use crate::{ - backend::{Literal, Type}, - bail, - bindings::python::gen_python::AsCodeType, - Result, -}; - -#[derive(Debug)] -pub struct OptionalCodeType { - inner: Type, -} - -impl OptionalCodeType { - pub fn new(inner: Type) -> Self { - Self { inner } - } -} - -impl CodeType for OptionalCodeType { - fn type_label(&self) -> String { - format!( - "typing.Optional[{}]", - super::PythonCodeOracle.find(&self.inner).type_label() - ) - } - - fn canonical_name(&self) -> String { - format!( - "Optional{}", - super::PythonCodeOracle.find(&self.inner).canonical_name(), - ) - } - - fn literal(&self, literal: &Literal) -> Result { - match literal { - Literal::None => Ok("None".into()), - Literal::Some { inner } => super::PythonCodeOracle.find(&self.inner).literal(inner), - _ => bail!("Invalid literal for Optional type: {literal:?}"), - } - } -} - -#[derive(Debug)] -pub struct SequenceCodeType { - inner: Type, -} - -impl SequenceCodeType { - pub fn new(inner: Type) -> Self { - Self { inner } - } -} - -impl CodeType for SequenceCodeType { - fn type_label(&self) -> String { - // Python 3.8 and below do not support `list[T]` - format!( - "typing.List[{}]", - super::PythonCodeOracle.find(&self.inner).type_label() - ) - } - - fn canonical_name(&self) -> String { - format!( - "Sequence{}", - super::PythonCodeOracle.find(&self.inner).canonical_name(), - ) - } - - fn literal(&self, literal: &Literal) -> Result { - match literal { - Literal::EmptySequence => Ok("[]".into()), - _ => bail!("Invalid literal for sequence type: {literal:?}"), - } - } -} - -#[derive(Debug)] -pub struct MapCodeType { - key: Type, - value: Type, -} - -impl MapCodeType { - pub fn new(key: Type, value: Type) -> Self { - Self { key, value } - } -} - -impl CodeType for MapCodeType { - fn type_label(&self) -> String { - format!( - "dict[{}, {}]", - self.key.as_codetype().type_label(), - self.value.as_codetype().type_label() - ) - } - - fn canonical_name(&self) -> String { - format!( - "Map{}{}", - super::PythonCodeOracle.find(&self.key).canonical_name(), - super::PythonCodeOracle.find(&self.value).canonical_name(), - ) - } - - fn literal(&self, literal: &Literal) -> Result { - match literal { - Literal::EmptyMap => Ok("{}".into()), - _ => bail!("Invalid literal for map type: {literal:?}"), - } - } -} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/convert_ir.rs b/uniffi_bindgen/src/bindings/python/gen_python/convert_ir.rs new file mode 100644 index 0000000000..1e2e9fe600 --- /dev/null +++ b/uniffi_bindgen/src/bindings/python/gen_python/convert_ir.rs @@ -0,0 +1,868 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::collections::{BTreeSet, HashSet}; + +use anyhow::{bail, Result}; +use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase}; +use once_cell::sync::Lazy; + +use super::*; +use crate::interface::ir; + +/// Convert a general IR to a Python IR +pub fn convert_bindings_ir(general_ir: BindingsIr, config: Config) -> Result { + BindingsIrConverter::new(config).convert_bindings_ir(general_ir) +} + +/// Struct that represents an in-process conversion +struct BindingsIrConverter { + config: Config, +} + +impl BindingsIrConverter { + fn new(config: Config) -> Self { + Self { config } + } + + fn convert_bindings_ir(&self, general_ir: BindingsIr) -> Result { + // Create a mostly empty new `BindingsIr` + let globals = GlobalNodes { + ffi_rustbuffer_alloc: general_ir.globals.ffi_rustbuffer_alloc.name, + ffi_rustbuffer_reserve: general_ir.globals.ffi_rustbuffer_reserve.name, + ffi_rustbuffer_free: general_ir.globals.ffi_rustbuffer_free.name, + ffi_uniffi_contract_version: general_ir.globals.ffi_uniffi_contract_version.name, + callback_interface_free_type: self + .ffi_type_name(general_ir.globals.callback_interface_free_type), + string_type: self.convert_type(general_ir.globals.string_type), + contract_version: general_ir.globals.contract_version, + }; + let mut py_ir = PythonBindingsIr { + namespace: general_ir.namespace, + cdylib_name: self + .config + .cdylib_name + .clone() + .unwrap_or_else(|| "uniffi".to_string()), + module_docstring: self.format_docstring(general_ir.crate_docstring), + runtimes: Runtimes::default(), + globals, + ffi_definitions: vec![], + protocols: vec![], + type_definitions: vec![], + functions: vec![], + checksum_checks: vec![], + imports: BTreeSet::new(), + exports: vec![], + }; + // Call the `process_*` methods to populate the new IR + for ffi_def in general_ir.ffi_definitions { + match ffi_def { + ir::FfiDefinition::Function(f) => self.process_ffi_func(&mut py_ir, f), + ir::FfiDefinition::FunctionType(f) => self.process_ffi_func_type(&mut py_ir, f), + ir::FfiDefinition::Struct(s) => self.process_ffi_struct(&mut py_ir, s), + } + } + for type_def in general_ir.type_definitions { + match type_def { + ir::TypeDefinition::Simple(t) => self.process_builtin(&mut py_ir, t)?, + ir::TypeDefinition::Optional(o) => self.process_optional(&mut py_ir, o)?, + ir::TypeDefinition::Sequence(o) => self.process_sequence(&mut py_ir, o)?, + ir::TypeDefinition::Map(m) => self.process_map(&mut py_ir, m)?, + ir::TypeDefinition::Record(r) => self.process_record(&mut py_ir, r)?, + ir::TypeDefinition::Enum(e) => self.process_enum(&mut py_ir, e)?, + ir::TypeDefinition::Interface(i) => self.process_interface(&mut py_ir, i)?, + ir::TypeDefinition::CallbackInterface(c) => { + self.process_callback_interface(&mut py_ir, c)? + } + ir::TypeDefinition::Custom(c) => self.process_custom_type(&mut py_ir, c)?, + ir::TypeDefinition::External(e) => self.process_external_type(&mut py_ir, e)?, + } + } + for func in general_ir.functions { + self.process_function(&mut py_ir, func)?; + } + for check in general_ir.checksum_checks { + self.process_checksum_check(&mut py_ir, check); + } + Ok(py_ir) + } + + fn process_ffi_func(&self, py_ir: &mut PythonBindingsIr, ffi_func: ir::FfiFunction) { + let ffi_func = FfiFunction { + name: ffi_func.name, + is_async: ffi_func.is_async, + arguments: self.convert_ffi_arguments(ffi_func.arguments), + return_type: ffi_func + .return_type + .map(|ffi_type| self.ffi_type_name(ffi_type)), + has_rust_call_status_arg: ffi_func.has_rust_call_status_arg, + }; + py_ir + .ffi_definitions + .push(FfiDefinition::Function(ffi_func)); + } + + fn process_ffi_func_type( + &self, + py_ir: &mut PythonBindingsIr, + ffi_func_type: ir::FfiFunctionType, + ) { + let ffi_func_type = FfiFunctionType { + name: self.ffi_function_type_name(&ffi_func_type.name), + arguments: self.convert_ffi_arguments(ffi_func_type.arguments), + return_type: ffi_func_type + .return_type + .map(|ffi_type| self.ffi_type_name(ffi_type)), + has_rust_call_status_arg: ffi_func_type.has_rust_call_status_arg, + }; + py_ir + .ffi_definitions + .push(FfiDefinition::FunctionType(ffi_func_type)); + } + + fn process_ffi_struct(&self, py_ir: &mut PythonBindingsIr, st: ir::FfiStruct) { + let ffi_struct = FfiStruct { + name: self.ffi_struct_name(&st.name), + fields: self.convert_ffi_fields(st.fields), + }; + py_ir + .ffi_definitions + .push(FfiDefinition::Struct(ffi_struct)); + } + + fn process_checksum_check(&self, py_ir: &mut PythonBindingsIr, check: ir::ChecksumCheck) { + py_ir.checksum_checks.push(ChecksumCheck { + func: check.func.name, + checksum: check.checksum, + }) + } + + fn convert_ffi_arguments(&self, args: Vec) -> Vec { + args.into_iter() + .map(|arg| FfiArgument { + name: arg.name, + ty: self.ffi_type_name(arg.ty), + }) + .collect() + } + + fn convert_ffi_fields(&self, fields: Vec) -> Vec { + fields + .into_iter() + .map(|field| FfiField { + name: field.name, + ty: self.ffi_type_name(field.ty), + }) + .collect() + } + + fn process_builtin(&self, py_ir: &mut PythonBindingsIr, ty: ir::Type) -> Result<()> { + py_ir.type_definitions.push(match &ty.kind { + uniffi_meta::Type::UInt8 + | uniffi_meta::Type::Int8 + | uniffi_meta::Type::UInt16 + | uniffi_meta::Type::Int16 + | uniffi_meta::Type::UInt32 + | uniffi_meta::Type::Int32 + | uniffi_meta::Type::UInt64 + | uniffi_meta::Type::Int64 + | uniffi_meta::Type::Float32 + | uniffi_meta::Type::Float64 + | uniffi_meta::Type::Boolean + | uniffi_meta::Type::String + | uniffi_meta::Type::Bytes + | uniffi_meta::Type::Timestamp + | uniffi_meta::Type::Duration => TypeDefinition::Simple(self.convert_type(ty)), + _ => bail!("Invalid builtin type: {ty:?}"), + }); + Ok(()) + } + + fn process_optional(&self, py_ir: &mut PythonBindingsIr, opt: ir::OptionalType) -> Result<()> { + py_ir + .type_definitions + .push(TypeDefinition::Optional(OptionalType { + inner: self.convert_type(opt.inner), + self_type: self.convert_type(opt.self_type), + })); + Ok(()) + } + + fn process_sequence(&self, py_ir: &mut PythonBindingsIr, seq: ir::SequenceType) -> Result<()> { + py_ir + .type_definitions + .push(TypeDefinition::Sequence(SequenceType { + inner: self.convert_type(seq.inner), + self_type: self.convert_type(seq.self_type), + })); + Ok(()) + } + + fn process_map(&self, py_ir: &mut PythonBindingsIr, map: ir::MapType) -> Result<()> { + py_ir.type_definitions.push(TypeDefinition::Map(MapType { + key: self.convert_type(map.key), + value: self.convert_type(map.value), + self_type: self.convert_type(map.self_type), + })); + Ok(()) + } + + fn process_record(&self, py_ir: &mut PythonBindingsIr, rec: ir::Record) -> Result<()> { + let rec = Record { + name: self.class_name(&rec.name), + fields: self.convert_fields(rec.fields)?, + docstring: self.format_docstring(rec.docstring), + self_type: self.convert_type(rec.self_type), + }; + py_ir.exports.push(rec.name.clone()); + py_ir.type_definitions.push(TypeDefinition::Record(rec)); + Ok(()) + } + + fn process_enum(&self, py_ir: &mut PythonBindingsIr, en: ir::Enum) -> Result<()> { + let enum_ = Enum { + name: self.class_name(&en.name), + discr_type: self.convert_opt_type(en.discr_type), + variants: self.convert_variants(en.variants)?, + shape: en.shape, + docstring: self.format_docstring(en.docstring), + self_type: self.convert_type(en.self_type), + }; + py_ir.exports.push(enum_.name.clone()); + py_ir.type_definitions.push(TypeDefinition::Enum(enum_)); + Ok(()) + } + + fn process_interface(&self, py_ir: &mut PythonBindingsIr, int: ir::Interface) -> Result<()> { + if int.has_callback_interface() { + // This is a trait interface that can be implemented in Python, so it is treated like a + // callback interface where the primary use-case is the trait being implemented + // locally. It is a base-class local implementations might subclass. + // We reuse "Protocol.py" for this, even though here we are not generating a protocol + py_ir.runtimes.callback_interface = true; + if int.has_async_method() { + py_ir.runtimes.async_callback = true; + } + } + let (protocol, interface_name) = if int.has_callback_interface() { + // This is a trait interface that can be implemented in Python, so it is treated like a + // callback interface where the primary use-case is the trait being implemented + // locally. It is a base-class local implementations might subclass. + // We reuse "Protocol.py" for this, even though here we are not generating a protocol + let protocol = Protocol { + name: self.class_name(&int.name), + base_class: "".to_string(), + docstring: self.format_docstring(int.docstring.clone()), + methods: self.convert_methods(py_ir, int.methods.clone())?, + }; + (protocol, format!("{}Impl", self.class_name(&int.name))) + } else { + let protocol = Protocol { + name: format!("{}Protocol", self.class_name(&int.name)), + base_class: "typing.Protocol".to_string(), + docstring: self.format_docstring(int.docstring.clone()), + methods: self.convert_methods(py_ir, int.methods.clone())?, + }; + (protocol, self.class_name(&int.name)) + }; + let had_async_constructor = int.constructors.iter().any(|c| c.primary && c.is_async()); + + let int = Interface { + name: interface_name, + protocol_name: protocol.name.clone(), + constructors: self + .convert_constructors(py_ir, int.constructors)? + .into_iter() + .map(|cons| { + // Python constructors can't be async. If the primary constructor from Rust is async, then + // treat it like a secondary constructor which generates a factory method. + if cons.primary && cons.is_async() { + Constructor { + name: "new".to_string(), + primary: false, + ..cons + } + } else { + cons + } + }) + .collect(), + methods: self.convert_methods(py_ir, int.methods)?, + had_async_constructor, + uniffi_traits: self.convert_uniffi_traits(int.uniffi_traits)?, + base_classes: std::iter::once(protocol.name.clone()) + .chain( + int.trait_impls + .iter() + .map(|trait_impl| self.class_name(&trait_impl.trait_name)), + ) + .chain( + int.self_type + .is_used_as_error + .then(|| "Exception".to_string()), + ) + .collect(), + vtable: self.convert_opt_vtable(int.vtable)?, + ffi_clone: int.ffi_clone.name, + ffi_free: int.ffi_free.name, + docstring: self.format_docstring(int.docstring), + self_type: self.convert_type(int.self_type), + }; + py_ir.exports.push(protocol.name.clone()); + py_ir.exports.push(int.name.clone()); + py_ir.protocols.push(protocol); + py_ir.type_definitions.push(TypeDefinition::Interface(int)); + Ok(()) + } + + fn process_callback_interface( + &self, + py_ir: &mut PythonBindingsIr, + cbi: ir::CallbackInterface, + ) -> Result<()> { + py_ir.runtimes.callback_interface = true; + if cbi.has_async_method() { + py_ir.runtimes.async_callback = true; + } + let cbi = CallbackInterface { + name: self.class_name(&cbi.name), + methods: self.convert_methods(py_ir, cbi.methods)?, + vtable: self.convert_vtable(cbi.vtable)?, + docstring: self.format_docstring(cbi.docstring), + self_type: self.convert_type(cbi.self_type), + }; + + let protocol = Protocol { + name: self.class_name(&cbi.name), + base_class: "typing.Protocol".to_string(), + docstring: cbi.docstring.clone(), + methods: cbi.methods.clone(), + }; + py_ir.exports.push(cbi.name.clone()); + py_ir.exports.push(protocol.name.clone()); + py_ir + .type_definitions + .push(TypeDefinition::CallbackInterface(cbi)); + py_ir.protocols.push(protocol); + Ok(()) + } + + fn process_custom_type( + &self, + py_ir: &mut PythonBindingsIr, + custom: ir::CustomType, + ) -> Result<()> { + let config = self.config.custom_types.get(&custom.name).cloned(); + if let Some(config) = &config { + for mod_name in config.imports.iter().flatten() { + py_ir.imports.insert(format!("import {mod_name}")); + } + } + let custom = CustomType { + config, + name: self.class_name(&custom.name), + builtin: self.convert_type(custom.builtin), + self_type: self.convert_type(custom.self_type), + }; + py_ir.exports.push(custom.name.clone()); + py_ir.type_definitions.push(TypeDefinition::Custom(custom)); + Ok(()) + } + + fn process_external_type( + &self, + py_ir: &mut PythonBindingsIr, + ext: ir::ExternalType, + ) -> Result<()> { + let mod_name = self.config.module_for_namespace(&ext.namespace); + let name = self.class_name(&ext.name); + py_ir.imports.insert(format!("import {mod_name}")); + py_ir + .imports + .insert(format!("from {mod_name} import {name}")); + let ext = ExternalType { + name, + namespace: ext.namespace, + kind: ext.kind, + self_type: self.convert_type(ext.self_type), + }; + py_ir.type_definitions.push(TypeDefinition::External(ext)); + Ok(()) + } + + fn process_function(&self, py_ir: &mut PythonBindingsIr, func: ir::Function) -> Result<()> { + let func = Function { + name: self.fn_name(&func.name), + callable: self.convert_callable(func.callable)?, + docstring: self.format_docstring(func.docstring), + }; + if func.is_async() { + self.process_async_func(py_ir); + } + py_ir.exports.push(func.name.clone()); + py_ir.functions.push(func); + Ok(()) + } + + fn convert_callable(&self, callable: ir::Callable) -> Result { + Ok(Callable { + kind: callable.kind, + async_data: self.convert_async_data(callable.async_data), + arguments: self.convert_arguments(callable.arguments)?, + return_type: self.convert_opt_type(callable.return_type), + throws_type: self.convert_opt_type(callable.throws_type), + ffi_func: callable.ffi_func.name, + }) + } + + fn convert_methods( + &self, + py_ir: &mut PythonBindingsIr, + methods: Vec, + ) -> Result> { + let methods = methods + .into_iter() + .map(|meth| self.convert_method(meth)) + .collect::>>()?; + for meth in methods.iter() { + if meth.is_async() { + self.process_async_func(py_ir); + } + } + Ok(methods) + } + + fn convert_method(&self, meth: ir::Method) -> Result { + Ok(Method { + name: self.fn_name(&meth.name), + interface: self.convert_type(meth.interface), + callable: self.convert_callable(meth.callable)?, + docstring: self.format_docstring(meth.docstring), + }) + } + + fn convert_constructors( + &self, + py_ir: &mut PythonBindingsIr, + constructors: Vec, + ) -> Result> { + let constructors = constructors + .into_iter() + .map(|cons| { + Ok(Constructor { + name: if cons.primary { + "__init__".to_string() + } else { + self.fn_name(&cons.name) + }, + primary: cons.primary, + interface: self.convert_type(cons.interface), + callable: self.convert_callable(cons.callable)?, + docstring: self.format_docstring(cons.docstring), + }) + }) + .collect::>>()?; + for cons in &constructors { + if cons.is_async() { + self.process_async_func(py_ir); + } + } + Ok(constructors) + } + + fn convert_opt_vtable(&self, vtable: Option) -> Result> { + vtable.map(|vtable| self.convert_vtable(vtable)).transpose() + } + + fn convert_vtable(&self, vtable: ir::VTable) -> Result { + Ok(VTable { + name: self.ffi_struct_name(&vtable.name), + ffi_type: self.ffi_type_name(vtable.ffi_type), + ffi_init_callback: vtable.ffi_init_callback.name, + methods: self.convert_vtable_methods(vtable.methods)?, + }) + } + + fn convert_vtable_methods(&self, methods: Vec) -> Result> { + methods + .into_iter() + .map(|vmeth| { + let default_return_value = match &vmeth.callable.return_type { + Some(t) => match &t.ffi_type { + ir::FfiType::UInt8 + | ir::FfiType::Int8 + | ir::FfiType::UInt16 + | ir::FfiType::Int16 + | ir::FfiType::UInt32 + | ir::FfiType::Int32 + | ir::FfiType::UInt64 + | ir::FfiType::Int64 + | ir::FfiType::Handle => "0".to_string(), + ir::FfiType::Float32 | ir::FfiType::Float64 => "0.0".to_string(), + ir::FfiType::RustArcPtr(_) => "ctypes.c_void_p()".to_string(), + ir::FfiType::RustBuffer(meta) => match meta { + None => "_UniffiRustBuffer.default()".to_string(), + Some(meta) => { + let module_name = self.config.module_for_namespace(&meta.namespace); + format!("{module_name}._UniffiRustBuffer.default()") + } + }, + _ => unimplemented!("FFI default for: {t:?}"), + }, + // When we need to use a value for void returns, we use a `u8` placeholder and `0` as + // the default. + None => "0".to_string(), + }; + Ok(VTableMethod { + ffi_type: self.ffi_type_name(vmeth.ffi_type), + name: vmeth.name, + default_return_value, + callable: self.convert_callable(vmeth.callable)?, + }) + }) + .collect() + } + + fn convert_uniffi_traits( + &self, + uniffi_traits: Vec, + ) -> Result> { + uniffi_traits + .into_iter() + .map(|ut| { + Ok(match ut { + ir::UniffiTrait::Debug { fmt } => UniffiTrait::Debug { + fmt: Method { + name: "__repr__".to_string(), + ..self.convert_method(fmt)? + }, + }, + ir::UniffiTrait::Display { fmt } => UniffiTrait::Display { + fmt: Method { + name: "__str__".to_string(), + ..self.convert_method(fmt)? + }, + }, + ir::UniffiTrait::Eq { eq, ne } => UniffiTrait::Eq { + eq: Method { + name: "__eq__".to_string(), + ..self.convert_method(eq)? + }, + ne: Method { + name: "__ne__".to_string(), + ..self.convert_method(ne)? + }, + }, + ir::UniffiTrait::Hash { hash } => UniffiTrait::Hash { + hash: Method { + name: "__hash__".to_string(), + ..self.convert_method(hash)? + }, + }, + }) + }) + .collect() + } + + fn convert_variants(&self, variants: Vec) -> Result> { + variants + .into_iter() + .map(|variant| { + Ok(Variant { + name: if variant.enum_shape.is_error() { + self.class_name(&variant.name) + } else { + self.enum_variant_name(&variant.name) + }, + docstring: self.format_docstring(variant.docstring), + discr: self.literal(variant.discr)?, + enum_shape: variant.enum_shape, + fields: self.convert_fields(variant.fields)?, + }) + }) + .collect() + } + + fn convert_arguments(&self, arguments: Vec) -> Result> { + arguments + .into_iter() + .map(|arg| { + Ok(Argument { + name: self.var_name(&arg.name), + ty: self.convert_type(arg.ty), + default: self.convert_default(arg.default)?, + }) + }) + .collect() + } + + fn convert_fields(&self, fields: Vec) -> Result> { + fields + .into_iter() + .map(|field| { + Ok(Field { + name: self.var_name(&field.name), + docstring: self.format_docstring(field.docstring), + ty: self.convert_type(field.ty), + default: self.convert_default(field.default)?, + }) + }) + .collect() + } + + fn convert_async_data(&self, async_data: Option) -> Option { + async_data.map(|async_data| AsyncData { + ffi_rust_future_poll: async_data.ffi_rust_future_poll.name, + ffi_rust_future_complete: async_data.ffi_rust_future_complete.name, + ffi_rust_future_free: async_data.ffi_rust_future_free.name, + foreign_future_result_type: self.ffi_type_name(async_data.foreign_future_result_type), + }) + } + + fn convert_opt_type(&self, ty: Option) -> Option { + ty.map(|ty| self.convert_type(ty)) + } + + fn convert_type(&self, ty: ir::Type) -> Type { + let ffi_converter_name = match &ty.kind { + uniffi_meta::Type::External { namespace, .. } => { + let mod_name = self.config.module_for_namespace(namespace); + format!("{mod_name}._UniffiConverter{}", ty.canonical_name()) + } + _ => format!("_UniffiConverter{}", ty.canonical_name()), + }; + Type { + type_name: self.type_name(&ty.kind), + ffi_converter_name, + kind: ty.kind, + ffi_type: self.ffi_type_name(ty.ffi_type), + is_used_as_error: ty.is_used_as_error, + } + } + + fn type_name(&self, ty: &uniffi_meta::Type) -> String { + match ty { + uniffi_meta::Type::Boolean => "bool".to_string(), + uniffi_meta::Type::String => "str".to_string(), + uniffi_meta::Type::Bytes => "bytes".to_string(), + uniffi_meta::Type::Int8 => "int".to_string(), + uniffi_meta::Type::Int16 + | uniffi_meta::Type::Int32 + | uniffi_meta::Type::Int64 + | uniffi_meta::Type::UInt8 + | uniffi_meta::Type::UInt16 + | uniffi_meta::Type::UInt32 + | uniffi_meta::Type::UInt64 => "int".to_string(), + uniffi_meta::Type::Duration => "Duration".to_string(), + uniffi_meta::Type::Timestamp => "Timestamp".to_string(), + uniffi_meta::Type::Float32 | uniffi_meta::Type::Float64 => "float".to_string(), + uniffi_meta::Type::Object { name, .. } + | uniffi_meta::Type::Record { name, .. } + | uniffi_meta::Type::Enum { name, .. } + | uniffi_meta::Type::CallbackInterface { name, .. } + | uniffi_meta::Type::Custom { name, .. } + | uniffi_meta::Type::External { name, .. } => self.class_name(name), + uniffi_meta::Type::Optional { inner_type } => { + format!("typing.Optional[{}]", self.type_name(inner_type)) + } + uniffi_meta::Type::Sequence { inner_type } => { + format!("typing.List[{}]", self.type_name(inner_type)) + } + uniffi_meta::Type::Map { + key_type, + value_type, + } => format!( + "dict[{}, {}]", + self.type_name(key_type), + self.type_name(value_type) + ), + } + } + + fn process_async_func(&self, py_ir: &mut PythonBindingsIr) { + py_ir.imports.insert("import asyncio".to_string()); + py_ir.runtimes.async_ = true; + } + + /// Python type name for an FfiType + fn ffi_type_name(&self, ffi_type: ir::FfiType) -> String { + match ffi_type { + ir::FfiType::Int8 => "ctypes.c_int8".to_string(), + ir::FfiType::UInt8 => "ctypes.c_uint8".to_string(), + ir::FfiType::Int16 => "ctypes.c_int16".to_string(), + ir::FfiType::UInt16 => "ctypes.c_uint16".to_string(), + ir::FfiType::Int32 => "ctypes.c_int32".to_string(), + ir::FfiType::UInt32 => "ctypes.c_uint32".to_string(), + ir::FfiType::Int64 => "ctypes.c_int64".to_string(), + ir::FfiType::UInt64 => "ctypes.c_uint64".to_string(), + ir::FfiType::Float32 => "ctypes.c_float".to_string(), + ir::FfiType::Float64 => "ctypes.c_double".to_string(), + ir::FfiType::Handle => "ctypes.c_uint64".to_string(), + ir::FfiType::RustArcPtr(_) => "ctypes.c_void_p".to_string(), + ir::FfiType::RustBuffer(meta) => match meta { + None => "_UniffiRustBuffer".to_string(), + Some(meta) => { + let module_name = self.config.module_for_namespace(&meta.namespace); + format!("{module_name}._UniffiRustBuffer") + } + }, + ir::FfiType::RustCallStatus => "_UniffiRustCallStatus".to_string(), + ir::FfiType::ForeignBytes => "_UniffiForeignBytes".to_string(), + ir::FfiType::FunctionPointer(name) => self.ffi_function_type_name(&name), + ir::FfiType::Struct(name) => self.ffi_struct_name(&name), + // Pointer to an `asyncio.EventLoop` instance + ir::FfiType::Reference(inner) | ir::FfiType::MutReference(inner) => { + format!("ctypes.POINTER({})", self.ffi_type_name(*inner)) + } + ir::FfiType::VoidPointer => "ctypes.c_void_p".to_string(), + } + } + + /// Idiomatic name for an FFI function type. + /// + /// These follow the ctypes convention of SHOUTY_SNAKE_CASE. Prefix with `_UNIFFI` so that + /// it can't conflict with user-defined items. + fn ffi_function_type_name(&self, nm: &str) -> String { + format!("_UNIFFI_FN_{}", nm.to_shouty_snake_case()) + } + + /// Idiomatic name for an FFI struct. + /// + /// These follow the ctypes convention of UpperCamelCase (although the ctypes docs also uses + /// SHOUTY_SNAKE_CASE in some places). Prefix with `_Uniffi` so that it can't conflict with + /// user-defined items. + fn ffi_struct_name(&self, nm: &str) -> String { + format!("_UniffiStruct{}", nm.to_upper_camel_case()) + } + + /// Idiomatic Python class name (for enums, records, errors, etc). + fn class_name(&self, nm: &str) -> String { + self.fixup_keyword(nm.to_string().to_upper_camel_case()) + } + + /// Idiomatic Python function name. + fn fn_name(&self, nm: &str) -> String { + self.fixup_keyword(nm.to_string().to_snake_case()) + } + + /// Idiomatic Python a variable name. + fn var_name(&self, nm: &str) -> String { + self.fixup_keyword(nm.to_string().to_snake_case()) + } + + /// Idiomatic Python enum variant name. + /// + /// These use SHOUTY_SNAKE_CASE style. User's access them through `EnumName.VARIANT_NAME`. + /// This naming style is used for both flat enums and fielded ones. + /// + /// The exception is error enums. In that case, there's no enum in the generated python. + /// Instead there's a base class and subclasses -- all with UpperCamelCase names. + /// That case is not handled by this method. + fn enum_variant_name(&self, nm: &str) -> String { + self.fixup_keyword(nm.to_string().to_shouty_snake_case()) + } + + /// Fixup a name by ensuring it's not a keyword + fn fixup_keyword(&self, name: String) -> String { + if KEYWORDS.contains(&name) { + format!("_{name}") + } else { + name + } + } + + pub fn format_docstring(&self, docstring: Option) -> Option { + docstring.map(|docstring| { + // Escape triple quotes to avoid syntax errors in docstrings + let docstring = docstring.replace(r#"""""#, r#"\"\"\""#); + // Remove indentation and surround with quotes + format!("\"\"\"\n{}\n\"\"\"", &textwrap::dedent(&docstring)) + }) + } + + fn convert_default(&self, default: Option) -> Result> { + default.map(|lit| self.literal(lit)).transpose() + } + + /// Python rendering of a literal value + fn literal(&self, lit: ir::Literal) -> Result { + Ok(match lit { + ir::Literal::Boolean(true) => "True".to_string(), + ir::Literal::Boolean(false) => "False".to_string(), + ir::Literal::String(s) => format!("\"{s}\""), + // https://docs.python.org/3/reference/lexical_analysis.html#integer-literals + ir::Literal::Int(i, radix, _) => match radix { + ir::Radix::Octal => format!("int(0o{i:o})"), + ir::Radix::Decimal => format!("{i}"), + ir::Radix::Hexadecimal => format!("{i:#x}"), + }, + ir::Literal::UInt(i, radix, _) => match radix { + ir::Radix::Octal => format!("0o{i:o}"), + ir::Radix::Decimal => format!("{i}"), + ir::Radix::Hexadecimal => format!("{i:#x}"), + }, + ir::Literal::Float(value, _) => value.clone(), + ir::Literal::EmptySequence => "[]".to_string(), + ir::Literal::EmptyMap => "{}".to_string(), + ir::Literal::None => "None".to_string(), + ir::Literal::Some { inner } => self.literal(*inner)?, + ir::Literal::Enum(variant, ty) => match &ty.kind { + uniffi_meta::Type::Enum { name, .. } => { + format!( + "{}.{}", + self.class_name(name), + self.enum_variant_name(&variant) + ) + } + type_kind => { + bail!("Invalid type for enum literal: {type_kind:?}") + } + }, + }) + } +} + +// Taken from Python's `keyword.py` module. +static KEYWORDS: Lazy> = Lazy::new(|| { + let kwlist = vec![ + "False", + "None", + "True", + "__peg_parser__", + "and", + "as", + "assert", + "async", + "await", + "break", + "class", + "continue", + "def", + "del", + "elif", + "else", + "except", + "finally", + "for", + "from", + "global", + "if", + "import", + "in", + "is", + "lambda", + "nonlocal", + "not", + "or", + "pass", + "raise", + "return", + "try", + "while", + "with", + "yield", + ]; + HashSet::from_iter(kwlist.into_iter().map(|s| s.to_string())) +}); diff --git a/uniffi_bindgen/src/bindings/python/gen_python/custom.rs b/uniffi_bindgen/src/bindings/python/gen_python/custom.rs deleted file mode 100644 index 8782339170..0000000000 --- a/uniffi_bindgen/src/bindings/python/gen_python/custom.rs +++ /dev/null @@ -1,26 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use super::CodeType; - -#[derive(Debug)] -pub struct CustomCodeType { - name: String, -} - -impl CustomCodeType { - pub fn new(name: String) -> Self { - Self { name } - } -} - -impl CodeType for CustomCodeType { - fn type_label(&self) -> String { - super::PythonCodeOracle.class_name(&self.name) - } - - fn canonical_name(&self) -> String { - format!("Type{}", self.type_label()) - } -} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/dataclasses.rs b/uniffi_bindgen/src/bindings/python/gen_python/dataclasses.rs new file mode 100644 index 0000000000..417c34bacf --- /dev/null +++ b/uniffi_bindgen/src/bindings/python/gen_python/dataclasses.rs @@ -0,0 +1,92 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use uniffi_internal_macros::AsType; +use uniffi_meta::EnumShape; + +use super::{AsType, Type}; + +/// Represents a "data class" style object, for passing around complex values. +/// +/// In the FFI these are represented as a byte buffer, which one side explicitly +/// serializes the data into and the other serializes it out of. So I guess they're +/// kind of like "pass by clone" values. +#[derive(Debug, Clone, AsType)] +pub struct Record { + pub name: String, + pub fields: Vec, + pub docstring: Option, + pub self_type: Type, +} + +// Represents an individual field on a Record. +#[derive(Debug, Clone, AsType)] +pub struct Field { + pub name: String, + pub ty: Type, + pub default: Option, + pub docstring: Option, +} + +/// Represents an enum with named variants, each of which may have named +/// and typed fields. +/// +/// Enums are passed across the FFI by serializing to a bytebuffer, with a +/// i32 indicating the variant followed by the serialization of each field. +#[derive(Debug, Clone, AsType)] +pub struct Enum { + pub name: String, + pub discr_type: Option, + pub variants: Vec, + pub shape: EnumShape, + pub docstring: Option, + pub self_type: Type, +} + +/// Represents an individual variant in an Enum. +/// +/// Each variant has a name and zero or more fields. +#[derive(Debug, Clone)] +pub struct Variant { + pub name: String, + pub discr: String, + pub fields: Vec, + pub enum_shape: EnumShape, + pub docstring: Option, +} + +impl Record { + pub fn has_fields(&self) -> bool { + !self.fields.is_empty() + } +} + +impl Enum { + pub fn is_flat(&self) -> bool { + match self.shape { + EnumShape::Error { flat } => flat, + EnumShape::Enum => self.variants.iter().all(|v| v.fields.is_empty()), + } + } + + pub fn is_flat_error(&self) -> bool { + self.shape.is_flat_error() + } +} + +impl Variant { + pub fn has_fields(&self) -> bool { + !self.fields.is_empty() + } + + pub fn has_nameless_fields(&self) -> bool { + self.fields.iter().any(|f| f.name.is_empty()) + } +} + +impl Field { + pub fn has_default(&self) -> bool { + self.default.is_some() + } +} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/enum_.rs b/uniffi_bindgen/src/bindings/python/gen_python/enum_.rs deleted file mode 100644 index 30419c546e..0000000000 --- a/uniffi_bindgen/src/bindings/python/gen_python/enum_.rs +++ /dev/null @@ -1,39 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use super::CodeType; -use crate::{backend::Literal, bail, Result}; - -#[derive(Debug)] -pub struct EnumCodeType { - id: String, -} - -impl EnumCodeType { - pub fn new(id: String) -> Self { - Self { id } - } -} - -impl CodeType for EnumCodeType { - fn type_label(&self) -> String { - super::PythonCodeOracle.class_name(&self.id) - } - - fn canonical_name(&self) -> String { - format!("Type{}", self.type_label()) - } - - fn literal(&self, literal: &Literal) -> Result { - if let Literal::Enum(v, _) = literal { - Ok(format!( - "{}.{}", - self.type_label(), - super::PythonCodeOracle.enum_variant_name(v) - )) - } else { - bail!("Invalid literal for enum type: {literal:?}") - } - } -} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/external.rs b/uniffi_bindgen/src/bindings/python/gen_python/external.rs deleted file mode 100644 index c98738eba4..0000000000 --- a/uniffi_bindgen/src/bindings/python/gen_python/external.rs +++ /dev/null @@ -1,26 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use super::CodeType; - -#[derive(Debug)] -pub struct ExternalCodeType { - name: String, -} - -impl ExternalCodeType { - pub fn new(name: String) -> Self { - Self { name } - } -} - -impl CodeType for ExternalCodeType { - fn type_label(&self) -> String { - super::PythonCodeOracle.class_name(&self.name) - } - - fn canonical_name(&self) -> String { - format!("Type{}", self.type_label()) - } -} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/ffi.rs b/uniffi_bindgen/src/bindings/python/gen_python/ffi.rs new file mode 100644 index 0000000000..35b4df6717 --- /dev/null +++ b/uniffi_bindgen/src/bindings/python/gen_python/ffi.rs @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/// Represents an Ffi definition +#[derive(Debug, Clone)] +pub enum FfiDefinition { + // Scaffolding function exported in the library + Function(FfiFunction), + // Function type, for function pointers + FunctionType(FfiFunctionType), + // FFI struct definition + Struct(FfiStruct), +} + +/// Represents an "extern C"-style function that will be part of the FFI. +/// +/// These can't be declared explicitly in the UDL, but rather, are derived automatically +/// from the high-level interface. Each callable thing in the component API will have a +/// corresponding `FfiFunction` through which it can be invoked, and UniFFI also provides +/// some built-in `FfiFunction` helpers for use in the foreign language bindings. +#[derive(Debug, Clone)] +pub struct FfiFunction { + pub name: String, + pub is_async: bool, + pub arguments: Vec, + pub return_type: Option, + pub has_rust_call_status_arg: bool, +} + +/// Represents an "extern C"-style callback function +/// +/// These are defined in the foreign code and passed to Rust as a function pointer. +#[derive(Debug, Clone)] +pub struct FfiFunctionType { + pub name: String, + pub arguments: Vec, + pub return_type: Option, + pub has_rust_call_status_arg: bool, +} + +/// Represents a repr(C) struct +#[derive(Debug, Default, Clone)] +pub struct FfiStruct { + pub name: String, + pub fields: Vec, +} + +/// Represents a field of an [FfiStruct] +#[derive(Debug, Clone)] +pub struct FfiField { + pub name: String, + pub ty: String, +} + +/// Represents an argument to an FFI function. +/// +/// Each argument has a name and a type. +#[derive(Debug, Clone)] +pub struct FfiArgument { + pub name: String, + pub ty: String, +} + +/// Ffi function to check a checksum for an item in the interface +/// +/// Bindings generators should call each of these functions and check that they return the +/// `checksum` value. +#[derive(Debug, Clone)] +pub struct ChecksumCheck { + pub func: String, + pub checksum: u16, +} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/functions.rs b/uniffi_bindgen/src/bindings/python/gen_python/functions.rs new file mode 100644 index 0000000000..cefa17422d --- /dev/null +++ b/uniffi_bindgen/src/bindings/python/gen_python/functions.rs @@ -0,0 +1,124 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use uniffi_internal_macros::{AsCallable, AsType}; + +use super::{AsType, Type}; +use crate::interface::ir::CallableKind; + +/// Represents a standalone function. +/// +/// Each `Function` corresponds to a standalone function in the rust module, +/// and has a corresponding standalone function in the foreign language bindings. +/// +/// In the FFI, this will be a standalone function with appropriately lowered types. +#[derive(Debug, Clone, AsCallable)] +pub struct Function { + pub name: String, + pub docstring: Option, + pub callable: Callable, +} + +#[derive(Debug, Clone)] +pub struct Callable { + pub kind: CallableKind, + pub async_data: Option, + pub arguments: Vec, + pub return_type: Option, + pub throws_type: Option, + pub ffi_func: String, +} + +#[derive(Debug, Clone)] +pub struct AsyncData { + pub ffi_rust_future_poll: String, + pub ffi_rust_future_complete: String, + pub ffi_rust_future_free: String, + /// The FFI struct to pass to the completion function for callback interface methods + pub foreign_future_result_type: String, +} + +/// Represents an argument to a function/constructor/method call. +/// +/// Each argument has a name and a type, along with some optional metadata. +#[derive(Debug, Clone, AsType)] +pub struct Argument { + pub name: String, + pub ty: Type, + pub default: Option, +} + +/// Function/Method/Constructor node that can be mapped to a Callable +pub trait AsCallable { + fn as_callable(&self) -> &Callable; + + fn async_data(&self) -> Option<&AsyncData> { + self.as_callable().async_data.as_ref() + } + + fn arguments(&self) -> &[Argument] { + &self.as_callable().arguments + } + + fn return_type(&self) -> Option<&Type> { + self.as_callable().return_type.as_ref() + } + + fn throws_type(&self) -> Option<&Type> { + self.as_callable().throws_type.as_ref() + } + + fn ffi_func(&self) -> &str { + &self.as_callable().ffi_func + } + + fn is_async(&self) -> bool { + self.as_callable().async_data.is_some() + } + + fn is_sync(&self) -> bool { + !self.is_async() + } + + fn is_function(&self) -> bool { + matches!(self.as_callable().kind, CallableKind::Function) + } + + fn is_constructor(&self) -> bool { + matches!(self.as_callable().kind, CallableKind::Constructor { .. }) + } + + fn is_primary_constructor(&self) -> bool { + matches!( + self.as_callable().kind, + CallableKind::Constructor { primary: true, .. } + ) + } + + fn is_alternate_constructor(&self) -> bool { + matches!( + self.as_callable().kind, + CallableKind::Constructor { primary: false, .. } + ) + } + + fn is_method(&self) -> bool { + matches!( + self.as_callable().kind, + CallableKind::Method { .. } | CallableKind::VTableMethod { .. } + ) + } +} + +impl AsCallable for Callable { + fn as_callable(&self) -> &Callable { + self + } +} + +impl AsCallable for &T { + fn as_callable(&self) -> &Callable { + (**self).as_callable() + } +} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/interfaces.rs b/uniffi_bindgen/src/bindings/python/gen_python/interfaces.rs new file mode 100644 index 0000000000..4a3960f449 --- /dev/null +++ b/uniffi_bindgen/src/bindings/python/gen_python/interfaces.rs @@ -0,0 +1,109 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use uniffi_internal_macros::{AsCallable, AsType}; + +use super::{AsCallable, AsType, Callable, Type}; + +#[derive(Debug, Clone, AsType)] +pub struct Interface { + pub name: String, + pub protocol_name: String, + pub constructors: Vec, + pub methods: Vec, + pub had_async_constructor: bool, + pub uniffi_traits: Vec, + pub base_classes: Vec, + pub vtable: Option, + pub ffi_clone: String, + pub ffi_free: String, + pub docstring: Option, + pub self_type: Type, +} + +/// Rust traits we support generating helper methods for. +#[derive(Clone, Debug)] +pub enum UniffiTrait { + Debug { fmt: Method }, + Display { fmt: Method }, + Eq { eq: Method, ne: Method }, + Hash { hash: Method }, +} + +/// Interface implemented on the foreign side of the FFI +#[derive(Debug, Clone, AsType)] +pub struct CallbackInterface { + pub name: String, + pub methods: Vec, + pub vtable: VTable, + pub docstring: Option, + pub self_type: Type, +} + +/// VTable for a callback / trait interface +#[derive(Clone, Debug)] +pub struct VTable { + /// Initially the name of the CallbalkInterface or Interface associated with this VTable. + /// Languages can change to the name of the VTable item they generate. + pub name: String, + /// FFI type for the VTable. + pub ffi_type: String, + // Ffi function to initialize the foreign callback for trait interfaces + pub ffi_init_callback: String, + pub methods: Vec, +} + +// Represents a constructor for an object type. +// +// In the FFI, this will be a function that returns a pointer to an instance +// of the corresponding object type. +#[derive(Debug, Clone, AsCallable)] +pub struct Constructor { + pub name: String, + pub primary: bool, + pub interface: Type, + pub callable: Callable, + pub docstring: Option, +} + +/// Represents an instance method for an object type. +#[derive(Debug, Clone, AsCallable)] +pub struct Method { + pub name: String, + pub interface: Type, + pub callable: Callable, + pub docstring: Option, +} + +#[derive(Debug, Clone, AsCallable)] +pub struct VTableMethod { + pub name: String, + pub ffi_type: String, + pub default_return_value: String, + pub callable: Callable, +} + +impl Interface { + pub fn has_callback_interface(&self) -> bool { + self.vtable.is_some() + } + + pub fn primary_constructor(&self) -> Option { + self.constructors.iter().find(|c| c.primary).cloned() + } + + pub fn alternate_constructors(&self) -> impl Iterator { + self.constructors.iter().filter(|c| !c.primary) + } +} + +impl UniffiTrait { + pub fn methods(&self) -> Vec<&Method> { + match self { + UniffiTrait::Debug { fmt } | UniffiTrait::Display { fmt } => vec![fmt], + UniffiTrait::Eq { eq, ne } => vec![eq, ne], + UniffiTrait::Hash { hash } => vec![hash], + } + } +} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/miscellany.rs b/uniffi_bindgen/src/bindings/python/gen_python/miscellany.rs deleted file mode 100644 index 1ee22b95a4..0000000000 --- a/uniffi_bindgen/src/bindings/python/gen_python/miscellany.rs +++ /dev/null @@ -1,30 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use super::CodeType; - -use paste::paste; - -macro_rules! impl_code_type_for_miscellany { - ($T:ty, $canonical_name:literal) => { - paste! { - #[derive(Debug)] - pub struct $T; - - impl CodeType for $T { - fn type_label(&self) -> String { - format!("{}", $canonical_name) - } - - fn canonical_name(&self) -> String { - format!("{}", $canonical_name) - } - } - } - }; -} - -impl_code_type_for_miscellany!(TimestampCodeType, "Timestamp"); - -impl_code_type_for_miscellany!(DurationCodeType, "Duration"); diff --git a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs index 32f9674722..e515274712 100644 --- a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs +++ b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs @@ -1,114 +1,38 @@ /* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result}; +use heck::ToSnakeCase; use rinja::Template; -use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase}; -use once_cell::sync::Lazy; +use crate::backend::TemplateExpression; +use crate::interface::{ir::BindingsIr, ComponentInterface}; use serde::{Deserialize, Serialize}; -use std::borrow::Borrow; -use std::cell::RefCell; -use std::collections::{BTreeSet, HashMap, HashSet}; +use std::collections::{BTreeSet, HashMap}; use std::fmt::Debug; -use crate::backend::TemplateExpression; - -use crate::interface::*; -use crate::VisitMut; +mod convert_ir; +mod dataclasses; +mod ffi; +mod functions; +mod interfaces; +mod types; -mod callback_interface; -mod compounds; -mod custom; -mod enum_; -mod external; -mod miscellany; -mod object; -mod primitives; -mod record; +pub use convert_ir::*; +pub use dataclasses::*; +pub use ffi::*; +pub use functions::*; +pub use interfaces::*; +pub use types::*; -/// A trait tor the implementation. -trait CodeType: Debug { - /// The language specific label used to reference this type. This will be used in - /// method signatures and property declarations. - fn type_label(&self) -> String; - - /// A representation of this type label that can be used as part of another - /// identifier. e.g. `read_foo()`, or `FooInternals`. - /// - /// This is especially useful when creating specialized objects or methods to deal - /// with this type only. - fn canonical_name(&self) -> String { - self.type_label() - } - - fn literal(&self, _literal: &Literal) -> Result { - bail!("Unimplemented for {}", self.type_label()) - } - - /// Name of the FfiConverter - /// - /// This is the object that contains the lower, write, lift, and read methods for this type. - fn ffi_converter_name(&self) -> String { - format!("FfiConverter{}", self.canonical_name()) - } - - /// A list of imports that are needed if this type is in use. - /// Classes are imported exactly once. - fn imports(&self) -> Option> { - None - } - - /// Function to run at startup - fn initialization_fn(&self) -> Option { - None - } +// Generate python bindings for the given ComponentInterface, as a string. +pub fn generate_python_bindings(config: &Config, ci: &ComponentInterface) -> Result { + PythonBindingsIr::from_ci(ci.clone(), config.clone())? + .render() + .context("failed to render python bindings") } -// Taken from Python's `keyword.py` module. -static KEYWORDS: Lazy> = Lazy::new(|| { - let kwlist = vec![ - "False", - "None", - "True", - "__peg_parser__", - "and", - "as", - "assert", - "async", - "await", - "break", - "class", - "continue", - "def", - "del", - "elif", - "else", - "except", - "finally", - "for", - "from", - "global", - "if", - "import", - "in", - "is", - "lambda", - "nonlocal", - "not", - "or", - "pass", - "raise", - "return", - "try", - "while", - "with", - "yield", - ]; - HashSet::from_iter(kwlist.into_iter().map(|s| s.to_string())) -}); - // Config options to customize the generated python. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Config { @@ -119,24 +43,7 @@ pub struct Config { external_packages: HashMap, } -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct CustomTypeConfig { - // This `CustomTypeConfig` doesn't have a `type_name` like the others -- which is why we have - // separate structs rather than a shared one. - imports: Option>, - into_custom: TemplateExpression, - from_custom: TemplateExpression, -} - impl Config { - pub fn cdylib_name(&self) -> String { - if let Some(cdylib_name) = &self.cdylib_name { - cdylib_name.clone() - } else { - "uniffi".into() - } - } - /// Get the package name for a given external namespace. pub fn module_for_namespace(&self, ns: &str) -> String { let ns = ns.to_string().to_snake_case(); @@ -148,531 +55,182 @@ impl Config { } } -// Generate python bindings for the given ComponentInterface, as a string. -pub fn generate_python_bindings(config: &Config, ci: &mut ComponentInterface) -> Result { - PythonWrapper::new(config.clone(), ci) - .render() - .context("failed to render python bindings") -} - -/// A struct to record a Python import statement. -#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub enum ImportRequirement { - /// A simple module import. - Module { mod_name: String }, - /// A single symbol from a module. - Symbol { - mod_name: String, - symbol_name: String, - }, - /// A single symbol from a module with the specified local name. - SymbolAs { - mod_name: String, - symbol_name: String, - as_name: String, - }, -} - -impl ImportRequirement { - /// Render the Python import statement. - fn render(&self) -> String { - match &self { - ImportRequirement::Module { mod_name } => format!("import {mod_name}"), - ImportRequirement::Symbol { - mod_name, - symbol_name, - } => format!("from {mod_name} import {symbol_name}"), - ImportRequirement::SymbolAs { - mod_name, - symbol_name, - as_name, - } => format!("from {mod_name} import {symbol_name} as {as_name}"), - } - } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct CustomTypeConfig { + type_name: Option, + imports: Option>, + into_custom: TemplateExpression, + from_custom: TemplateExpression, } -/// Renders Python helper code for all types +/// Python Bindings Ir /// -/// This template is a bit different than others in that it stores internal state from the render -/// process. Make sure to only call `render()` once. -#[derive(Template)] -#[template(syntax = "py", escape = "none", path = "Types.py")] -pub struct TypeRenderer<'a> { - python_config: &'a Config, - ci: &'a ComponentInterface, - // Track included modules for the `include_once()` macro - include_once_names: RefCell>, - // Track imports added with the `add_import()` macro - imports: RefCell>, -} - -impl<'a> TypeRenderer<'a> { - fn new(python_config: &'a Config, ci: &'a ComponentInterface) -> Self { - Self { - python_config, - ci, - include_once_names: RefCell::new(HashSet::new()), - imports: RefCell::new(BTreeSet::new()), - } - } - - // The following methods are used by the `Types.py` macros. - - // Helper for the including a template, but only once. - // - // The first time this is called with a name it will return true, indicating that we should - // include the template. Subsequent calls will return false. - fn include_once_check(&self, name: &str) -> bool { - self.include_once_names - .borrow_mut() - .insert(name.to_string()) - } - - // Helper to add an import statement - // - // Call this inside your template to cause an import statement to be added at the top of the - // file. Imports will be sorted and de-deuped. - // - // Returns an empty string so that it can be used inside an rinja `{{ }}` block. - fn add_import(&self, name: &str) -> &str { - self.imports.borrow_mut().insert(ImportRequirement::Module { - mod_name: name.to_owned(), - }); - "" - } - - // Like add_import, but arranges for `from module import name`. - fn add_import_of(&self, mod_name: &str, name: &str) -> &str { - self.imports.borrow_mut().insert(ImportRequirement::Symbol { - mod_name: mod_name.to_owned(), - symbol_name: name.to_owned(), - }); - "" - } - - // Like add_import, but arranges for `from module import name as other`. - fn add_import_of_as(&self, mod_name: &str, symbol_name: &str, as_name: &str) -> &str { - self.imports - .borrow_mut() - .insert(ImportRequirement::SymbolAs { - mod_name: mod_name.to_owned(), - symbol_name: symbol_name.to_owned(), - as_name: as_name.to_owned(), - }); - "" - } - - // An inefficient algo to return type aliases needed for custom types - // in an order such that dependencies are in the correct order. - // Eg, if there's a custom type `Guid` -> `str` and another `GuidWrapper` -> `Guid`, - // it's important the type alias for `Guid` appears first. Fails to handle - // another level of indirection (eg, `A { builtin: C}, B { }, C { builtin: B })`) - // but that's pathological :) - fn get_custom_type_aliases(&self) -> Vec<(String, &Type)> { - let mut ordered = vec![]; - for type_ in self.ci.iter_types() { - if let Type::Custom { name, builtin, .. } = type_ { - match ordered.iter().position(|x: &(&str, &Type)| { - x.1.iter_types() - .any(|nested_type| *name == nested_type.as_codetype().type_label()) - }) { - // This 'name' appears as a builtin, so we must insert our type first. - Some(pos) => ordered.insert(pos, (name, builtin)), - // Otherwise at the end. - None => ordered.push((name, builtin)), - } - } - } - ordered - .into_iter() - .map(|(n, t)| (PythonCodeOracle.class_name(n), t)) - .collect() - } - - // Sort object types to avoid forward references; traits before everything else. - fn iter_sorted_object_types(&self) -> impl Iterator { - let mut obs: Vec<&Type> = self - .ci - .iter_types() - .filter(|t| matches!(t, Type::Object { .. })) - .collect(); - obs.sort_by_key(|t| !matches!(t, Type::Object { imp, .. } if imp.is_trait_interface())); - obs.into_iter() - } -} - -#[derive(Template)] +/// This is constructed from the general `BindingsIr` -- specializing it for Python. +/// It's then used as a Rinja template to generate the Python source +#[derive(Debug, Clone, rinja::Template)] #[template(syntax = "py", escape = "none", path = "wrapper.py")] -pub struct PythonWrapper<'a> { - ci: &'a ComponentInterface, - config: Config, - type_helper_code: String, - type_imports: BTreeSet, +pub struct PythonBindingsIr { + pub namespace: String, + pub cdylib_name: String, + pub module_docstring: Option, + pub runtimes: Runtimes, + pub globals: GlobalNodes, + pub ffi_definitions: Vec, + pub protocols: Vec, + pub type_definitions: Vec, + pub functions: Vec, + pub checksum_checks: Vec, + /// Import statements, using a BTreeset to sort/de-dupe them + pub imports: BTreeSet, + /// Names to export via the `__all__` attribute + pub exports: Vec, } -impl<'a> PythonWrapper<'a> { - pub fn new(config: Config, ci: &'a mut ComponentInterface) -> Self { - ci.visit_mut(&PythonCodeOracle); - let type_renderer = TypeRenderer::new(&config, ci); - let type_helper_code = type_renderer.render().unwrap(); - let type_imports = type_renderer.imports.into_inner(); - - Self { - config, - ci, - type_helper_code, - type_imports, - } +impl PythonBindingsIr { + pub fn from_ci(ci: ComponentInterface, config: Config) -> Result { + Self::from_general_ir(ci.try_into()?, config) } - pub fn imports(&self) -> Vec { - self.type_imports.iter().cloned().collect() + pub fn from_general_ir(ir: BindingsIr, config: Config) -> Result { + convert_bindings_ir(ir, config.clone()) } } -fn fixup_keyword(name: String) -> String { - if KEYWORDS.contains(&name) { - format!("_{name}") - } else { - name - } +/// Global definitions +/// +/// These are nodes that we always define for the bindings. +/// Putting them here means that we always have easy access them from the bindings generation code. +#[derive(Debug, Clone)] +pub struct GlobalNodes { + pub ffi_rustbuffer_alloc: String, + pub ffi_rustbuffer_reserve: String, + pub ffi_rustbuffer_free: String, + /// FFI function to check the contract version + pub ffi_uniffi_contract_version: String, + /// FFI function type for freeing a callback interface + pub callback_interface_free_type: String, + /// Always defined String, since it's used to handle Rust call errors + pub string_type: Type, + pub contract_version: u32, +} + +// These are sections of helper code that we load once +#[derive(Default, Debug, Clone)] +pub struct Runtimes { + pub async_: bool, + pub async_callback: bool, + pub callback_interface: bool, +} + +/// Protocol to define +#[derive(Debug, Clone)] +pub struct Protocol { + pub name: String, + pub base_class: String, + pub docstring: Option, + pub methods: Vec, } -#[derive(Clone, Default)] -pub struct PythonCodeOracle; - -impl PythonCodeOracle { - fn find(&self, type_: &Type) -> Box { - type_.clone().as_type().as_codetype() - } - - /// Get the idiomatic Python rendering of a class name (for enums, records, errors, etc). - fn class_name(&self, nm: &str) -> String { - fixup_keyword(nm.to_string().to_upper_camel_case()) - } - - /// Get the idiomatic Python rendering of a function name. - fn fn_name(&self, nm: &str) -> String { - fixup_keyword(nm.to_string().to_snake_case()) - } - - /// Get the idiomatic Python rendering of a variable name. - fn var_name(&self, nm: &str) -> String { - fixup_keyword(nm.to_string().to_snake_case()) - } - - /// Get the idiomatic Python rendering of an individual enum variant. - fn enum_variant_name(&self, nm: &str) -> String { - fixup_keyword(nm.to_string().to_shouty_snake_case()) - } - - /// Get the idiomatic Python rendering of an FFI callback function name - fn ffi_callback_name(&self, nm: &str) -> String { - format!("_UNIFFI_{}", nm.to_shouty_snake_case()) - } - - /// Get the idiomatic Python rendering of an FFI struct name - fn ffi_struct_name(&self, nm: &str) -> String { - // The ctypes docs use both SHOUTY_SNAKE_CASE AND UpperCamelCase for structs. Let's use - // UpperCamelCase and reserve shouting for global variables - format!("_Uniffi{}", nm.to_upper_camel_case()) - } - - fn ffi_type_label(&self, ffi_type: &FfiType) -> String { - match ffi_type { - FfiType::Int8 => "ctypes.c_int8".to_string(), - FfiType::UInt8 => "ctypes.c_uint8".to_string(), - FfiType::Int16 => "ctypes.c_int16".to_string(), - FfiType::UInt16 => "ctypes.c_uint16".to_string(), - FfiType::Int32 => "ctypes.c_int32".to_string(), - FfiType::UInt32 => "ctypes.c_uint32".to_string(), - FfiType::Int64 => "ctypes.c_int64".to_string(), - FfiType::UInt64 => "ctypes.c_uint64".to_string(), - FfiType::Float32 => "ctypes.c_float".to_string(), - FfiType::Float64 => "ctypes.c_double".to_string(), - FfiType::Handle => "ctypes.c_uint64".to_string(), - FfiType::RustArcPtr(_) => "ctypes.c_void_p".to_string(), - FfiType::RustBuffer(maybe_external) => match maybe_external { - Some(external_meta) => format!("_UniffiRustBuffer{}", external_meta.name), - None => "_UniffiRustBuffer".to_string(), - }, - FfiType::RustCallStatus => "_UniffiRustCallStatus".to_string(), - FfiType::ForeignBytes => "_UniffiForeignBytes".to_string(), - FfiType::Callback(name) => self.ffi_callback_name(name), - FfiType::Struct(name) => self.ffi_struct_name(name), - // Pointer to an `asyncio.EventLoop` instance - FfiType::Reference(inner) | FfiType::MutReference(inner) => { - format!("ctypes.POINTER({})", self.ffi_type_label(inner)) - } - FfiType::VoidPointer => "ctypes.c_void_p".to_string(), - } - } - - /// Default values for FFI types - /// - /// Used to set a default return value when returning an error - fn ffi_default_value(&self, return_type: Option<&FfiType>) -> String { - match return_type { - Some(t) => match t { - FfiType::UInt8 - | FfiType::Int8 - | FfiType::UInt16 - | FfiType::Int16 - | FfiType::UInt32 - | FfiType::Int32 - | FfiType::UInt64 - | FfiType::Int64 => "0".to_owned(), - FfiType::Float32 | FfiType::Float64 => "0.0".to_owned(), - FfiType::RustArcPtr(_) => "ctypes.c_void_p()".to_owned(), - FfiType::RustBuffer(maybe_external) => match maybe_external { - Some(external_meta) => { - format!("_UniffiRustBuffer{}.default()", external_meta.name) - } - None => "_UniffiRustBuffer.default()".to_owned(), - }, - _ => unimplemented!("FFI return type: {t:?}"), - }, - // When we need to use a value for void returns, we use a `u8` placeholder - None => "0".to_owned(), - } - } -} +pub mod filters { -impl VisitMut for PythonCodeOracle { - fn visit_record(&self, record: &mut Record) { - record.rename(self.class_name(record.name())); - } + use super::*; - fn visit_object(&self, object: &mut Object) { - object.rename(self.class_name(object.name())); - for i in object.trait_impls_mut() { - i.trait_name = self.class_name(&i.trait_name); - // should i.tr_module_path be fixed? - } + pub fn lower_fn(node: impl AsType) -> rinja::Result { + Ok(format!("{}.lower", node.as_type().ffi_converter_name)) } - fn visit_field(&self, field: &mut Field) { - field.rename(self.var_name(field.name())); + pub fn check_lower_fn(node: impl AsType) -> rinja::Result { + Ok(format!("{}.check_lower", node.as_type().ffi_converter_name)) } - fn visit_ffi_field(&self, ffi_field: &mut FfiField) { - ffi_field.rename(self.var_name(ffi_field.name())); + pub fn lift_fn(node: impl AsType) -> rinja::Result { + Ok(format!("{}.lift", node.as_type().ffi_converter_name)) } - fn visit_ffi_argument(&self, ffi_argument: &mut FfiArgument) { - ffi_argument.rename(self.class_name(ffi_argument.name())); + pub fn write_fn(node: impl AsType) -> rinja::Result { + Ok(format!("{}.write", node.as_type().ffi_converter_name)) } - fn visit_enum(&self, _is_error: bool, enum_: &mut Enum) { - enum_.rename(self.class_name(enum_.name())); + pub fn read_fn(node: impl AsType) -> rinja::Result { + Ok(format!("{}.read", node.as_type().ffi_converter_name)) } - fn visit_enum_key(&self, key: &mut String) -> String { - self.class_name(key) + pub fn return_type(callable: impl AsCallable) -> rinja::Result { + Ok(match callable.return_type() { + Some(ty) => ty.type_name.clone(), + None => "None".to_string(), + }) } - fn visit_variant(&self, is_error: bool, variant: &mut Variant) { - if is_error { - variant.rename(self.class_name(variant.name())); + /// Generate `def` or `async def` for a callable + pub fn def(callable: impl AsCallable) -> rinja::Result { + if callable.is_async() { + Ok("async def".to_string()) } else { - variant.rename(self.enum_variant_name(variant.name())); - } - } - - fn visit_type(&self, type_: &mut Type) { - // Renaming Types is a special case. We have simple types with names like - // an Object, but we also have types which have inner_types and builtin types. - // Which in turn have a different name. Therefore we pass the patterns as a - // function down to the renaming operation of the type itself, which can apply it - // to all its nested names if needed. - let name_transformer = |name: &str| self.class_name(name); - type_.rename_recursive(&name_transformer); - } - - fn visit_method(&self, method: &mut Method) { - method.rename(self.fn_name(method.name())); - } - - fn visit_argument(&self, argument: &mut Argument) { - argument.rename(self.var_name(argument.name())); - } - - fn visit_constructor(&self, constructor: &mut Constructor) { - if !constructor.is_primary_constructor() { - constructor.rename(self.fn_name(constructor.name())); + Ok("def".to_string()) } } - fn visit_function(&self, function: &mut Function) { - // Conversions for wrapper.py - //TODO: Renaming the function name in wrapper.py is not currently tested - function.rename(self.fn_name(function.name())); - } + /// Generate a comma-separated list argument names and types + pub fn arg_list(callable: impl AsCallable) -> rinja::Result { + let args = callable.arguments().iter().map(|a| { + let ty = &a.ty.type_name; + let name = &a.name; + Ok(match &a.default { + Some(_) => format!("{name}: typing.Union[object, {ty}] = _DEFAULT"), + None => format!("{name}: {ty}"), + }) + }); + let self_arg = (callable.is_method() || callable.is_primary_constructor()) + .then(|| Ok("self".to_string())); - fn visit_error_name(&self, name: &mut String) { - *name = self.class_name(name); + Ok(self_arg + .into_iter() + .chain(args) + .collect::>>()? + .join(", ")) } -} - -trait AsCodeType { - fn as_codetype(&self) -> Box; -} - -impl AsCodeType for T { - fn as_codetype(&self) -> Box { - // Map `Type` instances to a `Box` for that type. - // - // There is a companion match in `templates/Types.py` which performs a similar function for the - // template code. - // - // - When adding additional types here, make sure to also add a match arm to the `Types.py` template. - // - To keep things manageable, let's try to limit ourselves to these 2 mega-matches - match self.as_type() { - Type::UInt8 => Box::new(primitives::UInt8CodeType), - Type::Int8 => Box::new(primitives::Int8CodeType), - Type::UInt16 => Box::new(primitives::UInt16CodeType), - Type::Int16 => Box::new(primitives::Int16CodeType), - Type::UInt32 => Box::new(primitives::UInt32CodeType), - Type::Int32 => Box::new(primitives::Int32CodeType), - Type::UInt64 => Box::new(primitives::UInt64CodeType), - Type::Int64 => Box::new(primitives::Int64CodeType), - Type::Float32 => Box::new(primitives::Float32CodeType), - Type::Float64 => Box::new(primitives::Float64CodeType), - Type::Boolean => Box::new(primitives::BooleanCodeType), - Type::String => Box::new(primitives::StringCodeType), - Type::Bytes => Box::new(primitives::BytesCodeType), - Type::Timestamp => Box::new(miscellany::TimestampCodeType), - Type::Duration => Box::new(miscellany::DurationCodeType), - - Type::Enum { name, .. } => Box::new(enum_::EnumCodeType::new(name)), - Type::Object { name, .. } => Box::new(object::ObjectCodeType::new(name)), - Type::Record { name, .. } => Box::new(record::RecordCodeType::new(name)), - Type::CallbackInterface { name, .. } => { - Box::new(callback_interface::CallbackInterfaceCodeType::new(name)) - } - Type::Optional { inner_type } => { - Box::new(compounds::OptionalCodeType::new(*inner_type)) - } - Type::Sequence { inner_type } => { - Box::new(compounds::SequenceCodeType::new(*inner_type)) + /// Get the FFI converter for a throws type + /// + /// Returns "None" if there isn't a throws type. + pub fn error_ffi_converter(callable: impl AsCallable) -> rinja::Result { + Ok(match callable.throws_type() { + Some(error_type) => { + let ffi_converter_name = &error_type.ffi_converter_name; + match &error_type.kind { + uniffi_meta::Type::Object { .. } => format!("{ffi_converter_name}__as_error"), + _ => ffi_converter_name.clone(), + } } - Type::Map { - key_type, - value_type, - } => Box::new(compounds::MapCodeType::new(*key_type, *value_type)), - Type::External { name, .. } => Box::new(external::ExternalCodeType::new(name)), - Type::Custom { name, .. } => Box::new(custom::CustomCodeType::new(name)), - } + None => "None".to_string(), + }) } -} -pub mod filters { - use crate::backend::filters::to_rinja_error; - - use super::*; - - pub(super) fn type_name(as_ct: &impl AsCodeType) -> Result { - Ok(as_ct.as_codetype().type_label()) - } - - pub(super) fn ffi_converter_name(as_ct: &impl AsCodeType) -> Result { - Ok(String::from("_Uniffi") + &as_ct.as_codetype().ffi_converter_name()[3..]) - } - - pub(super) fn canonical_name(as_ct: &impl AsCodeType) -> Result { - Ok(as_ct.as_codetype().canonical_name()) - } - - pub(super) fn lift_fn(as_ct: &impl AsCodeType) -> Result { - Ok(format!("{}.lift", ffi_converter_name(as_ct)?)) - } - - pub(super) fn check_lower_fn(as_ct: &impl AsCodeType) -> Result { - Ok(format!("{}.check_lower", ffi_converter_name(as_ct)?)) - } - - pub(super) fn lower_fn(as_ct: &impl AsCodeType) -> Result { - Ok(format!("{}.lower", ffi_converter_name(as_ct)?)) - } - - pub(super) fn read_fn(as_ct: &impl AsCodeType) -> Result { - Ok(format!("{}.read", ffi_converter_name(as_ct)?)) - } - - pub(super) fn write_fn(as_ct: &impl AsCodeType) -> Result { - Ok(format!("{}.write", ffi_converter_name(as_ct)?)) - } - - pub(super) fn literal_py( - literal: &Literal, - as_ct: &impl AsCodeType, - ) -> Result { - as_ct - .as_codetype() - .literal(literal) - .map_err(|e| to_rinja_error(&e)) - } - - // Get the idiomatic Python rendering of an individual enum variant's discriminant - pub fn variant_discr_literal(e: &Enum, index: &usize) -> Result { - let literal = e - .variant_discr(*index) - .context("invalid index") - .map_err(|e| to_rinja_error(&e))?; - Type::UInt64 - .as_codetype() - .literal(&literal) - .map_err(|e| to_rinja_error(&e)) - } - - pub fn ffi_type_name(type_: &FfiType) -> Result { - Ok(PythonCodeOracle.ffi_type_label(type_)) - } - - pub fn ffi_default_value(return_type: Option) -> Result { - Ok(PythonCodeOracle.ffi_default_value(return_type.as_ref())) - } - - /// Get the idiomatic Python rendering of an FFI callback function name - pub fn ffi_callback_name(nm: &str) -> Result { - Ok(PythonCodeOracle.ffi_callback_name(nm)) - } - - /// Get the idiomatic Python rendering of an FFI struct name - pub fn ffi_struct_name(nm: &str) -> Result { - Ok(PythonCodeOracle.ffi_struct_name(nm)) - } - - /// Get the idiomatic Python rendering of docstring - pub fn docstring(docstring: &str, spaces: &i32) -> Result { - let docstring = textwrap::dedent(docstring); - // Escape triple quotes to avoid syntax error - let escaped = docstring.replace(r#"""""#, r#"\"\"\""#); - - let wrapped = format!("\"\"\"\n{escaped}\n\"\"\""); - - let spaces = usize::try_from(*spaces).unwrap_or_default(); - Ok(textwrap::indent(&wrapped, &" ".repeat(spaces))) - } -} - -#[cfg(test)] -mod tests { - #[test] - fn test_docstring_escape() { - let docstring = r#""""This is a docstring beginning with triple quotes. -Contains "quotes" in it. -It also has a triple quote: """ -And a even longer quote: """"""#; - - let expected = r#"""" -\"\"\"This is a docstring beginning with triple quotes. -Contains "quotes" in it. -It also has a triple quote: \"\"\" -And a even longer quote: \"\"\""" -""""#; - - assert_eq!(super::filters::docstring(docstring, &0).unwrap(), expected); + /// Indent a docstring + /// + /// For Some values will indent each line, except the first by `spaces`. + /// For None, it will return the empty string. + /// + /// This gets both cases right with template code that looks like this: + /// + /// ```python + /// {{ meth.docstring|docindent(4) }} + /// fn {{ meth.name }}(...) + /// ``` + pub fn docindent(docstring: &Option, spaces: usize) -> rinja::Result { + Ok(match docstring { + None => "".to_string(), + Some(docstring) => { + let mut output = String::new(); + let leading_space = " ".repeat(spaces); + for line in docstring.split('\n') { + output.push_str(line); + output.push('\n'); + output.push_str(&leading_space); + } + output + } + }) } } diff --git a/uniffi_bindgen/src/bindings/python/gen_python/object.rs b/uniffi_bindgen/src/bindings/python/gen_python/object.rs deleted file mode 100644 index 092d09b61a..0000000000 --- a/uniffi_bindgen/src/bindings/python/gen_python/object.rs +++ /dev/null @@ -1,26 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use super::CodeType; - -#[derive(Debug)] -pub struct ObjectCodeType { - id: String, -} - -impl ObjectCodeType { - pub fn new(id: String) -> Self { - Self { id } - } -} - -impl CodeType for ObjectCodeType { - fn type_label(&self) -> String { - super::PythonCodeOracle.class_name(&self.id) - } - - fn canonical_name(&self) -> String { - format!("Type{}", self.type_label()) - } -} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/primitives.rs b/uniffi_bindgen/src/bindings/python/gen_python/primitives.rs deleted file mode 100644 index 768f5fea7a..0000000000 --- a/uniffi_bindgen/src/bindings/python/gen_python/primitives.rs +++ /dev/null @@ -1,70 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use super::CodeType; -use crate::{backend::Literal, bail, interface::Radix, Result}; -use paste::paste; - -fn render_literal(literal: &Literal) -> Result { - Ok(match literal { - Literal::Boolean(v) => { - if *v { - "True".into() - } else { - "False".into() - } - } - Literal::String(s) => format!("\"{s}\""), - // https://docs.python.org/3/reference/lexical_analysis.html#integer-literals - Literal::Int(i, radix, _) => match radix { - Radix::Octal => format!("int(0o{i:o})"), - Radix::Decimal => format!("{i}"), - Radix::Hexadecimal => format!("{i:#x}"), - }, - Literal::UInt(i, radix, _) => match radix { - Radix::Octal => format!("0o{i:o}"), - Radix::Decimal => format!("{i}"), - Radix::Hexadecimal => format!("{i:#x}"), - }, - Literal::Float(string, _type_) => string.clone(), - - _ => bail!("Invalid literal {literal:?}"), - }) -} - -macro_rules! impl_code_type_for_primitive { - ($T:ty, $python_name:literal, $canonical_name:literal) => { - paste! { - #[derive(Debug)] - pub struct $T; - impl CodeType for $T { - fn type_label(&self) -> String { - $python_name.into() - } - - fn canonical_name(&self) -> String { - $canonical_name.into() - } - - fn literal(&self, literal: &Literal) -> Result { - render_literal(&literal) - } - } - } - }; -} - -impl_code_type_for_primitive!(BooleanCodeType, "bool", "Bool"); -impl_code_type_for_primitive!(StringCodeType, "str", "String"); -impl_code_type_for_primitive!(BytesCodeType, "bytes", "Bytes"); -impl_code_type_for_primitive!(Int8CodeType, "int", "Int8"); -impl_code_type_for_primitive!(Int16CodeType, "int", "Int16"); -impl_code_type_for_primitive!(Int32CodeType, "int", "Int32"); -impl_code_type_for_primitive!(Int64CodeType, "int", "Int64"); -impl_code_type_for_primitive!(UInt8CodeType, "int", "UInt8"); -impl_code_type_for_primitive!(UInt16CodeType, "int", "UInt16"); -impl_code_type_for_primitive!(UInt32CodeType, "int", "UInt32"); -impl_code_type_for_primitive!(UInt64CodeType, "int", "UInt64"); -impl_code_type_for_primitive!(Float32CodeType, "float", "Float"); -impl_code_type_for_primitive!(Float64CodeType, "float", "Double"); diff --git a/uniffi_bindgen/src/bindings/python/gen_python/record.rs b/uniffi_bindgen/src/bindings/python/gen_python/record.rs deleted file mode 100644 index ef14343650..0000000000 --- a/uniffi_bindgen/src/bindings/python/gen_python/record.rs +++ /dev/null @@ -1,26 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use super::CodeType; - -#[derive(Debug)] -pub struct RecordCodeType { - id: String, -} - -impl RecordCodeType { - pub fn new(id: String) -> Self { - Self { id } - } -} - -impl CodeType for RecordCodeType { - fn type_label(&self) -> String { - super::PythonCodeOracle.class_name(&self.id) - } - - fn canonical_name(&self) -> String { - format!("Type{}", self.type_label()) - } -} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/types.rs b/uniffi_bindgen/src/bindings/python/gen_python/types.rs new file mode 100644 index 0000000000..a76b139839 --- /dev/null +++ b/uniffi_bindgen/src/bindings/python/gen_python/types.rs @@ -0,0 +1,101 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use uniffi_internal_macros::AsType; +pub use uniffi_meta::ExternalKind; + +use super::*; + +/// Type definitions +/// +/// Foreign bindings will typically define an FFI converter for these and a class for types like +/// Record, Enum, and Object. +#[derive(Debug, Clone, AsType)] +pub enum TypeDefinition { + /// Builtin types from the general IR are split into several categories: + /// - Simple -- these don't contain any other types + /// - Optional, Sequence, and Map -- these need a type definition since Python doesn't have + /// generics. We need to define an FFI converter for each one used in the + /// interface. + Simple(Type), + Optional(OptionalType), + Sequence(SequenceType), + Map(MapType), + Record(Record), + Enum(Enum), + Interface(Interface), + CallbackInterface(CallbackInterface), + Custom(CustomType), + External(ExternalType), +} + +/// ComponentInterface node that stores a type. +/// +/// Nodes like [Record] and [Interface] that represent a type definition, will store one of these in +/// as a `self_type` field +#[derive(Debug, Clone)] +pub struct Type { + pub kind: uniffi_meta::Type, + pub type_name: String, + pub ffi_converter_name: String, + /// FFI type for this type + pub ffi_type: String, + /// Was this type used as the error half of a result for any function/method/constructor? + pub is_used_as_error: bool, +} + +#[derive(Debug, Clone, AsType)] +pub struct OptionalType { + pub inner: Type, + pub self_type: Type, +} + +#[derive(Debug, Clone, AsType)] +pub struct SequenceType { + pub inner: Type, + pub self_type: Type, +} + +#[derive(Debug, Clone, AsType)] +pub struct MapType { + pub key: Type, + pub value: Type, + pub self_type: Type, +} + +#[derive(Debug, Clone, AsType)] +pub struct CustomType { + pub name: String, + pub config: Option, + pub builtin: Type, + pub self_type: Type, +} + +#[derive(Debug, Clone, AsType)] +pub struct ExternalType { + pub name: String, + pub namespace: String, + pub kind: ExternalKind, + pub self_type: Type, +} + +/// Trait for nodes that are associated with a type +pub trait AsType { + fn as_type(&self) -> &Type; + + fn is_used_as_error(&self) -> bool { + self.as_type().is_used_as_error + } +} + +impl AsType for Type { + fn as_type(&self) -> &Type { + self + } +} +impl AsType for &T { + fn as_type(&self) -> &Type { + (*self).as_type() + } +} diff --git a/uniffi_bindgen/src/bindings/python/mod.rs b/uniffi_bindgen/src/bindings/python/mod.rs index ca598baf56..428fa2fdfa 100644 --- a/uniffi_bindgen/src/bindings/python/mod.rs +++ b/uniffi_bindgen/src/bindings/python/mod.rs @@ -10,6 +10,7 @@ use fs_err as fs; mod gen_python; #[cfg(feature = "bindgen-tests")] pub mod test; + use crate::{BindingGenerator, Component, GenerationSettings}; use gen_python::{generate_python_bindings, Config}; @@ -51,7 +52,7 @@ impl BindingGenerator for PythonBindingGenerator { ) -> Result<()> { for Component { ci, config, .. } in components { let py_file = settings.out_dir.join(format!("{}.py", ci.namespace())); - fs::write(&py_file, generate_python_bindings(config, &mut ci.clone())?)?; + fs::write(&py_file, generate_python_bindings(config, ci)?)?; if settings.try_format_code { if let Err(e) = Command::new("yapf").arg(&py_file).output() { diff --git a/uniffi_bindgen/src/bindings/python/templates/Async.py b/uniffi_bindgen/src/bindings/python/templates/Async.py index c80ac0c3f9..da618aa8db 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Async.py +++ b/uniffi_bindgen/src/bindings/python/templates/Async.py @@ -31,7 +31,7 @@ def _uniffi_get_event_loop(): # Continuation callback for async functions # lift the return value or error and resolve the future, causing the async function to resume. -@_UNIFFI_RUST_FUTURE_CONTINUATION_CALLBACK +@_UNIFFI_FN_RUST_FUTURE_CONTINUATION_CALLBACK def _uniffi_continuation_callback(future_ptr, poll_code): (eventloop, future) = _UniffiContinuationHandleMap.remove(future_ptr) eventloop.call_soon_threadsafe(_uniffi_set_future_result, future, poll_code) @@ -40,7 +40,7 @@ def _uniffi_set_future_result(future, poll_code): if not future.cancelled(): future.set_result(poll_code) -async def _uniffi_rust_call_async(rust_future, ffi_poll, ffi_complete, ffi_free, lift_func, error_ffi_converter): +async def _uniffi_rust_call_async(rust_future, ffi_poll, ffi_complete, ffi_free, error_ffi_converter): try: eventloop = _uniffi_get_event_loop() @@ -56,13 +56,11 @@ async def _uniffi_rust_call_async(rust_future, ffi_poll, ffi_complete, ffi_free, if poll_code == _UNIFFI_RUST_FUTURE_POLL_READY: break - return lift_func( - _uniffi_rust_call_with_error(error_ffi_converter, ffi_complete, rust_future) - ) + return _uniffi_rust_call_with_error(error_ffi_converter, ffi_complete, rust_future) finally: ffi_free(rust_future) -{%- if ci.has_async_callback_interface_definition() %} +{%- if runtimes.async_callback %} def _uniffi_trait_interface_call_async(make_call, handle_success, handle_error): async def make_call_and_call_callback(): try: @@ -72,12 +70,12 @@ async def make_call_and_call_callback(): traceback.print_exc(file=sys.stderr) handle_error( _UniffiRustCallStatus.CALL_UNEXPECTED_ERROR, - {{ Type::String.borrow()|lower_fn }}(repr(e)), + {{ globals.string_type|lower_fn }}(repr(e)), ) eventloop = _uniffi_get_event_loop() task = asyncio.run_coroutine_threadsafe(make_call_and_call_callback(), eventloop) handle = _UNIFFI_FOREIGN_FUTURE_HANDLE_MAP.insert((eventloop, task)) - return _UniffiForeignFuture(handle, _uniffi_foreign_future_free) + return _UniffiStructForeignFuture(handle, _uniffi_foreign_future_free) def _uniffi_trait_interface_call_async_with_error(make_call, handle_success, handle_error, error_type, lower_error): async def make_call_and_call_callback(): @@ -94,16 +92,16 @@ async def make_call_and_call_callback(): traceback.print_exc(file=sys.stderr) handle_error( _UniffiRustCallStatus.CALL_UNEXPECTED_ERROR, - {{ Type::String.borrow()|lower_fn }}(repr(e)), + {{ globals.string_type|lower_fn }}(repr(e)), ) eventloop = _uniffi_get_event_loop() task = asyncio.run_coroutine_threadsafe(make_call_and_call_callback(), eventloop) handle = _UNIFFI_FOREIGN_FUTURE_HANDLE_MAP.insert((eventloop, task)) - return _UniffiForeignFuture(handle, _uniffi_foreign_future_free) + return _UniffiStructForeignFuture(handle, _uniffi_foreign_future_free) _UNIFFI_FOREIGN_FUTURE_HANDLE_MAP = _UniffiHandleMap() -@_UNIFFI_FOREIGN_FUTURE_FREE +@_UNIFFI_FN_FOREIGN_FUTURE_FREE def _uniffi_foreign_future_free(handle): (eventloop, task) = _UNIFFI_FOREIGN_FUTURE_HANDLE_MAP.remove(handle) eventloop.call_soon(_uniffi_foreign_future_do_free, task) diff --git a/uniffi_bindgen/src/bindings/python/templates/BooleanHelper.py b/uniffi_bindgen/src/bindings/python/templates/BooleanHelper.py index 3f8c5d1d4d..6bcceb8e7f 100644 --- a/uniffi_bindgen/src/bindings/python/templates/BooleanHelper.py +++ b/uniffi_bindgen/src/bindings/python/templates/BooleanHelper.py @@ -1,4 +1,4 @@ -class _UniffiConverterBool: +class {{ ffi_converter_name }}: @classmethod def check_lower(cls, value): return not not value diff --git a/uniffi_bindgen/src/bindings/python/templates/BytesHelper.py b/uniffi_bindgen/src/bindings/python/templates/BytesHelper.py index 4d09531322..7051f86fb0 100644 --- a/uniffi_bindgen/src/bindings/python/templates/BytesHelper.py +++ b/uniffi_bindgen/src/bindings/python/templates/BytesHelper.py @@ -1,4 +1,4 @@ -class _UniffiConverterBytes(_UniffiConverterRustBuffer): +class {{ ffi_converter_name }}(_UniffiConverterRustBuffer): @staticmethod def read(buf): size = buf.read_i32() diff --git a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py index 21e13d9e0e..855f6d90bc 100644 --- a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py @@ -1,14 +1,5 @@ -{%- let cbi = ci.get_callback_interface_definition(name).unwrap() %} -{%- let ffi_init_callback = cbi.ffi_init_callback() %} -{%- let protocol_name = type_name.clone() %} -{%- let protocol_base_class = "typing.Protocol" %} -{%- let protocol_docstring = cbi.docstring() %} -{%- let vtable = cbi.vtable() %} -{%- let methods = cbi.methods() %} -{%- let vtable_methods = cbi.vtable_methods() %} - -{% include "Protocol.py" %} -{% include "CallbackInterfaceImpl.py" %} +{%- let vtable = cbi.vtable %} +{% include "VTable.py" %} # The _UniffiConverter which transforms the Callbacks in to Handles to pass to Rust. {{ ffi_converter_name }} = _UniffiCallbackInterfaceFfiConverter() diff --git a/uniffi_bindgen/src/bindings/python/templates/CustomType.py b/uniffi_bindgen/src/bindings/python/templates/CustomType.py index 3f7c88fcc4..cbdf85bc4d 100644 --- a/uniffi_bindgen/src/bindings/python/templates/CustomType.py +++ b/uniffi_bindgen/src/bindings/python/templates/CustomType.py @@ -1,59 +1,57 @@ -{%- match python_config.custom_types.get(name.as_str()) %} +{%- match custom_type.config %} {% when None %} {#- No custom type config, just forward all methods to our builtin type #} -class _UniffiConverterType{{ name }}: +{{ custom_type.name }} = {{ custom_type.builtin.type_name }} + +class {{ ffi_converter_name }}: @staticmethod def write(value, buf): - {{ builtin|ffi_converter_name }}.write(value, buf) + {{ custom_type.builtin.ffi_converter_name }}.write(value, buf) @staticmethod def read(buf): - return {{ builtin|ffi_converter_name }}.read(buf) + return {{ custom_type.builtin.ffi_converter_name }}.read(buf) @staticmethod def lift(value): - return {{ builtin|ffi_converter_name }}.lift(value) + return {{ custom_type.builtin.ffi_converter_name }}.lift(value) @staticmethod def check_lower(value): - return {{ builtin|ffi_converter_name }}.check_lower(value) + return {{ custom_type.builtin.ffi_converter_name }}.check_lower(value) @staticmethod def lower(value): - return {{ builtin|ffi_converter_name }}.lower(value) + return {{ custom_type.builtin.ffi_converter_name }}.lower(value) {%- when Some(config) %} - -{%- if let Some(imports) = config.imports %} -{%- for import_name in imports %} -{{ self.add_import(import_name) }} -{%- endfor %} +{%- if let Some(type_name) = config.type_name %} +{{ custom_type.name }} = {{ type_name }} {%- endif %} - {#- Custom type config supplied, use it to convert the builtin type #} -class _UniffiConverterType{{ name }}: +class {{ ffi_converter_name }}: @staticmethod def write(value, buf): builtin_value = {{ config.from_custom.render("value") }} - {{ builtin|write_fn }}(builtin_value, buf) + {{ custom_type.builtin|write_fn }}(builtin_value, buf) @staticmethod def read(buf): - builtin_value = {{ builtin|read_fn }}(buf) + builtin_value = {{ custom_type.builtin|read_fn }}(buf) return {{ config.into_custom.render("builtin_value") }} @staticmethod def lift(value): - builtin_value = {{ builtin|lift_fn }}(value) + builtin_value = {{ custom_type.builtin|lift_fn }}(value) return {{ config.into_custom.render("builtin_value") }} @staticmethod def check_lower(value): builtin_value = {{ config.from_custom.render("value") }} - return {{ builtin|check_lower_fn }}(builtin_value) + return {{ custom_type.builtin|check_lower_fn }}(builtin_value) @staticmethod def lower(value): builtin_value = {{ config.from_custom.render("value") }} - return {{ builtin|lower_fn }}(builtin_value) + return {{ custom_type.builtin|lower_fn }}(builtin_value) {%- endmatch %} diff --git a/uniffi_bindgen/src/bindings/python/templates/DurationHelper.py b/uniffi_bindgen/src/bindings/python/templates/DurationHelper.py index ecb035b7f4..bbecc887e7 100644 --- a/uniffi_bindgen/src/bindings/python/templates/DurationHelper.py +++ b/uniffi_bindgen/src/bindings/python/templates/DurationHelper.py @@ -4,7 +4,7 @@ # There is a loss of precision when converting from Rust durations, # which are accurate to the nanosecond, # to Python durations, which are only accurate to the microsecond. -class _UniffiConverterDuration(_UniffiConverterRustBuffer): +class {{ ffi_converter_name }}(_UniffiConverterRustBuffer): @staticmethod def read(buf): seconds = buf.read_u64() diff --git a/uniffi_bindgen/src/bindings/python/templates/EnumTemplate.py b/uniffi_bindgen/src/bindings/python/templates/EnumTemplate.py index f007313353..a9e5ad17de 100644 --- a/uniffi_bindgen/src/bindings/python/templates/EnumTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/EnumTemplate.py @@ -6,31 +6,30 @@ #} {% if e.is_flat() %} -class {{ type_name }}(enum.Enum): - {%- call py::docstring(e, 4) %} - {%- for variant in e.variants() %} - {{ variant.name() }} = {{ e|variant_discr_literal(loop.index0) }} - {%- call py::docstring(variant, 4) %} +class {{ e.name }}(enum.Enum): + {{ e.docstring|docindent(4) -}} + {%- for variant in e.variants %} + {{ variant.name }} = {{ variant.discr }} + {{ variant.docstring|docindent(4) -}} {% endfor %} {% else %} -class {{ type_name }}: - {%- call py::docstring(e, 4) %} +class {{ e.name }}: + {{ e.docstring|docindent(4) -}} def __init__(self): - raise RuntimeError("{{ type_name }} cannot be instantiated directly") + raise RuntimeError("{{ e.name }} cannot be instantiated directly") # Each enum variant is a nested class of the enum itself. - {% for variant in e.variants() -%} - class {{ variant.name() }}: - {%- call py::docstring(variant, 8) %} - + {% for variant in e.variants -%} + class {{ variant.name }}: + {{ variant.docstring|docindent(8) -}} {%- if variant.has_nameless_fields() %} def __init__(self, *values): - if len(values) != {{ variant.fields().len() }}: - raise TypeError(f"Expected {{ variant.fields().len() }} arguments, found {len(values)}") - {%- for field in variant.fields() %} - if not isinstance(values[{{ loop.index0 }}], {{ field|type_name }}): - raise TypeError(f"unexpected type for tuple element {{ loop.index0 }} - expected '{{ field|type_name }}', got '{type(values[{{ loop.index0 }}])}'") + if len(values) != {{ variant.fields.len() }}: + raise TypeError(f"Expected {{ variant.fields.len() }} arguments, found {len(values)}") + {%- for field in variant.fields %} + if not isinstance(values[{{ loop.index0 }}], {{ field.ty.type_name }}): + raise TypeError(f"unexpected type for tuple element {{ loop.index0 }} - expected '{{ field.ty.type_name }}', got '{type(values[{{ loop.index0 }}])}'") {%- endfor %} self._values = values @@ -38,36 +37,36 @@ def __getitem__(self, index): return self._values[index] def __str__(self): - return f"{{ type_name }}.{{ variant.name() }}{self._values!r}" + return f"{{ e.name }}.{{ variant.name }}{self._values!r}" def __eq__(self, other): - if not other.is_{{ variant.name() }}(): + if not other.is_{{ variant.name }}(): return False return self._values == other._values {%- else -%} - {%- for field in variant.fields() %} - {{ field.name() }}: "{{ field|type_name }}" - {%- call py::docstring(field, 8) %} + {%- for field in variant.fields %} + {{ field.name }}: "{{ field.ty.type_name }}" + {{ field.docstring|docindent(8) -}} {%- endfor %} - def __init__(self,{% for field in variant.fields() %}{{ field.name() }}: "{{- field|type_name }}"{% if loop.last %}{% else %}, {% endif %}{% endfor %}): + def __init__(self,{% for field in variant.fields %}{{ field.name }}: "{{- field.ty.type_name }}"{% if loop.last %}{% else %}, {% endif %}{% endfor %}): {%- if variant.has_fields() %} - {%- for field in variant.fields() %} - self.{{ field.name() }} = {{ field.name() }} + {%- for field in variant.fields %} + self.{{ field.name }} = {{ field.name }} {%- endfor %} {%- else %} pass {%- endif %} def __str__(self): - return "{{ type_name }}.{{ variant.name() }}({% for field in variant.fields() %}{{ field.name() }}={}{% if loop.last %}{% else %}, {% endif %}{% endfor %})".format({% for field in variant.fields() %}self.{{ field.name() }}{% if loop.last %}{% else %}, {% endif %}{% endfor %}) + return "{{ e.name }}.{{ variant.name }}({% for field in variant.fields %}{{ field.name }}={}{% if loop.last %}{% else %}, {% endif %}{% endfor %})".format({% for field in variant.fields %}self.{{ field.name }}{% if loop.last %}{% else %}, {% endif %}{% endfor %}) def __eq__(self, other): - if not other.is_{{ variant.name() }}(): + if not other.is_{{ variant.name }}(): return False - {%- for field in variant.fields() %} - if self.{{ field.name() }} != other.{{ field.name() }}: + {%- for field in variant.fields %} + if self.{{ field.name }} != other.{{ field.name }}: return False {%- endfor %} return True @@ -76,22 +75,22 @@ def __eq__(self, other): # For each variant, we have `is_NAME` and `is_name` methods for easily checking # whether an instance is that variant. - {% for variant in e.variants() -%} - def is_{{ variant.name() }}(self) -> bool: - return isinstance(self, {{ type_name }}.{{ variant.name() }}) + {% for variant in e.variants -%} + def is_{{ variant.name }}(self) -> bool: + return isinstance(self, {{ e.name }}.{{ variant.name }}) {#- We used to think we used `is_NAME` but did `is_name` instead. In #2270 we decided to do both. #} - {%- if variant.name() != variant.name().to_snake_case() %} - def is_{{ variant.name().to_snake_case() }}(self) -> bool: - return isinstance(self, {{ type_name }}.{{ variant.name() }}) + {%- if variant.name != variant.name.to_snake_case() %} + def is_{{ variant.name.to_snake_case() }}(self) -> bool: + return isinstance(self, {{ e.name }}.{{ variant.name }}) {%- endif %} {% endfor %} # Now, a little trick - we make each nested variant class be a subclass of the main # enum class, so that method calls and instance checks etc will work intuitively. # We might be able to do this a little more neatly with a metaclass, but this'll do. -{% for variant in e.variants() -%} -{{ type_name }}.{{ variant.name() }} = type("{{ type_name }}.{{ variant.name() }}", ({{ type_name }}.{{variant.name()}}, {{ type_name }},), {}) # type: ignore +{% for variant in e.variants -%} +{{ e.name }}.{{ variant.name }} = type("{{ e.name }}.{{ variant.name }}", ({{ e.name }}.{{variant.name}}, {{ e.name }},), {}) # type: ignore {% endfor %} {% endif %} @@ -101,13 +100,13 @@ class {{ ffi_converter_name }}(_UniffiConverterRustBuffer): def read(buf): variant = buf.read_i32() - {%- for variant in e.variants() %} + {%- for variant in e.variants %} if variant == {{ loop.index }}: {%- if e.is_flat() %} - return {{ type_name }}.{{variant.name()}} + return {{ e.name }}.{{variant.name}} {%- else %} - return {{ type_name }}.{{variant.name()}}( - {%- for field in variant.fields() %} + return {{ e.name }}.{{variant.name}}( + {%- for field in variant.fields %} {{ field|read_fn }}(buf), {%- endfor %} ) @@ -117,20 +116,20 @@ def read(buf): @staticmethod def check_lower(value): - {%- if e.variants().is_empty() %} + {%- if e.variants.is_empty() %} pass {%- else %} - {%- for variant in e.variants() %} + {%- for variant in e.variants %} {%- if e.is_flat() %} - if value == {{ type_name }}.{{ variant.name() }}: + if value == {{ e.name }}.{{ variant.name }}: {%- else %} - if value.is_{{ variant.name() }}(): + if value.is_{{ variant.name }}(): {%- endif %} - {%- for field in variant.fields() %} + {%- for field in variant.fields %} {%- if variant.has_nameless_fields() %} {{ field|check_lower_fn }}(value._values[{{ loop.index0 }}]) {%- else %} - {{ field|check_lower_fn }}(value.{{ field.name() }}) + {{ field|check_lower_fn }}(value.{{ field.name }}) {%- endif %} {%- endfor %} return @@ -140,18 +139,18 @@ def check_lower(value): @staticmethod def write(value, buf): - {%- for variant in e.variants() %} + {%- for variant in e.variants %} {%- if e.is_flat() %} - if value == {{ type_name }}.{{ variant.name() }}: + if value == {{ e.name }}.{{ variant.name }}: buf.write_i32({{ loop.index }}) {%- else %} - if value.is_{{ variant.name() }}(): + if value.is_{{ variant.name }}(): buf.write_i32({{ loop.index }}) - {%- for field in variant.fields() %} + {%- for field in variant.fields %} {%- if variant.has_nameless_fields() %} {{ field|write_fn }}(value._values[{{ loop.index0 }}], buf) {%- else %} - {{ field|write_fn }}(value.{{ field.name() }}, buf) + {{ field|write_fn }}(value.{{ field.name }}, buf) {%- endif %} {%- endfor %} {%- endif %} diff --git a/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py index 9d3b30831d..3e4587f8d4 100644 --- a/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py @@ -1,36 +1,32 @@ -# {{ type_name }} +# {{ e.name }} # We want to define each variant as a nested class that's also a subclass, # which is tricky in Python. To accomplish this we're going to create each # class separately, then manually add the child classes to the base class's # __dict__. All of this happens in dummy class to avoid polluting the module # namespace. -class {{ type_name }}(Exception): - {%- call py::docstring(e, 4) %} +class {{ e.name }}(Exception): + {{ e.docstring|docindent(4) }} pass -_UniffiTemp{{ type_name }} = {{ type_name }} +_UniffiTemp{{ e.name }} = {{ e.name }} -class {{ type_name }}: # type: ignore - {%- call py::docstring(e, 4) %} - {%- for variant in e.variants() -%} - {%- let variant_type_name = variant.name() -%} +class {{ e.name }}: # type: ignore + {%- for variant in e.variants -%} {%- if e.is_flat() %} - class {{ variant_type_name }}(_UniffiTemp{{ type_name }}): - {%- call py::docstring(variant, 8) %} - + class {{ variant.name }}(_UniffiTemp{{ e.name }}): + {{ variant.docstring|docindent(8) -}} def __repr__(self): - return "{{ type_name }}.{{ variant_type_name }}({})".format(repr(str(self))) + return "{{ e.name }}.{{ variant.name }}({})".format(repr(str(self))) {%- else %} - class {{ variant_type_name }}(_UniffiTemp{{ type_name }}): - {%- call py::docstring(variant, 8) %} - + class {{ variant.name }}(_UniffiTemp{{ e.name }}): + {{ variant.docstring|docindent(8) -}} {%- if variant.has_nameless_fields() %} def __init__(self, *values): - if len(values) != {{ variant.fields().len() }}: - raise TypeError(f"Expected {{ variant.fields().len() }} arguments, found {len(values)}") - {%- for field in variant.fields() %} - if not isinstance(values[{{ loop.index0 }}], {{ field|type_name }}): - raise TypeError(f"unexpected type for tuple element {{ loop.index0 }} - expected '{{ field|type_name }}', got '{type(values[{{ loop.index0 }}])}'") + if len(values) != {{ variant.fields.len() }}: + raise TypeError(f"Expected {{ variant.fields.len() }} arguments, found {len(values)}") + {%- for field in variant.fields %} + if not isinstance(values[{{ loop.index0 }}], {{ field.ty.type_name }}): + raise TypeError(f"unexpected type for tuple element {{ loop.index0 }} - expected '{{ field.name }}', got '{type(values[{{ loop.index0 }}])}'") {%- endfor %} super().__init__(", ".join(map(repr, values))) self._values = values @@ -39,15 +35,15 @@ def __getitem__(self, index): return self._values[index] {%- else %} - def __init__(self{% for field in variant.fields() %}, {{ field.name() }}{% endfor %}): + def __init__(self{% for field in variant.fields %}, {{ field.name }}{% endfor %}): {%- if variant.has_fields() %} super().__init__(", ".join([ - {%- for field in variant.fields() %} - "{{ field.name() }}={!r}".format({{ field.name() }}), + {%- for field in variant.fields %} + "{{ field.name }}={!r}".format({{ field.name }}), {%- endfor %} ])) - {%- for field in variant.fields() %} - self.{{ field.name() }} = {{ field.name() }} + {%- for field in variant.fields %} + self.{{ field.name }} = {{ field.name }} {%- endfor %} {%- else %} pass @@ -55,45 +51,47 @@ def __init__(self{% for field in variant.fields() %}, {{ field.name() }}{% endfo {%- endif %} def __repr__(self): - return "{{ type_name }}.{{ variant_type_name }}({})".format(str(self)) + return "{{ e.name }}.{{ variant.name }}({})".format(str(self)) {%- endif %} - _UniffiTemp{{ type_name }}.{{ variant_type_name }} = {{ variant_type_name }} # type: ignore + _UniffiTemp{{ e.name }}.{{ variant.name }} = {{ variant.name }} # type: ignore {%- endfor %} -{{ type_name }} = _UniffiTemp{{ type_name }} # type: ignore -del _UniffiTemp{{ type_name }} +{{ e.name }} = _UniffiTemp{{ e.name }} # type: ignore +del _UniffiTemp{{ e.name }} class {{ ffi_converter_name }}(_UniffiConverterRustBuffer): @staticmethod def read(buf): variant = buf.read_i32() - {%- for variant in e.variants() %} + {%- for variant in e.variants %} if variant == {{ loop.index }}: - return {{ type_name }}.{{ variant.name() }}( - {%- if e.is_flat() %} - {{ Type::String.borrow()|read_fn }}(buf), - {%- else %} - {%- for field in variant.fields() %} + {%- if e.is_flat() %} + return {{ e.name }}.{{ variant.name }}( + {{ globals.string_type|read_fn }}(buf) + ) + {%- else %} + return {{ e.name }}.{{ variant.name }}( + {%- for field in variant.fields %} {{ field|read_fn }}(buf), {%- endfor %} - {%- endif %} ) + {%- endif %} {%- endfor %} raise InternalError("Raw enum value doesn't match any cases") @staticmethod def check_lower(value): - {%- if e.variants().is_empty() %} + {%- if e.variants.is_empty() %} pass {%- else %} - {%- for variant in e.variants() %} - if isinstance(value, {{ type_name }}.{{ variant.name() }}): - {%- for field in variant.fields() %} + {%- for variant in e.variants %} + if isinstance(value, {{ e.name }}.{{ variant.name }}): + {%- for field in variant.fields %} {%- if variant.has_nameless_fields() %} {{ field|check_lower_fn }}(value._values[{{ loop.index0 }}]) {%- else %} - {{ field|check_lower_fn }}(value.{{ field.name() }}) + {{ field|check_lower_fn }}(value.{{ field.name }}) {%- endif %} {%- endfor %} return @@ -102,14 +100,14 @@ def check_lower(value): @staticmethod def write(value, buf): - {%- for variant in e.variants() %} - if isinstance(value, {{ type_name }}.{{ variant.name() }}): + {%- for variant in e.variants %} + if isinstance(value, {{ e.name }}.{{ variant.name }}): buf.write_i32({{ loop.index }}) - {%- for field in variant.fields() %} - {%- if variant.has_nameless_fields() %} + {%- for field in variant.fields %} + {%- if variant.has_nameless_fields() %} {{ field|write_fn }}(value._values[{{ loop.index0 }}], buf) - {%- else %} - {{ field|write_fn }}(value.{{ field.name() }}, buf) - {%- endif %} + {%- else %} + {{ field|write_fn }}(value.{{ field.name }}, buf) + {%- endif %} {%- endfor %} {%- endfor %} diff --git a/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py deleted file mode 100644 index 26e4639e19..0000000000 --- a/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py +++ /dev/null @@ -1,9 +0,0 @@ -{%- let module = python_config.module_for_namespace(namespace) -%} - -# External type {{name}} is in namespace "{{namespace}}", crate {{module_path}} -{%- let ffi_converter_name = "_UniffiConverterType{}"|format(name) %} -{{ self.add_import_of(module, ffi_converter_name) }} -{{ self.add_import_of(module, name) }} {#- import the type alias itself -#} - -{%- let rustbuffer_local_name = "_UniffiRustBuffer{}"|format(name) %} -{{ self.add_import_of_as(module, "_UniffiRustBuffer", rustbuffer_local_name) }} diff --git a/uniffi_bindgen/src/bindings/python/templates/Float32Helper.py b/uniffi_bindgen/src/bindings/python/templates/Float32Helper.py index 49a1a7286e..6bcae689d5 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Float32Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/Float32Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterFloat(_UniffiConverterPrimitiveFloat): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveFloat): @staticmethod def read(buf): return buf.read_float() diff --git a/uniffi_bindgen/src/bindings/python/templates/Float64Helper.py b/uniffi_bindgen/src/bindings/python/templates/Float64Helper.py index e2084c7b13..b658822c7c 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Float64Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/Float64Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterDouble(_UniffiConverterPrimitiveFloat): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveFloat): @staticmethod def read(buf): return buf.read_double() diff --git a/uniffi_bindgen/src/bindings/python/templates/Helpers.py b/uniffi_bindgen/src/bindings/python/templates/Helpers.py index 1b1d27840d..eff2f2b0fb 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Helpers.py +++ b/uniffi_bindgen/src/bindings/python/templates/Helpers.py @@ -75,7 +75,7 @@ def _uniffi_trait_interface_call(call_status, make_call, write_return_value): return write_return_value(make_call()) except Exception as e: call_status.code = _UniffiRustCallStatus.CALL_UNEXPECTED_ERROR - call_status.error_buf = {{ Type::String.borrow()|lower_fn }}(repr(e)) + call_status.error_buf = {{ globals.string_type|lower_fn }}(repr(e)) def _uniffi_trait_interface_call_with_error(call_status, make_call, write_return_value, error_type, lower_error): try: @@ -86,4 +86,4 @@ def _uniffi_trait_interface_call_with_error(call_status, make_call, write_return call_status.error_buf = lower_error(e) except Exception as e: call_status.code = _UniffiRustCallStatus.CALL_UNEXPECTED_ERROR - call_status.error_buf = {{ Type::String.borrow()|lower_fn }}(repr(e)) + call_status.error_buf = {{ globals.string_type|lower_fn }}(repr(e)) diff --git a/uniffi_bindgen/src/bindings/python/templates/Int16Helper.py b/uniffi_bindgen/src/bindings/python/templates/Int16Helper.py index befa563384..f68e99cddd 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Int16Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/Int16Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterInt16(_UniffiConverterPrimitiveInt): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveInt): CLASS_NAME = "i16" VALUE_MIN = -2**15 VALUE_MAX = 2**15 diff --git a/uniffi_bindgen/src/bindings/python/templates/Int32Helper.py b/uniffi_bindgen/src/bindings/python/templates/Int32Helper.py index 70644f6717..f320e3904f 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Int32Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/Int32Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterInt32(_UniffiConverterPrimitiveInt): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveInt): CLASS_NAME = "i32" VALUE_MIN = -2**31 VALUE_MAX = 2**31 diff --git a/uniffi_bindgen/src/bindings/python/templates/Int64Helper.py b/uniffi_bindgen/src/bindings/python/templates/Int64Helper.py index 232f127bd6..c99b0013aa 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Int64Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/Int64Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterInt64(_UniffiConverterPrimitiveInt): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveInt): CLASS_NAME = "i64" VALUE_MIN = -2**63 VALUE_MAX = 2**63 diff --git a/uniffi_bindgen/src/bindings/python/templates/Int8Helper.py b/uniffi_bindgen/src/bindings/python/templates/Int8Helper.py index c1de1625e7..d2f0a50a6e 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Int8Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/Int8Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterInt8(_UniffiConverterPrimitiveInt): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveInt): CLASS_NAME = "i8" VALUE_MIN = -2**7 VALUE_MAX = 2**7 diff --git a/uniffi_bindgen/src/bindings/python/templates/MapTemplate.py b/uniffi_bindgen/src/bindings/python/templates/MapTemplate.py index a09ca28a30..47015dde3f 100644 --- a/uniffi_bindgen/src/bindings/python/templates/MapTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/MapTemplate.py @@ -1,5 +1,5 @@ -{%- let key_ffi_converter = key_type|ffi_converter_name %} -{%- let value_ffi_converter = value_type|ffi_converter_name %} +{%- let key_ffi_converter = key.ffi_converter_name %} +{%- let value_ffi_converter = value.ffi_converter_name %} class {{ ffi_converter_name }}(_UniffiConverterRustBuffer): @classmethod diff --git a/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py b/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py index 2d9c5f514d..41e6681fbc 100644 --- a/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py @@ -31,22 +31,22 @@ def _uniffi_load_indirect(): # Anything else must be an ELF platform - Linux, *BSD, Solaris/illumos libname = "lib{}.so" - libname = libname.format("{{ config.cdylib_name() }}") + libname = libname.format("{{ cdylib_name }}") path = os.path.join(os.path.dirname(__file__), libname) lib = ctypes.cdll.LoadLibrary(path) return lib def _uniffi_check_contract_api_version(lib): # Get the bindings contract version from our ComponentInterface - bindings_contract_version = {{ ci.uniffi_contract_version() }} + bindings_contract_version = {{ globals.contract_version }} # Get the scaffolding contract version by calling the into the dylib - scaffolding_contract_version = lib.{{ ci.ffi_uniffi_contract_version().name() }}() + scaffolding_contract_version = lib.{{ globals.ffi_uniffi_contract_version }}() if bindings_contract_version != scaffolding_contract_version: raise InternalError("UniFFI contract version mismatch: try cleaning and rebuilding your project") def _uniffi_check_api_checksums(lib): - {%- for (name, expected_checksum) in ci.iter_checksums() %} - if lib.{{ name }}() != {{ expected_checksum }}: + {%- for check in checksum_checks %} + if lib.{{ check.func }}() != {{ check.checksum }}: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") {%- else %} pass @@ -57,33 +57,35 @@ def _uniffi_check_api_checksums(lib): _UniffiLib = _uniffi_load_indirect() -{%- for def in ci.ffi_definitions() %} +{%- for def in ffi_definitions %} {%- match def %} -{%- when FfiDefinition::CallbackFunction(callback) %} -{{ callback.name()|ffi_callback_name }} = ctypes.CFUNCTYPE( - {%- match callback.return_type() %} - {%- when Some(return_type) %}{{ return_type|ffi_type_name }}, - {%- when None %}None, - {%- endmatch %} - {%- for arg in callback.arguments() -%} - {{ arg.type_().borrow()|ffi_type_name }}, - {%- endfor -%} - {%- if callback.has_rust_call_status_arg() %} +{%- when FfiDefinition::FunctionType(func_type) %} +{{ func_type.name }} = ctypes.CFUNCTYPE( + {{ func_type.return_type.as_deref().unwrap_or("None") }}, + {%- for arg in func_type.arguments %} + {{ arg.ty }}, + {%- endfor %} + {%- if func_type.has_rust_call_status_arg %} ctypes.POINTER(_UniffiRustCallStatus), {%- endif %} ) {%- when FfiDefinition::Struct(ffi_struct) %} -class {{ ffi_struct.name()|ffi_struct_name }}(ctypes.Structure): +class {{ ffi_struct.name }}(ctypes.Structure): _fields_ = [ - {%- for field in ffi_struct.fields() %} - ("{{ field.name() }}", {{ field.type_().borrow()|ffi_type_name }}), + {%- for field in ffi_struct.fields %} + ("{{ field.name }}", {{ field.ty }}), {%- endfor %} ] {%- when FfiDefinition::Function(func) %} -_UniffiLib.{{ func.name() }}.argtypes = ( - {%- call py::arg_list_ffi_decl(func) -%} +_UniffiLib.{{ func.name }}.argtypes = ( + {%- for arg in func.arguments %} + {{ arg.ty }}, + {%- endfor %} + {%- if func.has_rust_call_status_arg %} + ctypes.POINTER(_UniffiRustCallStatus), + {%- endif %} ) -_UniffiLib.{{ func.name() }}.restype = {% match func.return_type() %}{% when Some(type_) %}{{ type_|ffi_type_name }}{% when None %}None{% endmatch %} +_UniffiLib.{{ func.name }}.restype = {{ func.return_type.as_deref().unwrap_or("None") }} {%- endmatch %} {%- endfor %} diff --git a/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py index 33a6e0f917..45fb75350c 100644 --- a/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py @@ -1,62 +1,34 @@ -{%- let obj = ci.get_object_definition(name).unwrap() %} -{%- let methods = obj.methods() %} -{%- let protocol_name = format!("{type_name}Protocol") %} -{%- let protocol_docstring = obj.docstring() %} -{%- let protocol_base_class = "typing.Protocol" %} -{%- include "Protocol.py" %} - -{%- let impl_name %} -{%- if obj.has_callback_interface() %} -# {{ type_name }} is a foreign trait so treated like a callback interface, where the -# primary use-case is the trait being implemented locally. -# It is a base-class local implementations might subclass. -{# We reuse "Protocol.py" for this, even though here we are not generating a protocol #} -{%- let protocol_name = format!("{type_name}") %} -{%- let protocol_base_class = "" %} -{% include "Protocol.py" %} - -{%- let impl_name = format!("{type_name}Impl") %} -# `{{ impl_name }}` is the implementation for a Rust implemented version. -{%- else %} -# {{ type_name }} is a Rust-only trait - it's a wrapper around a Rust implementation. -{%- let impl_name = type_name.clone() %} +{%- if let Some(vtable) = interface.vtable %} +{% include "VTable.py" %} {%- endif %} -{%- if ci.is_name_used_as_error(name) %} -class {{ impl_name }}(Exception): -{%- else %} -class {{ impl_name }}({% for t in obj.trait_impls() %}{{ t.trait_name }},{% endfor %}): -{%- endif %} - {%- call py::docstring(obj, 4) %} +class {{ interface.name }}({{ interface.base_classes|join(", ") }}): + {{ interface.docstring|docindent(4) -}} _pointer: ctypes.c_void_p -{%- match obj.primary_constructor() %} -{%- when Some(cons) %} -{%- if cons.is_async() %} - def __init__(self, *args, **kw): - raise ValueError("async constructors not supported.") -{%- else %} - def __init__(self, {% call py::arg_list_decl(cons) -%}): - {%- call py::docstring(cons, 8) %} - {%- call py::setup_args_extra_indent(cons) %} - self._pointer = {% call py::to_ffi_call(cons) %} -{%- endif %} -{%- when None %} - {# no __init__ means simple construction without a pointer works, which can confuse #} - def __init__(self, *args, **kwargs): + {%- match interface.primary_constructor() %} + {%- when Some(cons) %} + {% filter indent(4) %}{% call py::define_callable(cons) %}{% endfilter %} + {%- when None %} + {# Define __init__ to prevent construction without a pointer, which can confuse #} + def __init__(self): + {%- if interface.had_async_constructor %} + raise ValueError("Async constructors are not supported in Python, call the `new` classmethod instead.") + {%- else %} raise ValueError("This class has no default constructor") -{%- endmatch %} + {%- endif %} + {%- endmatch %} def __del__(self): # In case of partial initialization of instances. pointer = getattr(self, "_pointer", None) if pointer is not None: - _uniffi_rust_call(_UniffiLib.{{ obj.ffi_object_free().name() }}, pointer) + _uniffi_rust_call(_UniffiLib.{{ interface.ffi_free }}, pointer) def _uniffi_clone_pointer(self): - return _uniffi_rust_call(_UniffiLib.{{ obj.ffi_object_clone().name() }}, self._pointer) + return _uniffi_rust_call(_UniffiLib.{{ interface.ffi_clone }}, self._pointer) - # Used by alternative constructors or any methods which return this type. + # Used by the lift function to construct a new instance @classmethod def _make_instance_(cls, pointer): # Lightly yucky way to bypass the usual __init__ logic @@ -65,66 +37,53 @@ def _make_instance_(cls, pointer): inst._pointer = pointer return inst -{%- for cons in obj.alternate_constructors() %} - @classmethod -{%- if cons.is_async() %} - async def {{ cons.name() }}(cls, {% call py::arg_list_decl(cons) %}): - {%- call py::docstring(cons, 8) %} - {%- call py::setup_args_extra_indent(cons) %} - - return await _uniffi_rust_call_async( - _UniffiLib.{{ cons.ffi_func().name() }}({% call py::arg_list_lowered(cons) %}), - _UniffiLib.{{ cons.ffi_rust_future_poll(ci) }}, - _UniffiLib.{{ cons.ffi_rust_future_complete(ci) }}, - _UniffiLib.{{ cons.ffi_rust_future_free(ci) }}, - {{ ffi_converter_name }}.lift, - {% call py::error_ffi_converter(cons) %} - ) -{%- else %} - def {{ cons.name() }}(cls, {% call py::arg_list_decl(cons) %}): - {%- call py::docstring(cons, 8) %} - {%- call py::setup_args_extra_indent(cons) %} - # Call the (fallible) function before creating any half-baked object instances. - pointer = {% call py::to_ffi_call(cons) %} - return cls._make_instance_(pointer) -{%- endif %} +{%- for cons in interface.alternate_constructors() %} + @staticmethod + {% filter indent(4) %}{% call py::define_callable(cons) %}{% endfilter %} {% endfor %} -{%- for meth in obj.methods() -%} - {%- call py::method_decl(meth.name(), meth) %} +{%- for meth in interface.methods %} + {% filter indent(4) %}{% call py::define_callable(meth) %}{% endfilter %} {%- endfor %} -{%- for tm in obj.uniffi_traits() -%} -{%- match tm %} -{%- when UniffiTrait::Debug { fmt } %} - {%- call py::method_decl("__repr__", fmt) %} -{%- when UniffiTrait::Display { fmt } %} - {%- call py::method_decl("__str__", fmt) %} -{%- when UniffiTrait::Eq { eq, ne } %} - def __eq__(self, other: object) -> {{ eq.return_type().unwrap()|type_name }}: - if not isinstance(other, {{ type_name }}): + +{%- for uniffi_trait in interface.uniffi_traits %} +{%- match uniffi_trait %} +{%- when UniffiTrait::Eq { eq, ne } %} +{# Special-case these to return NotImplemented when the wrong type is passed in #} + def __eq__(self, other: object) -> bool: + if not isinstance(other, {{ interface.name }}): return NotImplemented - return {{ eq.return_type().unwrap()|lift_fn }}({% call py::to_ffi_call_with_prefix("self._uniffi_clone_pointer()", eq) %}) + return {{ eq.return_type().as_ref().unwrap()|lift_fn }}( + _uniffi_rust_call_with_error( + None, + _UniffiLib.{{ eq.ffi_func() }}, + self._uniffi_clone_pointer(), + {{ interface|lower_fn }}(other), + ) + ) - def __ne__(self, other: object) -> {{ ne.return_type().unwrap()|type_name }}: - if not isinstance(other, {{ type_name }}): + def __ne__(self, other: object) -> bool: + if not isinstance(other, {{ interface.name }}): return NotImplemented - return {{ ne.return_type().unwrap()|lift_fn }}({% call py::to_ffi_call_with_prefix("self._uniffi_clone_pointer()", ne) %}) -{%- when UniffiTrait::Hash { hash } %} - {%- call py::method_decl("__hash__", hash) %} -{%- endmatch %} + return {{ ne.return_type().as_ref().unwrap()|lift_fn }}( + _uniffi_rust_call_with_error( + None, + _UniffiLib.{{ ne.ffi_func() }}, + self._uniffi_clone_pointer(), + {{ interface|lower_fn }}(other), + ) + ) +{%- else %} +{%- for trait_method in uniffi_trait.methods() %} + {% filter indent(4) %}{% call py::define_callable(trait_method) %}{% endfilter %} +{%- endfor %} +{%- endmatch %} {%- endfor %} -{%- if obj.has_callback_interface() %} -{%- let ffi_init_callback = obj.ffi_init_callback() %} -{%- let vtable = obj.vtable().expect("trait interface should have a vtable") %} -{%- let vtable_methods = obj.vtable_methods() %} -{% include "CallbackInterfaceImpl.py" %} -{%- endif %} - -{# Objects as error #} -{%- if ci.is_name_used_as_error(name) %} +{# Interface as error #} +{%- if interface.is_used_as_error() %} {# Due to some mismatches in the ffi converter mechanisms, errors are forced to be a RustBuffer #} class {{ ffi_converter_name }}__as_error(_UniffiConverterRustBuffer): @classmethod @@ -148,30 +107,30 @@ def lower(value): {%- endif %} class {{ ffi_converter_name }}: - {%- if obj.has_callback_interface() %} + {%- if interface.has_callback_interface() %} _handle_map = _UniffiHandleMap() {%- endif %} @staticmethod def lift(value: int): - return {{ impl_name }}._make_instance_(value) + return {{ interface.name }}._make_instance_(value) @staticmethod - def check_lower(value: {{ type_name }}): - {%- if obj.has_callback_interface() %} + def check_lower(value: {{ interface.protocol_name }}): + {%- if interface.has_callback_interface() %} pass {%- else %} - if not isinstance(value, {{ impl_name }}): - raise TypeError("Expected {{ impl_name }} instance, {} found".format(type(value).__name__)) + if not isinstance(value, {{ interface.name }}): + raise TypeError("Expected {{ interface.name }} instance, {} found".format(type(value).__name__)) {%- endif %} @staticmethod - def lower(value: {{ protocol_name }}): - {%- if obj.has_callback_interface() %} + def lower(value: {{ interface.protocol_name }}): + {%- if interface.has_callback_interface() %} return {{ ffi_converter_name }}._handle_map.insert(value) {%- else %} - if not isinstance(value, {{ impl_name }}): - raise TypeError("Expected {{ impl_name }} instance, {} found".format(type(value).__name__)) + if not isinstance(value, {{ interface.name }}): + raise TypeError("Expected {{ interface.name }} instance, {} found".format(type(value).__name__)) return value._uniffi_clone_pointer() {%- endif %} @@ -183,5 +142,5 @@ def read(cls, buf: _UniffiRustBuffer): return cls.lift(ptr) @classmethod - def write(cls, value: {{ protocol_name }}, buf: _UniffiRustBuffer): + def write(cls, value: {{ interface.protocol_name }}, buf: _UniffiRustBuffer): buf.write_u64(cls.lower(value)) diff --git a/uniffi_bindgen/src/bindings/python/templates/OptionalTemplate.py b/uniffi_bindgen/src/bindings/python/templates/OptionalTemplate.py index 4c07ae3e34..7b5cabf144 100644 --- a/uniffi_bindgen/src/bindings/python/templates/OptionalTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/OptionalTemplate.py @@ -1,4 +1,4 @@ -{%- let inner_ffi_converter = inner_type|ffi_converter_name %} +{%- let inner_ffi_converter = inner.ffi_converter_name %} class {{ ffi_converter_name }}(_UniffiConverterRustBuffer): @classmethod diff --git a/uniffi_bindgen/src/bindings/python/templates/Protocol.py b/uniffi_bindgen/src/bindings/python/templates/Protocol.py index 0e48bda575..2caf6b864a 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Protocol.py +++ b/uniffi_bindgen/src/bindings/python/templates/Protocol.py @@ -1,9 +1,9 @@ {# misnamed - a generic "abstract base class". Used as both a protocol and an ABC for traits. #} -class {{ protocol_name }}({{ protocol_base_class }}): - {%- call py::docstring_value(protocol_docstring, 4) %} - {%- for meth in methods.iter() %} - def {{ meth.name() }}(self, {% call py::arg_list_decl(meth) %}): - {%- call py::docstring(meth, 8) %} +class {{ protocol.name }}({{ protocol.base_class }}): + {{ protocol.docstring|docindent(4) -}} + {%- for meth in protocol.methods %} + {{ meth|def }} {{ meth.name }}({{ meth|arg_list }}): + {{ meth.docstring|docindent(8) -}} raise NotImplementedError {%- else %} pass diff --git a/uniffi_bindgen/src/bindings/python/templates/RecordTemplate.py b/uniffi_bindgen/src/bindings/python/templates/RecordTemplate.py index d5a3838662..6a20b3f05d 100644 --- a/uniffi_bindgen/src/bindings/python/templates/RecordTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/RecordTemplate.py @@ -1,25 +1,24 @@ -{%- let rec = ci.get_record_definition(name).unwrap() %} -class {{ type_name }}: - {%- call py::docstring(rec, 4) %} - {%- for field in rec.fields() %} - {{ field.name() }}: "{{ field|type_name }}" - {%- call py::docstring(field, 4) %} +class {{ rec.name }}: + {{ rec.docstring|docindent(4) -}} + {%- for field in rec.fields %} + {{ field.name }}: "{{ field.ty.type_name }}" + {{ field.docstring|docindent(4) -}} {%- endfor %} {%- if rec.has_fields() %} - def __init__(self, *, {% for field in rec.fields() %} - {{- field.name() }}: "{{- field|type_name }}" - {%- if field.default_value().is_some() %} = _DEFAULT{% endif %} + def __init__(self, *, {% for field in rec.fields %} + {{- field.name }}: "{{- field.ty.type_name }}" + {%- if field.has_default() %} = _DEFAULT{% endif %} {%- if !loop.last %}, {% endif %} {%- endfor %}): - {%- for field in rec.fields() %} - {%- let field_name = field.name() %} - {%- match field.default_value() %} + {%- for field in rec.fields %} + {%- let field_name = field.name %} + {%- match field.default %} {%- when None %} self.{{ field_name }} = {{ field_name }} {%- when Some(literal) %} if {{ field_name }} is _DEFAULT: - self.{{ field_name }} = {{ literal|literal_py(field) }} + self.{{ field_name }} = {{ literal }} else: self.{{ field_name }} = {{ field_name }} {%- endmatch %} @@ -27,11 +26,11 @@ def __init__(self, *, {% for field in rec.fields() %} {%- endif %} def __str__(self): - return "{{ type_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 }}({% 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() }} != other.{{ field.name() }}: + {%- for field in rec.fields %} + if self.{{ field.name }} != other.{{ field.name }}: return False {%- endfor %} return True @@ -39,28 +38,24 @@ def __eq__(self, other): class {{ ffi_converter_name }}(_UniffiConverterRustBuffer): @staticmethod def read(buf): - return {{ type_name }}( - {%- for field in rec.fields() %} - {{ field.name() }}={{ field|read_fn }}(buf), + return {{ rec.name }}( + {%- for field in rec.fields %} + {{ field.name }}={{ field|read_fn }}(buf), {%- endfor %} ) @staticmethod def check_lower(value): - {%- if rec.fields().is_empty() %} - pass + {%- for field in rec.fields %} + {{ field|check_lower_fn }}(value.{{ field.name }}) {%- else %} - {%- for field in rec.fields() %} - {{ field|check_lower_fn }}(value.{{ field.name() }}) + pass {%- endfor %} - {%- endif %} @staticmethod def write(value, buf): - {%- if rec.has_fields() %} - {%- for field in rec.fields() %} - {{ field|write_fn }}(value.{{ field.name() }}, buf) - {%- endfor %} + {%- for field in rec.fields %} + {{ field|write_fn }}(value.{{ field.name }}, buf) {%- else %} pass - {%- endif %} + {%- endfor %} diff --git a/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py b/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py index 5005bd1147..477a1ab913 100644 --- a/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py @@ -12,14 +12,14 @@ def default(): @staticmethod def alloc(size): - return _uniffi_rust_call(_UniffiLib.{{ ci.ffi_rustbuffer_alloc().name() }}, size) + return _uniffi_rust_call(_UniffiLib.{{ globals.ffi_rustbuffer_alloc }}, size) @staticmethod def reserve(rbuf, additional): - return _uniffi_rust_call(_UniffiLib.{{ ci.ffi_rustbuffer_reserve().name() }}, rbuf, additional) + return _uniffi_rust_call(_UniffiLib.{{ globals.ffi_rustbuffer_reserve }}, rbuf, additional) def free(self): - return _uniffi_rust_call(_UniffiLib.{{ ci.ffi_rustbuffer_free().name() }}, self) + return _uniffi_rust_call(_UniffiLib.{{ globals.ffi_rustbuffer_free }}, self) def __str__(self): return "_UniffiRustBuffer(capacity={}, len={}, data={})".format( diff --git a/uniffi_bindgen/src/bindings/python/templates/SequenceTemplate.py b/uniffi_bindgen/src/bindings/python/templates/SequenceTemplate.py index 3c30d9f9f5..d13d378119 100644 --- a/uniffi_bindgen/src/bindings/python/templates/SequenceTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/SequenceTemplate.py @@ -1,4 +1,4 @@ -{%- let inner_ffi_converter = inner_type|ffi_converter_name %} +{%- let inner_ffi_converter = inner.ffi_converter_name %} class {{ ffi_converter_name}}(_UniffiConverterRustBuffer): @classmethod diff --git a/uniffi_bindgen/src/bindings/python/templates/StringHelper.py b/uniffi_bindgen/src/bindings/python/templates/StringHelper.py index d574edd09f..b74a079610 100644 --- a/uniffi_bindgen/src/bindings/python/templates/StringHelper.py +++ b/uniffi_bindgen/src/bindings/python/templates/StringHelper.py @@ -1,4 +1,4 @@ -class _UniffiConverterString: +class {{ ffi_converter_name }}: @staticmethod def check_lower(value): if not isinstance(value, str): diff --git a/uniffi_bindgen/src/bindings/python/templates/TimestampHelper.py b/uniffi_bindgen/src/bindings/python/templates/TimestampHelper.py index 76d7f8bcdb..dec65c389a 100644 --- a/uniffi_bindgen/src/bindings/python/templates/TimestampHelper.py +++ b/uniffi_bindgen/src/bindings/python/templates/TimestampHelper.py @@ -4,7 +4,7 @@ # There is a loss of precision when converting from Rust timestamps, # which are accurate to the nanosecond, # to Python datetimes, which have a variable precision due to the use of float as representation. -class _UniffiConverterTimestamp(_UniffiConverterRustBuffer): +class {{ ffi_converter_name }}(_UniffiConverterRustBuffer): @staticmethod def read(buf): seconds = buf.read_i64() diff --git a/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py b/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py index ae012a7a06..bce810b098 100644 --- a/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py @@ -1,42 +1,11 @@ -{%- if func.is_async() %} +{{ func|def }} {{ func.name }}({{ func|arg_list }}) -> {{ func|return_type }}: + {{ func.docstring|docindent(4) -}} -{%- match func.return_type() -%} -{%- when Some(return_type) %} -async def {{ func.name() }}({%- call py::arg_list_decl(func) -%}) -> "{{ return_type|type_name }}": -{% when None %} -async def {{ func.name() }}({%- call py::arg_list_decl(func) -%}) -> None: -{% endmatch %} - - {%- call py::docstring(func, 4) %} - {%- call py::setup_args(func) %} - return await _uniffi_rust_call_async( - _UniffiLib.{{ func.ffi_func().name() }}({% call py::arg_list_lowered(func) %}), - _UniffiLib.{{func.ffi_rust_future_poll(ci) }}, - _UniffiLib.{{func.ffi_rust_future_complete(ci) }}, - _UniffiLib.{{func.ffi_rust_future_free(ci) }}, - # lift function - {%- match func.return_type() %} - {%- when Some(return_type) %} - {{ return_type|lift_fn }}, - {%- when None %} - lambda val: None, - {% endmatch %} - {% call py::error_ffi_converter(func) %} + {%- match func.return_type().ty -%} + {%- when Some(return_type) %} + return {{ return_type|lift_fn }}( + {% if func.is_async() %}await {% endif %}{{ func|ffi_caller_name }}({{ func|arg_names }}) ) - -{%- else %} -{%- match func.return_type() -%} -{%- when Some(return_type) %} - -def {{ func.name() }}({%- call py::arg_list_decl(func) -%}) -> "{{ return_type|type_name }}": - {%- call py::docstring(func, 4) %} - {%- call py::setup_args(func) %} - return {{ return_type|lift_fn }}({% call py::to_ffi_call(func) %}) -{% when None %} - -def {{ func.name() }}({%- call py::arg_list_decl(func) -%}) -> None: - {%- call py::docstring(func, 4) %} - {%- call py::setup_args(func) %} - {% call py::to_ffi_call(func) %} -{% endmatch %} -{%- endif %} + {% when None %} + {% if func.is_async() %}await {% endif %}{{ func|ffi_caller_name }}({{ func|arg_names }}) + {%- endmatch %} diff --git a/uniffi_bindgen/src/bindings/python/templates/Types.py b/uniffi_bindgen/src/bindings/python/templates/Types.py index 2c83757dda..c0b2611286 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Types.py +++ b/uniffi_bindgen/src/bindings/python/templates/Types.py @@ -1,116 +1,93 @@ -{%- import "macros.py" as py %} - -{%- for type_ in ci.iter_types() %} -{%- let type_name = type_|type_name %} -{%- let ffi_converter_name = type_|ffi_converter_name %} -{%- let canonical_type_name = type_|canonical_name %} - -{# - # Map `Type` instances to an include statement for that type. - # - # There is a companion match in `PythonCodeOracle::create_code_type()` which performs a similar function for the - # Rust code. - # - # - When adding additional types here, make sure to also add a match arm to that function. - # - To keep things manageable, let's try to limit ourselves to these 2 mega-matches - #} -{%- match type_ %} - -{%- when Type::Boolean %} +{%- for type_def in type_definitions %} + +{%- let ffi_converter_name = type_def.as_type().ffi_converter_name %} + +{% match type_def %} + +{%- when TypeDefinition::Simple(type_node) %} +{%- match type_node.kind %} +{%- when uniffi_meta::Type::Boolean %} {%- include "BooleanHelper.py" %} -{%- when Type::Int8 %} +{%- when uniffi_meta::Type::Int8 %} {%- include "Int8Helper.py" %} -{%- when Type::Int16 %} +{%- when uniffi_meta::Type::Int16 %} {%- include "Int16Helper.py" %} -{%- when Type::Int32 %} +{%- when uniffi_meta::Type::Int32 %} {%- include "Int32Helper.py" %} -{%- when Type::Int64 %} +{%- when uniffi_meta::Type::Int64 %} {%- include "Int64Helper.py" %} -{%- when Type::UInt8 %} +{%- when uniffi_meta::Type::UInt8 %} {%- include "UInt8Helper.py" %} -{%- when Type::UInt16 %} +{%- when uniffi_meta::Type::UInt16 %} {%- include "UInt16Helper.py" %} -{%- when Type::UInt32 %} +{%- when uniffi_meta::Type::UInt32 %} {%- include "UInt32Helper.py" %} -{%- when Type::UInt64 %} +{%- when uniffi_meta::Type::UInt64 %} {%- include "UInt64Helper.py" %} -{%- when Type::Float32 %} +{%- when uniffi_meta::Type::Float32 %} {%- include "Float32Helper.py" %} -{%- when Type::Float64 %} +{%- when uniffi_meta::Type::Float64 %} {%- include "Float64Helper.py" %} -{%- when Type::String %} +{%- when uniffi_meta::Type::String %} {%- include "StringHelper.py" %} -{%- when Type::Bytes %} +{%- when uniffi_meta::Type::Bytes %} {%- include "BytesHelper.py" %} -{%- when Type::Enum { name, module_path } %} -{%- let e = ci.get_enum_definition(name).unwrap() %} -{# For enums, there are either an error *or* an enum, they can't be both. #} -{%- if ci.is_name_used_as_error(name) %} -{%- include "ErrorTemplate.py" %} -{%- else %} -{%- include "EnumTemplate.py" %} -{% endif %} - -{%- when Type::Record { name, module_path } %} -{%- include "RecordTemplate.py" %} - -{%- when Type::Timestamp %} +{%- when uniffi_meta::Type::Timestamp %} {%- include "TimestampHelper.py" %} -{%- when Type::Duration %} +{%- when uniffi_meta::Type::Duration %} {%- include "DurationHelper.py" %} -{%- when Type::Optional { inner_type } %} + +{%- else %} +# Invalid Primitive type: {type_def:?}") +{%- endmatch %} + +{%- when TypeDefinition::Optional(OptionalType { inner, .. }) %} {%- include "OptionalTemplate.py" %} -{%- when Type::Sequence { inner_type } %} +{%- when TypeDefinition::Sequence(SequenceType { inner, .. }) %} {%- include "SequenceTemplate.py" %} -{%- when Type::Map { key_type, value_type } %} +{%- when TypeDefinition::Map(MapType { key, value, .. }) %} {%- include "MapTemplate.py" %} -{%- when Type::CallbackInterface { name, module_path } %} + +{%- when TypeDefinition::Enum(e) %} +{# For enums, there are either an error *or* an enum, they can't be both. #} + +{%- if e.self_type.is_used_as_error %} +{%- include "ErrorTemplate.py" %} +{%- else %} +{%- include "EnumTemplate.py" %} +{% endif %} + +{%- when TypeDefinition::Record(rec) %} +{%- include "RecordTemplate.py" %} + +{%- when TypeDefinition::Interface(interface) %} +{%- include "ObjectTemplate.py" %} + +{%- when TypeDefinition::CallbackInterface(cbi) %} {%- include "CallbackInterfaceTemplate.py" %} -{%- when Type::Custom { name, module_path, builtin } %} +{%- when TypeDefinition::Custom(custom_type) %} {%- include "CustomType.py" %} -{%- when Type::External { name, module_path, namespace, .. } %} -{%- include "ExternalTemplate.py" %} - -{%- else %} +{%- when TypeDefinition::External(_) %} {%- endmatch %} -{%- endfor %} - -# objects. -{%- for type_ in self.iter_sorted_object_types() %} -{%- match type_ %} -{%- when Type::Object { name, .. } %} -{%- let type_name = type_|type_name %} -{%- let ffi_converter_name = type_|ffi_converter_name %} -{%- let canonical_type_name = type_|canonical_name %} -{%- include "ObjectTemplate.py" %} -{%- else %} -{%- endmatch %} -{%- endfor %} - -{#- -Setup type aliases for our custom types, has complications due to -forward type references, #2067 --#} -{%- for (name, ty) in self.get_custom_type_aliases() %} -{{ name }} = {{ ty|type_name }} -{%- endfor %} + +{% endfor %} diff --git a/uniffi_bindgen/src/bindings/python/templates/UInt16Helper.py b/uniffi_bindgen/src/bindings/python/templates/UInt16Helper.py index 039bf76162..930cef3b68 100644 --- a/uniffi_bindgen/src/bindings/python/templates/UInt16Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/UInt16Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterUInt16(_UniffiConverterPrimitiveInt): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveInt): CLASS_NAME = "u16" VALUE_MIN = 0 VALUE_MAX = 2**16 diff --git a/uniffi_bindgen/src/bindings/python/templates/UInt32Helper.py b/uniffi_bindgen/src/bindings/python/templates/UInt32Helper.py index 1650bf9b60..1bb69afb6e 100644 --- a/uniffi_bindgen/src/bindings/python/templates/UInt32Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/UInt32Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterUInt32(_UniffiConverterPrimitiveInt): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveInt): CLASS_NAME = "u32" VALUE_MIN = 0 VALUE_MAX = 2**32 diff --git a/uniffi_bindgen/src/bindings/python/templates/UInt64Helper.py b/uniffi_bindgen/src/bindings/python/templates/UInt64Helper.py index f354545e26..4c98b60731 100644 --- a/uniffi_bindgen/src/bindings/python/templates/UInt64Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/UInt64Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterUInt64(_UniffiConverterPrimitiveInt): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveInt): CLASS_NAME = "u64" VALUE_MIN = 0 VALUE_MAX = 2**64 diff --git a/uniffi_bindgen/src/bindings/python/templates/UInt8Helper.py b/uniffi_bindgen/src/bindings/python/templates/UInt8Helper.py index cee130b4d9..92b46ee52c 100644 --- a/uniffi_bindgen/src/bindings/python/templates/UInt8Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/UInt8Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterUInt8(_UniffiConverterPrimitiveInt): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveInt): CLASS_NAME = "u8" VALUE_MIN = 0 VALUE_MAX = 2**8 diff --git a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py b/uniffi_bindgen/src/bindings/python/templates/VTable.py similarity index 55% rename from uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py rename to uniffi_bindgen/src/bindings/python/templates/VTable.py index 78c7e606f9..fd698ab756 100644 --- a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py +++ b/uniffi_bindgen/src/bindings/python/templates/VTable.py @@ -1,28 +1,30 @@ -{% if self.include_once_check("CallbackInterfaceRuntime.py") %}{% include "CallbackInterfaceRuntime.py" %}{% endif %} -{%- let trait_impl=format!("_UniffiTraitImpl{}", name) %} - # Put all the bits inside a class to keep the top-level namespace clean -class {{ trait_impl }}: +class {{ vtable.name }}: # For each method, generate a callback function to pass to Rust - {%- for (ffi_callback, meth) in vtable_methods.iter() %} + {%- for VTableMethod { callable, name, ffi_type, default_return_value } in vtable.methods %} - @{{ ffi_callback.name()|ffi_callback_name }} - def {{ meth.name() }}( - {%- for arg in ffi_callback.arguments() %} - {{ arg.name() }}, - {%- endfor -%} - {%- if ffi_callback.has_rust_call_status_arg() %} - uniffi_call_status_ptr, - {%- endif %} - ): + @{{ ffi_type }} + def {{ name }}( + uniffi_handle, + {%- for arg in callable.arguments() %} + {{ arg.name }}, + {%- endfor %} + {%- if callable.is_sync() %} + uniffi_out_return, + uniffi_call_status_ptr + {%- else %} + uniffi_future_callback, + uniffi_callback_data, + uniffi_out_return, + {%- endif %} + ): uniffi_obj = {{ ffi_converter_name }}._handle_map.get(uniffi_handle) def make_call(): - args = ({% for arg in meth.arguments() %}{{ arg|lift_fn }}({{ arg.name() }}), {% endfor %}) - method = uniffi_obj.{{ meth.name() }} - return method(*args) + return uniffi_obj.{{ name }}({% for arg in callable.arguments() %}{{ arg|lift_fn }}({{ arg.name }}), {% endfor %}) - {% if !meth.is_async() %} - {%- match meth.return_type() %} + {% match callable.async_data() %} + {% when None %} + {%- match callable.return_type() %} {%- when Some(return_type) %} def write_return_value(v): uniffi_out_return[0] = {{ return_type|lower_fn }}(v) @@ -30,7 +32,7 @@ def write_return_value(v): write_return_value = lambda v: None {%- endmatch %} - {%- match meth.throws_type() %} + {%- match callable.throws_type() %} {%- when None %} _uniffi_trait_interface_call( uniffi_call_status_ptr.contents, @@ -42,16 +44,16 @@ def write_return_value(v): uniffi_call_status_ptr.contents, make_call, write_return_value, - {{ error|type_name }}, + {{ error.type_name }}, {{ error|lower_fn }}, ) {%- endmatch %} - {%- else %} + {% when Some(async_data) %} def handle_success(return_value): uniffi_future_callback( uniffi_callback_data, - {{ meth.foreign_future_ffi_result_struct().name()|ffi_struct_name }}( - {%- if let Some(return_type) = meth.return_type() %} + {{ async_data.foreign_future_result_type }}( + {%- if let Some(return_type) = callable.return_type() %} {{ return_type|lower_fn }}(return_value), {%- endif %} _UniffiRustCallStatus.default() @@ -61,36 +63,34 @@ def handle_success(return_value): def handle_error(status_code, rust_buffer): uniffi_future_callback( uniffi_callback_data, - {{ meth.foreign_future_ffi_result_struct().name()|ffi_struct_name }}( - {%- match meth.return_type() %} - {%- when Some(return_type) %} - {{ meth.return_type().map(FfiType::from)|ffi_default_value }}, - {%- when None %} - {%- endmatch %} + {{ async_data.foreign_future_result_type }}( + {%- if callable.return_type().is_some() %} + {{ default_return_value }}, + {%- endif %} _UniffiRustCallStatus(status_code, rust_buffer), ) ) - {%- match meth.throws_type() %} + {%- match callable.throws_type() %} {%- when None %} uniffi_out_return[0] = _uniffi_trait_interface_call_async(make_call, handle_success, handle_error) {%- when Some(error) %} - uniffi_out_return[0] = _uniffi_trait_interface_call_async_with_error(make_call, handle_success, handle_error, {{ error|type_name }}, {{ error|lower_fn }}) + uniffi_out_return[0] = _uniffi_trait_interface_call_async_with_error(make_call, handle_success, handle_error, {{ error.type_name }}, {{ error|lower_fn }}) + {%- endmatch %} {%- endmatch %} - {%- endif %} {%- endfor %} - @{{ "CallbackInterfaceFree"|ffi_callback_name }} + @{{ globals.callback_interface_free_type }} def _uniffi_free(uniffi_handle): {{ ffi_converter_name }}._handle_map.remove(uniffi_handle) # Generate the FFI VTable. This has a field for each callback interface method. - _uniffi_vtable = {{ vtable|ffi_type_name }}( - {%- for (_, meth) in vtable_methods.iter() %} - {{ meth.name() }}, + _uniffi_vtable = {{ vtable.ffi_type }}( + {%- for vmeth in vtable.methods %} + {{ vmeth.name }}, {%- endfor %} _uniffi_free ) # Send Rust a pointer to the VTable. Note: this means we need to keep the struct alive forever, # or else bad things will happen when Rust tries to access it. - _UniffiLib.{{ ffi_init_callback.name() }}(ctypes.byref(_uniffi_vtable)) + _UniffiLib.{{ vtable.ffi_init_callback }}(ctypes.byref(_uniffi_vtable)) diff --git a/uniffi_bindgen/src/bindings/python/templates/macros.py b/uniffi_bindgen/src/bindings/python/templates/macros.py index 92e6df18ad..0731796bc6 100644 --- a/uniffi_bindgen/src/bindings/python/templates/macros.py +++ b/uniffi_bindgen/src/bindings/python/templates/macros.py @@ -1,178 +1,47 @@ -{# -// Template to call into rust. Used in several places. -// Variable names in `arg_list_decl` should match up with arg lists -// passed to rust via `arg_list_lowered` -#} - -{%- macro to_ffi_call(func) -%} -{%- call _to_ffi_call_with_prefix_arg("", func) %} -{%- endmacro -%} - -{%- macro to_ffi_call_with_prefix(prefix, func) -%} -{%- call _to_ffi_call_with_prefix_arg(format!("{},", prefix), func) %} -{%- endmacro -%} - -{%- macro _to_ffi_call_with_prefix_arg(prefix, func) -%} -{%- match func.throws_type() -%} -{%- when Some with (e) -%} -{%- match e -%} -{%- when Type::Enum { .. } -%} -_uniffi_rust_call_with_error({{ e|ffi_converter_name }}, -{%- when Type::Object { .. } -%} -_uniffi_rust_call_with_error({{ e|ffi_converter_name }}__as_error, -{%- else %} -# unsupported error type! -{%- endmatch %} -{%- else -%} -_uniffi_rust_call( -{%- endmatch -%} - _UniffiLib.{{ func.ffi_func().name() }}, - {{- prefix }} - {%- call arg_list_lowered(func) -%} -) -{%- endmacro -%} - -{%- macro arg_list_lowered(func) %} - {%- for arg in func.arguments() %} - {{ arg|lower_fn }}({{ arg.name() }}) - {%- if !loop.last %},{% endif %} - {%- endfor %} -{%- endmacro -%} - -{%- macro docstring_value(maybe_docstring, indent_spaces) %} -{%- if let Some(docstring) = maybe_docstring %} -{{ docstring|docstring(indent_spaces) }} -{{ "" }} -{%- endif %} -{%- endmacro %} - -{%- macro docstring(defn, indent_spaces) %} -{%- call docstring_value(defn.docstring(), indent_spaces) %} -{%- endmacro %} - -{#- -// Arglist as used in Python declarations of methods, functions and constructors. -// Note the type_name filters. --#} - -{% macro arg_list_decl(func) %} - {%- for arg in func.arguments() -%} - {{ arg.name() }} - {%- match arg.default_value() %} - {%- when Some with(literal) %}: "typing.Union[object, {{ arg|type_name -}}]" = _DEFAULT - {%- else %}: "{{ arg|type_name -}}" - {%- endmatch %} - {%- if !loop.last %},{% endif -%} - {%- endfor %} -{%- endmacro %} - -{#- -// Arglist as used in the _UniffiLib function declarations. -// Note unfiltered name but ffi_type_name filters. --#} -{%- macro arg_list_ffi_decl(func) %} - {%- for arg in func.arguments() %} - {{ arg.type_().borrow()|ffi_type_name }}, - {%- endfor %} - {%- if func.has_rust_call_status_arg() %} - ctypes.POINTER(_UniffiRustCallStatus),{% endif %} -{% endmacro -%} - -{# - # Setup function arguments by initializing default values. - #} -{%- macro setup_args(func) %} - {%- for arg in func.arguments() %} - {%- if let Some(literal) = arg.default_value() %} - if {{ arg.name() }} is _DEFAULT: - {{ arg.name() }} = {{ literal|literal_py(arg.as_type().borrow()) }} +{# Generates a definition for a function/method/constructor #} +{%- macro define_callable(callable) -%} +{{ callable|def }} {{ callable.name }}({{ callable|arg_list }}){% if !callable.is_primary_constructor() %} -> {{ callable|return_type }}{% endif %}: + {{ callable.docstring|docindent(4) -}} + {%- for arg in callable.arguments() %} + {%- if let Some(literal) = arg.default %} + if {{ arg.name }} is _DEFAULT: + {{ arg.name }} = {{ literal }} {%- endif %} - {{ arg|check_lower_fn }}({{ arg.name() }}) - {% endfor -%} -{%- endmacro -%} + {{ arg|check_lower_fn }}({{ arg.name }}) + {%- endfor %} -{# - # Exactly the same thing as `setup_args()` but with an extra 4 spaces of - # indent so that it works with object methods. - #} -{%- macro setup_args_extra_indent(func) %} - {%- for arg in func.arguments() %} - {%- if let Some(literal) = arg.default_value() %} - if {{ arg.name() }} is _DEFAULT: - {{ arg.name() }} = {{ literal|literal_py(arg.as_type().borrow()) }} + {%- if let Some(async_data) = callable.async_data() %} + _uniffi_return = await _uniffi_rust_call_async( + _UniffiLib.{{ callable.ffi_func() }}( + {%- if callable.is_method() %} + self._uniffi_clone_pointer(), + {%- endif %} + {%- for arg in callable.arguments() %} + {{ arg|lower_fn }}({{ arg.name }}), + {%- endfor %} + ), + _UniffiLib.{{ async_data.ffi_rust_future_poll }}, + _UniffiLib.{{ async_data.ffi_rust_future_complete }}, + _UniffiLib.{{ async_data.ffi_rust_future_free }}, + {{ callable|error_ffi_converter }}, + ) + {%- else %} + _uniffi_return = _uniffi_rust_call_with_error( + {{ callable|error_ffi_converter }}, + _UniffiLib.{{ callable.ffi_func() }}, + {%- if callable.is_method() %} + self._uniffi_clone_pointer(), {%- endif %} - {{ arg|check_lower_fn }}({{ arg.name() }}) - {% endfor -%} -{%- endmacro -%} - -{# - # Macro to call methods - #} -{%- macro method_decl(py_method_name, meth) %} -{% if meth.is_async() %} - -{%- match meth.return_type() %} -{%- when Some(return_type) %} - async def {{ py_method_name }}(self, {% call arg_list_decl(meth) %}) -> "{{ return_type|type_name }}": -{%- when None %} - async def {{ py_method_name }}(self, {% call arg_list_decl(meth) %}) -> None: -{% endmatch %} - - {%- call docstring(meth, 8) %} - {%- call setup_args_extra_indent(meth) %} - return await _uniffi_rust_call_async( - _UniffiLib.{{ meth.ffi_func().name() }}( - self._uniffi_clone_pointer(), {% call arg_list_lowered(meth) %} - ), - _UniffiLib.{{ meth.ffi_rust_future_poll(ci) }}, - _UniffiLib.{{ meth.ffi_rust_future_complete(ci) }}, - _UniffiLib.{{ meth.ffi_rust_future_free(ci) }}, - # lift function - {%- match meth.return_type() %} - {%- when Some(return_type) %} - {{ return_type|lift_fn }}, - {%- when None %} - lambda val: None, - {% endmatch %} - {% call error_ffi_converter(meth) %} - ) - -{%- else -%} -{%- match meth.return_type() %} - -{%- when Some(return_type) %} - - def {{ py_method_name }}(self, {% call arg_list_decl(meth) %}) -> "{{ return_type|type_name }}": - {%- call docstring(meth, 8) %} - {%- call setup_args_extra_indent(meth) %} - return {{ return_type|lift_fn }}( - {% call to_ffi_call_with_prefix("self._uniffi_clone_pointer()", meth) %} - ) - -{%- when None %} - - def {{ py_method_name }}(self, {% call arg_list_decl(meth) %}) -> None: - {%- call docstring(meth, 8) %} - {%- call setup_args_extra_indent(meth) %} - {% call to_ffi_call_with_prefix("self._uniffi_clone_pointer()", meth) %} -{% endmatch %} -{% endif %} + {%- for arg in callable.arguments() %} + {{ arg|lower_fn }}({{ arg.name }}), + {%- endfor %} + ) + {%- endif %} -{% endmacro %} + {%- if callable.is_primary_constructor() %} + self._pointer = _uniffi_return + {%- else if let Some(return_type) = &callable.return_type() %} + return {{ return_type|lift_fn }}(_uniffi_return) + {%- endif %} -{%- macro error_ffi_converter(func) %} - # Error FFI converter -{% match func.throws_type() %} -{%- when Some(e) %} -{%- match e -%} -{%- when Type::Enum { .. } -%} - {{ e|ffi_converter_name }}, -{%- when Type::Object { .. } -%} - {{ e|ffi_converter_name }}__as_error, -{%- else %} - # unsupported error type! -{%- endmatch %} -{%- when None %} - None, -{%- endmatch %} -{% endmacro %} +{%- endmacro %} diff --git a/uniffi_bindgen/src/bindings/python/templates/wrapper.py b/uniffi_bindgen/src/bindings/python/templates/wrapper.py index d97761f192..6840db5f81 100644 --- a/uniffi_bindgen/src/bindings/python/templates/wrapper.py +++ b/uniffi_bindgen/src/bindings/python/templates/wrapper.py @@ -1,4 +1,7 @@ -{%- call py::docstring_value(ci.namespace_docstring(), 0) %} +{%- import "macros.py" as py %} +{%- if let Some(docstring) = module_docstring %} +{{ docstring }} +{%- endif %} # This file was autogenerated by some hot garbage in the `uniffi` crate. # Trust me, you don't want to mess with it! @@ -27,12 +30,9 @@ import itertools import traceback import typing -{%- if ci.has_async_fns() %} -import asyncio -{%- endif %} import platform -{%- for req in self.imports() %} -{{ req.render() }} +{%- for import in imports %} +{{ import }} {%- endfor %} # Used for default argument values @@ -46,35 +46,29 @@ # Contains loading, initialization code, and the FFI Function declarations. {% include "NamespaceLibraryTemplate.py" %} -# Public interface members begin here. -{{ type_helper_code }} - -# Async support -{%- if ci.has_async_fns() %} +# Runtimes loaded: +# {{ "{:?}"|format(runtimes) }} +{%- if runtimes.callback_interface %} +{%- include "CallbackInterfaceRuntime.py" %} +{%- endif %} +{%- if runtimes.async_ %} {%- include "Async.py" %} {%- endif %} -{%- for func in ci.function_definitions() %} -{%- include "TopLevelFunctionTemplate.py" %} +# Public interface members begin here. +{%- for protocol in protocols %} +{%- include "Protocol.py" %} +{%- endfor %} + +{%- include "Types.py" %} + +{%- for func in functions %} +{% call py::define_callable(func) %} {%- endfor %} __all__ = [ "InternalError", - {%- for e in ci.enum_definitions() %} - "{{ e.name() }}", - {%- endfor %} - {%- for record in ci.record_definitions() %} - "{{ record.name() }}", - {%- endfor %} - {%- for func in ci.function_definitions() %} - "{{ func.name() }}", - {%- endfor %} - {%- for obj in ci.object_definitions() %} - "{{ obj.name() }}", - {%- endfor %} - {%- for c in ci.callback_interface_definitions() %} - "{{ c.name() }}", + {%- for export in exports %} + "{{ export }}", {%- endfor %} ] - -{% import "macros.py" as py %} diff --git a/uniffi_internal_macros/src/lib.rs b/uniffi_internal_macros/src/lib.rs index 3111679b98..643740a26d 100644 --- a/uniffi_internal_macros/src/lib.rs +++ b/uniffi_internal_macros/src/lib.rs @@ -134,3 +134,115 @@ pub fn checksum_derive(input: TokenStream) -> TokenStream { } .into() } + +/// Custom derive for AsType traits +/// +/// Bindings generators will typically define their own `Type` struct and an `AsType` trait that +/// maps Bindings IR nodes to that type struct. The details of these vary by language, but this +/// macro can derive the `AsType` trait for typical type layouts. +/// +/// +/// * For structs with a `ty` or `self_type` field, `as_type()` map to that field. +/// * For new-type style enums where each variant stores another struct, `as_type()` will map to +/// those struct's `as_type()` method. +/// +/// See the Python bindings for an example of this in the wild. +#[proc_macro_derive(AsType)] +pub fn as_type_derive(input: TokenStream) -> TokenStream { + let input: DeriveInput = parse_macro_input!(input); + let name = input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let code = match input.data { + Data::Struct(struct_) => { + let potential_fields = struct_ + .fields + .iter() + .filter(|field| match field.ident.as_ref() { + None => false, + Some(i) => { + let name = i.to_string(); + name == "ty" || name == "self_type" + } + }) + .collect::>(); + match potential_fields.len() { + 0 => panic!("#[derive(AsType)] requires a `ty` or `self_type` field"), + 1 => { + let ident = &potential_fields[0].ident; + quote! { + &self.#ident + } + } + _ => panic!("#[derive(AsType)] both `ty` and `self_type` defined"), + } + } + Data::Enum(enum_) => { + let match_inner = enum_.variants.iter().map(|variant| { + let ident = &variant.ident; + match &variant.fields { + Fields::Unnamed(fields) => { + if fields.unnamed.len() != 1 { + panic!("#[derive(AsType)] enum variants must have exactly 1 field") + } + quote! { + Self::#ident(inner) => inner.as_type(), + } + } + Fields::Named(fields) => { + if fields.named.len() != 1 { + panic!("#[derive(AsType)] enum variants must have exactly 1 field") + } + let field_ident = fields.named[0].ident.as_ref().unwrap(); + quote! { + Self::#ident { #field_ident } => #field_ident.as_type(), + } + } + Fields::Unit => { + panic!("#[derive(AsType)] enum variants must have exactly 1 field"); + } + } + }); + quote! { + match self { + #(#match_inner)* + } + } + } + Data::Union(_) => { + panic!("#[derive(AsType)] is not supported for unions"); + } + }; + + quote! { + #[automatically_derived] + impl #impl_generics AsType for #name #ty_generics #where_clause { + fn as_type(&self) -> &Type { + #code + } + } + } + .into() +} + +/// Custom derive for AsCallable traits +/// +/// AsCallable works similarly to AsType, except it maps nodes to the `Callable` struct. +/// Again, each bindings generator will define their own `Callable` struct and `AsCallable` trait. +/// This macro can auto-implement that trait for typical type layouts. +/// +/// This macro only works for structs and is implemented by mapping `&self` -> `&self.callable` +#[proc_macro_derive(AsCallable)] +pub fn as_callable_derive(input: TokenStream) -> TokenStream { + let input: DeriveInput = parse_macro_input!(input); + let name = input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + quote! { + #[automatically_derived] + impl #impl_generics AsCallable for #name #ty_generics #where_clause { + fn as_callable(&self) -> &Callable { + &self.callable + } + } + } + .into() +}