diff options
Diffstat (limited to 'third_party/rust/uniffi_bindgen/src/bindings/python')
46 files changed, 2365 insertions, 0 deletions
diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/callback_interface.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/callback_interface.rs new file mode 100644 index 0000000000..9359fbaeae --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/callback_interface.rs @@ -0,0 +1,33 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; + +pub struct CallbackInterfaceCodeType { + id: String, +} + +impl CallbackInterfaceCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for CallbackInterfaceCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("CallbackInterface{}", self.id) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!(); + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.to_string() + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/compounds.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/compounds.rs new file mode 100644 index 0000000000..80b1da040d --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/compounds.rs @@ -0,0 +1,122 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal, TypeIdentifier}; + +pub struct OptionalCodeType { + inner: TypeIdentifier, +} + +impl OptionalCodeType { + pub fn new(inner: TypeIdentifier) -> Self { + Self { inner } + } +} + +impl CodeType for OptionalCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.find(&self.inner).type_label(oracle) + } + + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + format!( + "Optional{}", + oracle.find(&self.inner).canonical_name(oracle), + ) + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + match literal { + Literal::Null => "None".into(), + _ => oracle.find(&self.inner).literal(oracle, literal), + } + } + + fn coerce(&self, oracle: &dyn CodeOracle, nm: &str) -> String { + format!( + "(None if {} is None else {})", + nm, + oracle.find(&self.inner).coerce(oracle, nm) + ) + } +} + +pub struct SequenceCodeType { + inner: TypeIdentifier, +} + +impl SequenceCodeType { + pub fn new(inner: TypeIdentifier) -> Self { + Self { inner } + } +} + +impl CodeType for SequenceCodeType { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + "list".to_string() + } + + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + format!( + "Sequence{}", + oracle.find(&self.inner).canonical_name(oracle), + ) + } + + fn literal(&self, _oracle: &dyn CodeOracle, literal: &Literal) -> String { + match literal { + Literal::EmptySequence => "[]".into(), + _ => unimplemented!(), + } + } + + fn coerce(&self, oracle: &dyn CodeOracle, nm: &str) -> String { + format!( + "list({} for x in {})", + oracle.find(&self.inner).coerce(oracle, "x"), + nm + ) + } +} + +pub struct MapCodeType { + key: TypeIdentifier, + value: TypeIdentifier, +} + +impl MapCodeType { + pub fn new(key: TypeIdentifier, value: TypeIdentifier) -> Self { + Self { key, value } + } +} + +impl CodeType for MapCodeType { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + "dict".to_string() + } + + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + format!( + "Map{}{}", + oracle.find(&self.key).canonical_name(oracle), + oracle.find(&self.value).canonical_name(oracle), + ) + } + + fn literal(&self, _oracle: &dyn CodeOracle, literal: &Literal) -> String { + match literal { + Literal::EmptyMap => "{}".into(), + _ => unimplemented!(), + } + } + + fn coerce(&self, oracle: &dyn CodeOracle, nm: &str) -> String { + format!( + "dict(({}, {}) for (k, v) in {}.items())", + self.key.coerce(oracle, "k"), + self.value.coerce(oracle, "v"), + nm + ) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/custom.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/custom.rs new file mode 100644 index 0000000000..d53a946947 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/custom.rs @@ -0,0 +1,29 @@ +/* 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 crate::backend::{CodeOracle, CodeType}; + +pub struct CustomCodeType { + name: String, +} + +impl CustomCodeType { + pub fn new(name: String) -> Self { + Self { name } + } +} + +impl CodeType for CustomCodeType { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + self.name.clone() + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.name) + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.to_string() + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/enum_.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/enum_.rs new file mode 100644 index 0000000000..9a86076770 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/enum_.rs @@ -0,0 +1,41 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; + +pub struct EnumCodeType { + id: String, +} + +impl EnumCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for EnumCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + if let Literal::Enum(v, _) = literal { + format!( + "{}.{}", + self.type_label(oracle), + oracle.enum_variant_name(v) + ) + } else { + unreachable!(); + } + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.to_string() + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/error.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/error.rs new file mode 100644 index 0000000000..f1567281f2 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/error.rs @@ -0,0 +1,33 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; + +pub struct ErrorCodeType { + id: String, +} + +impl ErrorCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for ErrorCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!(); + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.to_string() + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/external.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/external.rs new file mode 100644 index 0000000000..1739d84cec --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/external.rs @@ -0,0 +1,29 @@ +/* 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 crate::backend::{CodeOracle, CodeType}; + +pub struct ExternalCodeType { + name: String, +} + +impl ExternalCodeType { + pub fn new(name: String) -> Self { + Self { name } + } +} + +impl CodeType for ExternalCodeType { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + self.name.clone() + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.name) + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.into() + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/miscellany.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/miscellany.rs new file mode 100644 index 0000000000..c885428bc3 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/miscellany.rs @@ -0,0 +1,36 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; +use paste::paste; + +macro_rules! impl_code_type_for_miscellany { + ($T:ty, $canonical_name:literal) => { + paste! { + pub struct $T; + + impl CodeType for $T { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + format!("{}", $canonical_name) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("{}", $canonical_name) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!() + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.to_string() + } + } + } + }; +} + +impl_code_type_for_miscellany!(TimestampCodeType, "Timestamp"); + +impl_code_type_for_miscellany!(DurationCodeType, "Duration"); diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/mod.rs new file mode 100644 index 0000000000..0b2218402a --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/mod.rs @@ -0,0 +1,448 @@ +/* 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 anyhow::{Context, Result}; +use askama::Template; +use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase}; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; +use std::cell::RefCell; +use std::collections::{BTreeSet, HashMap, HashSet}; + +use crate::backend::{CodeOracle, CodeType, TemplateExpression, TypeIdentifier}; +use crate::interface::*; +use crate::MergeWith; + +mod callback_interface; +mod compounds; +mod custom; +mod enum_; +mod error; +mod external; +mod miscellany; +mod object; +mod primitives; +mod record; + +// Taken from Python's `keyword.py` module. +static KEYWORDS: Lazy<HashSet<String>> = 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 { + cdylib_name: Option<String>, + #[serde(default)] + custom_types: HashMap<String, CustomTypeConfig>, +} + +#[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<Vec<String>>, + 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() + } + } +} + +impl From<&ComponentInterface> for Config { + fn from(ci: &ComponentInterface) -> Self { + Config { + cdylib_name: Some(format!("uniffi_{}", ci.namespace())), + custom_types: HashMap::new(), + } + } +} + +impl MergeWith for Config { + fn merge_with(&self, other: &Self) -> Self { + Config { + cdylib_name: self.cdylib_name.merge_with(&other.cdylib_name), + custom_types: self.custom_types.merge_with(&other.custom_types), + } + } +} + +// Generate python bindings for the given ComponentInterface, as a string. +pub fn generate_python_bindings(config: &Config, ci: &ComponentInterface) -> Result<String> { + 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}"), + } + } +} + +/// Renders Python helper code for all types +/// +/// 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<HashSet<String>>, + // Track imports added with the `add_import()` macro + imports: RefCell<BTreeSet<ImportRequirement>>, +} + +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 askama `{{ }}` 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(), + }); + "" + } +} + +#[derive(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<ImportRequirement>, +} +impl<'a> PythonWrapper<'a> { + pub fn new(config: Config, ci: &'a ComponentInterface) -> Self { + 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, + } + } + + pub fn imports(&self) -> Vec<ImportRequirement> { + self.type_imports.iter().cloned().collect() + } +} + +fn fixup_keyword(name: String) -> String { + if KEYWORDS.contains(&name) { + format!("_{name}") + } else { + name + } +} + +#[derive(Clone, Default)] +pub struct PythonCodeOracle; + +impl PythonCodeOracle { + // Map `Type` instances to a `Box<dyn CodeType>` 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 + fn create_code_type(&self, type_: TypeIdentifier) -> Box<dyn CodeType> { + match 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::Timestamp => Box::new(miscellany::TimestampCodeType), + Type::Duration => Box::new(miscellany::DurationCodeType), + + Type::Enum(id) => Box::new(enum_::EnumCodeType::new(id)), + Type::Object(id) => Box::new(object::ObjectCodeType::new(id)), + Type::Record(id) => Box::new(record::RecordCodeType::new(id)), + Type::Error(id) => Box::new(error::ErrorCodeType::new(id)), + Type::CallbackInterface(id) => { + Box::new(callback_interface::CallbackInterfaceCodeType::new(id)) + } + + Type::Optional(inner) => Box::new(compounds::OptionalCodeType::new(*inner)), + Type::Sequence(inner) => Box::new(compounds::SequenceCodeType::new(*inner)), + Type::Map(key, value) => Box::new(compounds::MapCodeType::new(*key, *value)), + Type::External { name, .. } => Box::new(external::ExternalCodeType::new(name)), + Type::Custom { name, .. } => Box::new(custom::CustomCodeType::new(name)), + Type::Unresolved { name } => { + unreachable!("Type `{name}` must be resolved before calling create_code_type") + } + } + } +} + +impl CodeOracle for PythonCodeOracle { + fn find(&self, type_: &TypeIdentifier) -> Box<dyn CodeType> { + self.create_code_type(type_.clone()) + } + + /// 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 exception name + /// This replaces "Error" at the end of the name with "Exception". + fn error_name(&self, nm: &str) -> String { + let name = fixup_keyword(self.class_name(nm)); + match name.strip_suffix("Error") { + None => name, + Some(stripped) => format!("{stripped}Exception"), + } + } + + 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::RustArcPtr(_) => "ctypes.c_void_p".to_string(), + FfiType::RustBuffer(maybe_suffix) => match maybe_suffix { + Some(suffix) => format!("RustBuffer{}", suffix), + None => "RustBuffer".to_string(), + }, + FfiType::ForeignBytes => "ForeignBytes".to_string(), + FfiType::ForeignCallback => "FOREIGN_CALLBACK_T".to_string(), + } + } +} + +pub mod filters { + use super::*; + + fn oracle() -> &'static PythonCodeOracle { + &PythonCodeOracle + } + + pub fn type_name(codetype: &impl CodeType) -> Result<String, askama::Error> { + let oracle = oracle(); + Ok(codetype.type_label(oracle)) + } + + pub fn ffi_converter_name(codetype: &impl CodeType) -> Result<String, askama::Error> { + let oracle = oracle(); + Ok(codetype.ffi_converter_name(oracle)) + } + + pub fn canonical_name(codetype: &impl CodeType) -> Result<String, askama::Error> { + let oracle = oracle(); + Ok(codetype.canonical_name(oracle)) + } + + pub fn lift_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(format!("{}.lift", ffi_converter_name(codetype)?)) + } + + pub fn lower_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(format!("{}.lower", ffi_converter_name(codetype)?)) + } + + pub fn read_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(format!("{}.read", ffi_converter_name(codetype)?)) + } + + pub fn write_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(format!("{}.write", ffi_converter_name(codetype)?)) + } + + pub fn literal_py( + literal: &Literal, + codetype: &impl CodeType, + ) -> Result<String, askama::Error> { + let oracle = oracle(); + Ok(codetype.literal(oracle, literal)) + } + + /// Get the Python syntax for representing a given low-level `FfiType`. + pub fn ffi_type_name(type_: &FfiType) -> Result<String, askama::Error> { + Ok(oracle().ffi_type_label(type_)) + } + + /// Get the idiomatic Python rendering of a class name (for enums, records, errors, etc). + pub fn class_name(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().class_name(nm)) + } + + /// Get the idiomatic Python rendering of a function name. + pub fn fn_name(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().fn_name(nm)) + } + + /// Get the idiomatic Python rendering of a variable name. + pub fn var_name(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().var_name(nm)) + } + + /// Get the idiomatic Python rendering of an individual enum variant. + pub fn enum_variant_py(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().enum_variant_name(nm)) + } + + pub fn coerce_py(nm: &str, type_: &Type) -> Result<String, askama::Error> { + let oracle = oracle(); + Ok(oracle.find(type_).coerce(oracle, nm)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/object.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/object.rs new file mode 100644 index 0000000000..7744865ec0 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/object.rs @@ -0,0 +1,33 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; + +pub struct ObjectCodeType { + id: String, +} + +impl ObjectCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for ObjectCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!(); + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.to_string() + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/primitives.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/primitives.rs new file mode 100644 index 0000000000..de7918fb16 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/primitives.rs @@ -0,0 +1,69 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; +use crate::interface::Radix; +use paste::paste; + +fn render_literal(_oracle: &dyn CodeOracle, literal: &Literal) -> String { + 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(), + + _ => unreachable!("Literal"), + } +} + +macro_rules! impl_code_type_for_primitive { + ($T:ty, $class_name:literal, $coerce_code:expr) => { + paste! { + pub struct $T; + + impl CodeType for $T { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + $class_name.into() + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + render_literal(oracle, &literal) + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + format!($coerce_code, nm) + } + } + } + }; +} + +impl_code_type_for_primitive!(BooleanCodeType, "Bool", "bool({})"); +impl_code_type_for_primitive!(StringCodeType, "String", "{}"); +impl_code_type_for_primitive!(Int8CodeType, "Int8", "int({})"); +impl_code_type_for_primitive!(Int16CodeType, "Int16", "int({})"); +impl_code_type_for_primitive!(Int32CodeType, "Int32", "int({})"); +impl_code_type_for_primitive!(Int64CodeType, "Int64", "int({})"); +impl_code_type_for_primitive!(UInt8CodeType, "UInt8", "int({})"); +impl_code_type_for_primitive!(UInt16CodeType, "UInt16", "int({})"); +impl_code_type_for_primitive!(UInt32CodeType, "UInt32", "int({})"); +impl_code_type_for_primitive!(UInt64CodeType, "UInt64", "int({})"); +impl_code_type_for_primitive!(Float32CodeType, "Float", "float({})"); +impl_code_type_for_primitive!(Float64CodeType, "Double", "float({})"); diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/record.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/record.rs new file mode 100644 index 0000000000..1dba8e49fe --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/record.rs @@ -0,0 +1,33 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; + +pub struct RecordCodeType { + id: String, +} + +impl RecordCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for RecordCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!(); + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.to_string() + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/mod.rs new file mode 100644 index 0000000000..3a1cfaa78d --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/mod.rs @@ -0,0 +1,40 @@ +/* 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::{io::Write, process::Command}; + +use anyhow::Result; +use camino::Utf8Path; +use fs_err::File; + +pub mod gen_python; +mod test; + +use super::super::interface::ComponentInterface; +pub use gen_python::{generate_python_bindings, Config}; +pub use test::run_test; + +// Generate python bindings for the given ComponentInterface, in the given output directory. +pub fn write_bindings( + config: &Config, + ci: &ComponentInterface, + out_dir: &Utf8Path, + try_format_code: bool, +) -> Result<()> { + let py_file = out_dir.join(format!("{}.py", ci.namespace())); + let mut f = File::create(&py_file)?; + write!(f, "{}", generate_python_bindings(config, ci)?)?; + + if try_format_code { + if let Err(e) = Command::new("yapf").arg(&py_file).output() { + println!( + "Warning: Unable to auto-format {} using yapf: {:?}", + py_file.file_name().unwrap(), + e + ) + } + } + + Ok(()) +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/BooleanHelper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/BooleanHelper.py new file mode 100644 index 0000000000..d7b64b91f7 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/BooleanHelper.py @@ -0,0 +1,16 @@ +class FfiConverterBool: + @classmethod + def read(cls, buf): + return cls.lift(buf.readU8()) + + @classmethod + def write(cls, value, buf): + buf.writeU8(cls.lower(value)) + + @staticmethod + def lift(value): + return int(value) != 0 + + @staticmethod + def lower(value): + return 1 if value else 0 diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceRuntime.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceRuntime.py new file mode 100644 index 0000000000..e8a0ec56d9 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceRuntime.py @@ -0,0 +1,73 @@ +import threading + +class ConcurrentHandleMap: + """ + A map where inserting, getting and removing data is synchronized with a lock. + """ + + def __init__(self): + # type Handle = int + self._left_map = {} # type: Dict[Handle, Any] + self._right_map = {} # type: Dict[Any, Handle] + + self._lock = threading.Lock() + self._current_handle = 0 + self._stride = 1 + + + def insert(self, obj): + with self._lock: + if obj in self._right_map: + return self._right_map[obj] + else: + handle = self._current_handle + self._current_handle += self._stride + self._left_map[handle] = obj + self._right_map[obj] = handle + return handle + + def get(self, handle): + with self._lock: + return self._left_map.get(handle) + + def remove(self, handle): + with self._lock: + if handle in self._left_map: + obj = self._left_map.pop(handle) + del self._right_map[obj] + return obj + +# Magic number for the Rust proxy to call using the same mechanism as every other method, +# to free the callback once it's dropped by Rust. +IDX_CALLBACK_FREE = 0 + +class FfiConverterCallbackInterface: + _handle_map = ConcurrentHandleMap() + + def __init__(self, cb): + self._foreign_callback = cb + + def drop(self, handle): + self.__class__._handle_map.remove(handle) + + @classmethod + def lift(cls, handle): + obj = cls._handle_map.get(handle) + if not obj: + raise InternalError("The object in the handle map has been dropped already") + + return obj + + @classmethod + def read(cls, buf): + handle = buf.readU64() + cls.lift(handle) + + @classmethod + def lower(cls, cb): + handle = cls._handle_map.insert(cb) + return handle + + @classmethod + def write(cls, cb, buf): + buf.writeU64(cls.lower(cb)) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py new file mode 100644 index 0000000000..cf4411b8a0 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py @@ -0,0 +1,104 @@ +{%- let cbi = ci.get_callback_interface_definition(id).unwrap() %} +{%- let foreign_callback = format!("foreignCallback{}", canonical_type_name) %} + +{% if self.include_once_check("CallbackInterfaceRuntime.py") %}{% include "CallbackInterfaceRuntime.py" %}{% endif %} + +# Declaration and FfiConverters for {{ type_name }} Callback Interface + +class {{ type_name }}: + {% for meth in cbi.methods() -%} + def {{ meth.name()|fn_name }}({% call py::arg_list_decl(meth) %}): + raise NotImplementedError + + {% endfor %} + +def py_{{ foreign_callback }}(handle, method, args, buf_ptr): + {% for meth in cbi.methods() -%} + {% let method_name = format!("invoke_{}", meth.name())|fn_name %} + def {{ method_name }}(python_callback, args): + {#- Unpacking args from the RustBuffer #} + {%- if meth.arguments().len() != 0 -%} + {#- Calling the concrete callback object #} + with args.consumeWithStream() as buf: + rval = python_callback.{{ meth.name()|fn_name }}( + {% for arg in meth.arguments() -%} + {{ arg|read_fn }}(buf) + {%- if !loop.last %}, {% endif %} + {% endfor -%} + ) + {% else %} + rval = python_callback.{{ meth.name()|fn_name }}() + {% endif -%} + + {#- Packing up the return value into a RustBuffer #} + {%- match meth.return_type() -%} + {%- when Some with (return_type) -%} + with RustBuffer.allocWithBuilder() as builder: + {{ return_type|write_fn }}(rval, builder) + return builder.finalize() + {%- else -%} + return RustBuffer.alloc(0) + {% endmatch -%} + {% endfor %} + + cb = {{ ffi_converter_name }}.lift(handle) + if not cb: + raise InternalError("No callback in handlemap; this is a Uniffi bug") + + if method == IDX_CALLBACK_FREE: + {{ ffi_converter_name }}.drop(handle) + # No return value. + # See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` + return 0 + + {% for meth in cbi.methods() -%} + {% let method_name = format!("invoke_{}", meth.name())|fn_name -%} + if method == {{ loop.index }}: + # Call the method and handle any errors + # See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` for details + try: + {%- match meth.throws_type() %} + {%- when Some(err) %} + try: + # Successful return + buf_ptr[0] = {{ method_name }}(cb, args) + return 1 + except {{ err|type_name }} as e: + # Catch errors declared in the UDL file + with RustBuffer.allocWithBuilder() as builder: + {{ err|write_fn }}(e, builder) + buf_ptr[0] = builder.finalize() + return -2 + {%- else %} + # Successful return + buf_ptr[0] = {{ method_name }}(cb, args) + return 1 + {%- endmatch %} + except BaseException as e: + # Catch unexpected errors + try: + # Try to serialize the exception into a String + buf_ptr[0] = {{ Type::String.borrow()|lower_fn }}(repr(e)) + except: + # If that fails, just give up + pass + return -1 + {% endfor %} + + # This should never happen, because an out of bounds method index won't + # ever be used. Once we can catch errors, we should return an InternalException. + # https://github.com/mozilla/uniffi-rs/issues/351 + + # An unexpected error happened. + # See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` + return -1 + +# We need to keep this function reference alive: +# if they get GC'd while in use then UniFFI internals could attempt to call a function +# that is in freed memory. +# That would be...uh...bad. Yeah, that's the word. Bad. +{{ foreign_callback }} = FOREIGN_CALLBACK_T(py_{{ foreign_callback }}) + +# The FfiConverter which transforms the Callbacks in to Handles to pass to Rust. +rust_call(lambda err: _UniFFILib.{{ cbi.ffi_init_callback().name() }}({{ foreign_callback }}, err)) +{{ ffi_converter_name }} = FfiConverterCallbackInterface({{ foreign_callback }}) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/CustomType.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/CustomType.py new file mode 100644 index 0000000000..9ce6a21ced --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/CustomType.py @@ -0,0 +1,52 @@ +{%- match python_config.custom_types.get(name.as_str()) %} +{% when None %} +{#- No custom type config, just forward all methods to our builtin type #} +class FfiConverterType{{ name }}: + @staticmethod + def write(value, buf): + {{ builtin|ffi_converter_name }}.write(value, buf) + + @staticmethod + def read(buf): + return {{ builtin|ffi_converter_name }}.read(buf) + + @staticmethod + def lift(value): + return {{ builtin|ffi_converter_name }}.lift(value) + + @staticmethod + def lower(value): + return {{ builtin|ffi_converter_name }}.lower(value) + +{%- when Some(config) %} + +{%- match config.imports %} +{%- when Some(imports) %} +{%- for import_name in imports %} +{{ self.add_import(import_name) }} +{%- endfor %} +{%- else %} +{%- endmatch %} + +{#- Custom type config supplied, use it to convert the builtin type #} +class FfiConverterType{{ name }}: + @staticmethod + def write(value, buf): + builtin_value = {{ config.from_custom.render("value") }} + {{ builtin|write_fn }}(builtin_value, buf) + + @staticmethod + def read(buf): + builtin_value = {{ builtin|read_fn }}(buf) + return {{ config.into_custom.render("builtin_value") }} + + @staticmethod + def lift(value): + builtin_value = {{ builtin|lift_fn }}(value) + return {{ config.into_custom.render("builtin_value") }} + + @staticmethod + def lower(value): + builtin_value = {{ config.from_custom.render("value") }} + return {{ builtin|lower_fn }}(builtin_value) +{%- endmatch %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/DurationHelper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/DurationHelper.py new file mode 100644 index 0000000000..16fdd986f3 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/DurationHelper.py @@ -0,0 +1,19 @@ +# The Duration type. +# 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 FfiConverterDuration(FfiConverterRustBuffer): + @staticmethod + def read(buf): + seconds = buf.readU64() + microseconds = buf.readU32() / 1.0e3 + return datetime.timedelta(seconds=seconds, microseconds=microseconds) + + @staticmethod + def write(value, buf): + seconds = value.seconds + value.days * 24 * 3600 + nanoseconds = value.microseconds * 1000 + if seconds < 0: + raise ValueError("Invalid duration, must be non-negative") + buf.writeI64(seconds) + buf.writeU32(nanoseconds) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/EnumTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/EnumTemplate.py new file mode 100644 index 0000000000..5c366d8855 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/EnumTemplate.py @@ -0,0 +1,93 @@ +{# +# Python has a built-in `enum` module which is nice to use, but doesn't support +# variants with associated data. So, we switch here, and generate a stdlib `enum` +# when none of the variants have associated data, or a generic nested-class +# construct when they do. +#} +{%- let e = ci.get_enum_definition(name).unwrap() %} +{% if e.is_flat() %} + +class {{ type_name }}(enum.Enum): + {% for variant in e.variants() -%} + {{ variant.name()|enum_variant_py }} = {{ loop.index }} + {% endfor %} +{% else %} + +class {{ type_name }}: + def __init__(self): + raise RuntimeError("{{ type_name }} cannot be instantiated directly") + + # Each enum variant is a nested class of the enum itself. + {% for variant in e.variants() -%} + class {{ variant.name()|enum_variant_py }}(object): + def __init__(self,{% for field in variant.fields() %}{{ field.name()|var_name }}{% if loop.last %}{% else %}, {% endif %}{% endfor %}): + {% if variant.has_fields() %} + {%- for field in variant.fields() %} + self.{{ field.name()|var_name }} = {{ field.name()|var_name }} + {%- endfor %} + {% else %} + pass + {% endif %} + + def __str__(self): + return "{{ type_name }}.{{ variant.name()|enum_variant_py }}({% for field in variant.fields() %}{{ field.name()|var_name }}={}{% if loop.last %}{% else %}, {% endif %}{% endfor %})".format({% for field in variant.fields() %}self.{{ field.name()|var_name }}{% if loop.last %}{% else %}, {% endif %}{% endfor %}) + + def __eq__(self, other): + if not other.is_{{ variant.name()|var_name }}(): + return False + {%- for field in variant.fields() %} + if self.{{ field.name()|var_name }} != other.{{ field.name()|var_name }}: + return False + {%- endfor %} + return True + {% endfor %} + + # For each variant, we have an `is_NAME` method for easily checking + # whether an instance is that variant. + {% for variant in e.variants() -%} + def is_{{ variant.name()|var_name }}(self): + return isinstance(self, {{ type_name }}.{{ variant.name()|enum_variant_py }}) + {% 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()|enum_variant_py }} = type("{{ type_name }}.{{ variant.name()|enum_variant_py }}", ({{ type_name }}.{{variant.name()|enum_variant_py}}, {{ type_name }},), {}) +{% endfor %} + +{% endif %} + +class {{ ffi_converter_name }}(FfiConverterRustBuffer): + @staticmethod + def read(buf): + variant = buf.readI32() + + {%- for variant in e.variants() %} + if variant == {{ loop.index }}: + {%- if e.is_flat() %} + return {{ type_name }}.{{variant.name()|enum_variant_py}} + {%- else %} + return {{ type_name }}.{{variant.name()|enum_variant_py}}( + {%- for field in variant.fields() %} + {{ field|read_fn }}(buf), + {%- endfor %} + ) + {%- endif %} + {%- endfor %} + raise InternalError("Raw enum value doesn't match any cases") + + def write(value, buf): + {%- for variant in e.variants() %} + {%- if e.is_flat() %} + if value == {{ type_name }}.{{ variant.name()|enum_variant_py }}: + buf.writeI32({{ loop.index }}) + {%- else %} + if value.is_{{ variant.name()|var_name }}(): + buf.writeI32({{ loop.index }}) + {%- for field in variant.fields() %} + {{ field|write_fn }}(value.{{ field.name()|var_name }}, buf) + {%- endfor %} + {%- endif %} + {%- endfor %} + diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py new file mode 100644 index 0000000000..305ae88618 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py @@ -0,0 +1,75 @@ +{%- let e = ci.get_error_definition(name).unwrap() %} + +# {{ type_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 separated, 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 UniFFIExceptionTmpNamespace: + class {{ type_name }}(Exception): + pass + {% for variant in e.variants() %} + {%- let variant_type_name = variant.name()|class_name %} + + {%- if e.is_flat() %} + class {{ variant_type_name }}({{ type_name }}): + def __str__(self): + return "{{ type_name }}.{{ variant_type_name }}({})".format(repr(super().__str__())) + {%- else %} + class {{ variant_type_name }}({{ type_name }}): + def __init__(self{% for field in variant.fields() %}, {{ field.name()|var_name }}{% endfor %}): + {%- if variant.has_fields() %} + {%- for field in variant.fields() %} + self.{{ field.name()|var_name }} = {{ field.name()|var_name }} + {%- endfor %} + {%- else %} + pass + {%- endif %} + + def __str__(self): + {%- if variant.has_fields() %} + field_parts = [ + {%- for field in variant.fields() %} + '{{ field.name()|var_name }}={!r}'.format(self.{{ field.name()|var_name }}), + {%- endfor %} + ] + return "{{ type_name }}.{{ variant_type_name }}({})".format(', '.join(field_parts)) + {%- else %} + return "{{ type_name }}.{{ variant_type_name }}()" + {%- endif %} + {%- endif %} + + {{ type_name }}.{{ variant_type_name }} = {{ variant_type_name }} + {%- endfor %} +{{ type_name }} = UniFFIExceptionTmpNamespace.{{ type_name }} +del UniFFIExceptionTmpNamespace + + +class {{ ffi_converter_name }}(FfiConverterRustBuffer): + @staticmethod + def read(buf): + variant = buf.readI32() + {%- for variant in e.variants() %} + if variant == {{ loop.index }}: + return {{ type_name }}.{{ variant.name()|class_name }}( + {%- if e.is_flat() %} + {{ Type::String.borrow()|read_fn }}(buf), + {%- else %} + {%- for field in variant.fields() %} + {{ field.name()|var_name }}={{ field|read_fn }}(buf), + {%- endfor %} + {%- endif %} + ) + {%- endfor %} + raise InternalError("Raw enum value doesn't match any cases") + + @staticmethod + def write(value, buf): + {%- for variant in e.variants() %} + if isinstance(value, {{ type_name }}.{{ variant.name()|class_name }}): + buf.writeI32({{ loop.index }}) + {%- for field in variant.fields() %} + {{ field|write_fn }}(value.{{ field.name()|var_name }}, buf) + {%- endfor %} + {%- endfor %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py new file mode 100644 index 0000000000..a5f2584c62 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py @@ -0,0 +1,7 @@ +{%- let mod_name = crate_name|fn_name %} + +{%- let ffi_converter_name = "FfiConverterType{}"|format(name) %} +{{ self.add_import_of(mod_name, ffi_converter_name) }} + +{%- let rustbuffer_local_name = "RustBuffer{}"|format(name) %} +{{ self.add_import_of_as(mod_name, "RustBuffer", rustbuffer_local_name) }} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Float32Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Float32Helper.py new file mode 100644 index 0000000000..e5783dd158 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Float32Helper.py @@ -0,0 +1,8 @@ +class FfiConverterFloat(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readFloat() + + @staticmethod + def write(value, buf): + buf.writeFloat(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Float64Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Float64Helper.py new file mode 100644 index 0000000000..57c2131eb8 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Float64Helper.py @@ -0,0 +1,8 @@ +class FfiConverterDouble(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readDouble() + + @staticmethod + def write(value, buf): + buf.writeDouble(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Helpers.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Helpers.py new file mode 100644 index 0000000000..fbce501981 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Helpers.py @@ -0,0 +1,67 @@ +# A handful of classes and functions to support the generated data structures. +# This would be a good candidate for isolating in its own ffi-support lib. + +class InternalError(Exception): + pass + +class RustCallStatus(ctypes.Structure): + """ + Error runtime. + """ + _fields_ = [ + ("code", ctypes.c_int8), + ("error_buf", RustBuffer), + ] + + # These match the values from the uniffi::rustcalls module + CALL_SUCCESS = 0 + CALL_ERROR = 1 + CALL_PANIC = 2 + + def __str__(self): + if self.code == RustCallStatus.CALL_SUCCESS: + return "RustCallStatus(CALL_SUCCESS)" + elif self.code == RustCallStatus.CALL_ERROR: + return "RustCallStatus(CALL_ERROR)" + elif self.code == RustCallStatus.CALL_PANIC: + return "RustCallStatus(CALL_PANIC)" + else: + return "RustCallStatus(<invalid code>)" + +def rust_call(fn, *args): + # Call a rust function + return rust_call_with_error(None, fn, *args) + +def rust_call_with_error(error_ffi_converter, fn, *args): + # Call a rust function and handle any errors + # + # This function is used for rust calls that return Result<> and therefore can set the CALL_ERROR status code. + # error_ffi_converter must be set to the FfiConverter for the error class that corresponds to the result. + call_status = RustCallStatus(code=RustCallStatus.CALL_SUCCESS, error_buf=RustBuffer(0, 0, None)) + + args_with_error = args + (ctypes.byref(call_status),) + result = fn(*args_with_error) + if call_status.code == RustCallStatus.CALL_SUCCESS: + return result + elif call_status.code == RustCallStatus.CALL_ERROR: + if error_ffi_converter is None: + call_status.error_buf.free() + raise InternalError("rust_call_with_error: CALL_ERROR, but error_ffi_converter is None") + else: + raise error_ffi_converter.lift(call_status.error_buf) + elif call_status.code == RustCallStatus.CALL_PANIC: + # When the rust code sees a panic, it tries to construct a RustBuffer + # with the message. But if that code panics, then it just sends back + # an empty buffer. + if call_status.error_buf.len > 0: + msg = FfiConverterString.lift(call_status.error_buf) + else: + msg = "Unknown rust panic" + raise InternalError(msg) + else: + raise InternalError("Invalid RustCallStatus code: {}".format( + call_status.code)) + +# A function pointer for a callback as defined by UniFFI. +# Rust definition `fn(handle: u64, method: u32, args: RustBuffer, buf_ptr: *mut RustBuffer) -> int` +FOREIGN_CALLBACK_T = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_ulonglong, ctypes.c_ulong, RustBuffer, ctypes.POINTER(RustBuffer)) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int16Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int16Helper.py new file mode 100644 index 0000000000..b3d9602746 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int16Helper.py @@ -0,0 +1,8 @@ +class FfiConverterInt16(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readI16() + + @staticmethod + def write(value, buf): + buf.writeI16(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int32Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int32Helper.py new file mode 100644 index 0000000000..47a54d8c4a --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int32Helper.py @@ -0,0 +1,8 @@ +class FfiConverterInt32(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readI32() + + @staticmethod + def write(value, buf): + buf.writeI32(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int64Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int64Helper.py new file mode 100644 index 0000000000..471b9967ce --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int64Helper.py @@ -0,0 +1,8 @@ +class FfiConverterInt64(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readI64() + + @staticmethod + def write(value, buf): + buf.writeI64(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int8Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int8Helper.py new file mode 100644 index 0000000000..524fea7ea9 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int8Helper.py @@ -0,0 +1,8 @@ +class FfiConverterInt8(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readI8() + + @staticmethod + def write(value, buf): + buf.writeI8(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/MapTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/MapTemplate.py new file mode 100644 index 0000000000..7f42daa7a9 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/MapTemplate.py @@ -0,0 +1,27 @@ +{%- let key_ffi_converter = key_type|ffi_converter_name %} +{%- let value_ffi_converter = value_type|ffi_converter_name %} + +class {{ ffi_converter_name }}(FfiConverterRustBuffer): + @classmethod + def write(cls, items, buf): + buf.writeI32(len(items)) + for (key, value) in items.items(): + {{ key_ffi_converter }}.write(key, buf) + {{ value_ffi_converter }}.write(value, buf) + + @classmethod + def read(cls, buf): + count = buf.readI32() + if count < 0: + raise InternalError("Unexpected negative map size") + + # It would be nice to use a dict comprehension, + # but in Python 3.7 and before the evaluation order is not according to spec, + # so we we're reading the value before the key. + # This loop makes the order explicit: first reading the key, then the value. + d = {} + for i in range(count): + key = {{ key_ffi_converter }}.read(buf) + val = {{ value_ffi_converter }}.read(buf) + d[key] = val + return d diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py new file mode 100644 index 0000000000..4867ecb9b4 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py @@ -0,0 +1,39 @@ +# This is how we find and load the dynamic library provided by the component. +# For now we just look it up by name. +# +# XXX TODO: This will probably grow some magic for resolving megazording in future. +# E.g. we might start by looking for the named component in `libuniffi.so` and if +# that fails, fall back to loading it separately from `lib${componentName}.so`. + +from pathlib import Path + +def loadIndirect(): + if sys.platform == "darwin": + libname = "lib{}.dylib" + elif sys.platform.startswith("win"): + # As of python3.8, ctypes does not seem to search $PATH when loading DLLs. + # We could use `os.add_dll_directory` to configure the search path, but + # it doesn't feel right to mess with application-wide settings. Let's + # assume that the `.dll` is next to the `.py` file and load by full path. + libname = os.path.join( + os.path.dirname(__file__), + "{}.dll", + ) + else: + # Anything else must be an ELF platform - Linux, *BSD, Solaris/illumos + libname = "lib{}.so" + + lib = libname.format("{{ config.cdylib_name() }}") + path = str(Path(__file__).parent / lib) + return ctypes.cdll.LoadLibrary(path) + +# A ctypes library to expose the extern-C FFI definitions. +# This is an implementation detail which will be called internally by the public API. + +_UniFFILib = loadIndirect() +{%- for func in ci.iter_ffi_function_definitions() %} +_UniFFILib.{{ func.name() }}.argtypes = ( + {%- call py::arg_list_ffi_decl(func) -%} +) +_UniFFILib.{{ func.name() }}.restype = {% match func.return_type() %}{% when Some with (type_) %}{{ type_|ffi_type_name }}{% when None %}None{% endmatch %} +{%- endfor %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py new file mode 100644 index 0000000000..cc4cc35b4e --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py @@ -0,0 +1,74 @@ +{%- let obj = ci.get_object_definition(name).unwrap() %} + +class {{ type_name }}(object): + {%- match obj.primary_constructor() %} + {%- when Some with (cons) %} + def __init__(self, {% call py::arg_list_decl(cons) -%}): + {%- call py::setup_args_extra_indent(cons) %} + self._pointer = {% call py::to_ffi_call(cons) %} + {%- when None %} + {%- endmatch %} + + def __del__(self): + # In case of partial initialization of instances. + pointer = getattr(self, "_pointer", None) + if pointer is not None: + rust_call(_UniFFILib.{{ obj.ffi_object_free().name() }}, pointer) + + # Used by alternative constructors or any methods which return this type. + @classmethod + def _make_instance_(cls, pointer): + # Lightly yucky way to bypass the usual __init__ logic + # and just create a new instance with the required pointer. + inst = cls.__new__(cls) + inst._pointer = pointer + return inst + + {% for cons in obj.alternate_constructors() -%} + @classmethod + def {{ cons.name()|fn_name }}(cls, {% call py::arg_list_decl(cons) %}): + {%- 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) + {% endfor %} + + {% for meth in obj.methods() -%} + {%- match meth.return_type() -%} + + {%- when Some with (return_type) -%} + def {{ meth.name()|fn_name }}(self, {% call py::arg_list_decl(meth) %}): + {%- call py::setup_args_extra_indent(meth) %} + return {{ return_type|lift_fn }}( + {% call py::to_ffi_call_with_prefix("self._pointer", meth) %} + ) + + {%- when None -%} + def {{ meth.name()|fn_name }}(self, {% call py::arg_list_decl(meth) %}): + {%- call py::setup_args_extra_indent(meth) %} + {% call py::to_ffi_call_with_prefix("self._pointer", meth) %} + {% endmatch %} + {% endfor %} + + +class {{ ffi_converter_name }}: + @classmethod + def read(cls, buf): + ptr = buf.readU64() + if ptr == 0: + raise InternalError("Raw pointer value was null") + return cls.lift(ptr) + + @classmethod + def write(cls, value, buf): + if not isinstance(value, {{ type_name }}): + raise TypeError("Expected {{ type_name }} instance, {} found".format(value.__class__.__name__)) + buf.writeU64(cls.lower(value)) + + @staticmethod + def lift(value): + return {{ type_name }}._make_instance_(value) + + @staticmethod + def lower(value): + return value._pointer diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/OptionalTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/OptionalTemplate.py new file mode 100644 index 0000000000..70f705362f --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/OptionalTemplate.py @@ -0,0 +1,21 @@ +{%- let inner_ffi_converter = inner_type|ffi_converter_name %} + +class {{ ffi_converter_name }}(FfiConverterRustBuffer): + @classmethod + def write(cls, value, buf): + if value is None: + buf.writeU8(0) + return + + buf.writeU8(1) + {{ inner_ffi_converter }}.write(value, buf) + + @classmethod + def read(cls, buf): + flag = buf.readU8() + if flag == 0: + return None + elif flag == 1: + return {{ inner_ffi_converter }}.read(buf) + else: + raise InternalError("Unexpected flag byte for optional type") diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/RecordTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/RecordTemplate.py new file mode 100644 index 0000000000..2009d59e8a --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/RecordTemplate.py @@ -0,0 +1,45 @@ +{%- let rec = ci.get_record_definition(name).unwrap() %} +class {{ type_name }}: + + def __init__(self, {% for field in rec.fields() %} + {{- field.name()|var_name }} + {%- if field.default_value().is_some() %} = DEFAULT{% endif %} + {%- if !loop.last %}, {% endif %} + {%- endfor %}): + {%- for field in rec.fields() %} + {%- let field_name = field.name()|var_name %} + {%- match field.default_value() %} + {%- when None %} + self.{{ field_name }} = {{ field_name }} + {%- when Some with(literal) %} + if {{ field_name }} is DEFAULT: + self.{{ field_name }} = {{ literal|literal_py(field) }} + else: + self.{{ field_name }} = {{ field_name }} + {%- endmatch %} + {%- endfor %} + + def __str__(self): + return "{{ type_name }}({% for field in rec.fields() %}{{ field.name()|var_name }}={}{% if loop.last %}{% else %}, {% endif %}{% endfor %})".format({% for field in rec.fields() %}self.{{ field.name()|var_name }}{% if loop.last %}{% else %}, {% endif %}{% endfor %}) + + def __eq__(self, other): + {%- for field in rec.fields() %} + if self.{{ field.name()|var_name }} != other.{{ field.name()|var_name }}: + return False + {%- endfor %} + return True + +class {{ ffi_converter_name }}(FfiConverterRustBuffer): + @staticmethod + def read(buf): + return {{ type_name }}( + {%- for field in rec.fields() %} + {{ field.name()|var_name }}={{ field|read_fn }}(buf), + {%- endfor %} + ) + + @staticmethod + def write(value, buf): + {%- for field in rec.fields() %} + {{ field|write_fn }}(value.{{ field.name()|var_name }}, buf) + {%- endfor %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/RustBufferHelper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/RustBufferHelper.py new file mode 100644 index 0000000000..eafd2346bb --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/RustBufferHelper.py @@ -0,0 +1,23 @@ +# Types conforming to `FfiConverterPrimitive` pass themselves directly over the FFI. +class FfiConverterPrimitive: + @classmethod + def lift(cls, value): + return value + + @classmethod + def lower(cls, value): + return value + +# Helper class for wrapper types that will always go through a RustBuffer. +# Classes should inherit from this and implement the `read` and `write` static methods. +class FfiConverterRustBuffer: + @classmethod + def lift(cls, rbuf): + with rbuf.consumeWithStream() as stream: + return cls.read(stream) + + @classmethod + def lower(cls, value): + with RustBuffer.allocWithBuilder() as builder: + cls.write(value, builder) + return builder.finalize() diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py new file mode 100644 index 0000000000..95a3b1f706 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py @@ -0,0 +1,190 @@ + +class RustBuffer(ctypes.Structure): + _fields_ = [ + ("capacity", ctypes.c_int32), + ("len", ctypes.c_int32), + ("data", ctypes.POINTER(ctypes.c_char)), + ] + + @staticmethod + def alloc(size): + return rust_call(_UniFFILib.{{ ci.ffi_rustbuffer_alloc().name() }}, size) + + @staticmethod + def reserve(rbuf, additional): + return rust_call(_UniFFILib.{{ ci.ffi_rustbuffer_reserve().name() }}, rbuf, additional) + + def free(self): + return rust_call(_UniFFILib.{{ ci.ffi_rustbuffer_free().name() }}, self) + + def __str__(self): + return "RustBuffer(capacity={}, len={}, data={})".format( + self.capacity, + self.len, + self.data[0:self.len] + ) + + @contextlib.contextmanager + def allocWithBuilder(): + """Context-manger to allocate a buffer using a RustBufferBuilder. + + The allocated buffer will be automatically freed if an error occurs, ensuring that + we don't accidentally leak it. + """ + builder = RustBufferBuilder() + try: + yield builder + except: + builder.discard() + raise + + @contextlib.contextmanager + def consumeWithStream(self): + """Context-manager to consume a buffer using a RustBufferStream. + + The RustBuffer will be freed once the context-manager exits, ensuring that we don't + leak it even if an error occurs. + """ + try: + s = RustBufferStream(self) + yield s + if s.remaining() != 0: + raise RuntimeError("junk data left in buffer after consuming") + finally: + self.free() + + +class ForeignBytes(ctypes.Structure): + _fields_ = [ + ("len", ctypes.c_int32), + ("data", ctypes.POINTER(ctypes.c_char)), + ] + + def __str__(self): + return "ForeignBytes(len={}, data={})".format(self.len, self.data[0:self.len]) + + +class RustBufferStream(object): + """ + Helper for structured reading of bytes from a RustBuffer + """ + + def __init__(self, rbuf): + self.rbuf = rbuf + self.offset = 0 + + def remaining(self): + return self.rbuf.len - self.offset + + def _unpack_from(self, size, format): + if self.offset + size > self.rbuf.len: + raise InternalError("read past end of rust buffer") + value = struct.unpack(format, self.rbuf.data[self.offset:self.offset+size])[0] + self.offset += size + return value + + def read(self, size): + if self.offset + size > self.rbuf.len: + raise InternalError("read past end of rust buffer") + data = self.rbuf.data[self.offset:self.offset+size] + self.offset += size + return data + + def readI8(self): + return self._unpack_from(1, ">b") + + def readU8(self): + return self._unpack_from(1, ">B") + + def readI16(self): + return self._unpack_from(2, ">h") + + def readU16(self): + return self._unpack_from(2, ">H") + + def readI32(self): + return self._unpack_from(4, ">i") + + def readU32(self): + return self._unpack_from(4, ">I") + + def readI64(self): + return self._unpack_from(8, ">q") + + def readU64(self): + return self._unpack_from(8, ">Q") + + def readFloat(self): + v = self._unpack_from(4, ">f") + return v + + def readDouble(self): + return self._unpack_from(8, ">d") + + +class RustBufferBuilder(object): + """ + Helper for structured writing of bytes into a RustBuffer. + """ + + def __init__(self): + self.rbuf = RustBuffer.alloc(16) + self.rbuf.len = 0 + + def finalize(self): + rbuf = self.rbuf + self.rbuf = None + return rbuf + + def discard(self): + if self.rbuf is not None: + rbuf = self.finalize() + rbuf.free() + + @contextlib.contextmanager + def _reserve(self, numBytes): + if self.rbuf.len + numBytes > self.rbuf.capacity: + self.rbuf = RustBuffer.reserve(self.rbuf, numBytes) + yield None + self.rbuf.len += numBytes + + def _pack_into(self, size, format, value): + with self._reserve(size): + # XXX TODO: I feel like I should be able to use `struct.pack_into` here but can't figure it out. + for i, byte in enumerate(struct.pack(format, value)): + self.rbuf.data[self.rbuf.len + i] = byte + + def write(self, value): + with self._reserve(len(value)): + for i, byte in enumerate(value): + self.rbuf.data[self.rbuf.len + i] = byte + + def writeI8(self, v): + self._pack_into(1, ">b", v) + + def writeU8(self, v): + self._pack_into(1, ">B", v) + + def writeI16(self, v): + self._pack_into(2, ">h", v) + + def writeU16(self, v): + self._pack_into(2, ">H", v) + + def writeI32(self, v): + self._pack_into(4, ">i", v) + + def writeU32(self, v): + self._pack_into(4, ">I", v) + + def writeI64(self, v): + self._pack_into(8, ">q", v) + + def writeU64(self, v): + self._pack_into(8, ">Q", v) + + def writeFloat(self, v): + self._pack_into(4, ">f", v) + + def writeDouble(self, v): + self._pack_into(8, ">d", v) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/SequenceTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/SequenceTemplate.py new file mode 100644 index 0000000000..6de7bf2d4d --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/SequenceTemplate.py @@ -0,0 +1,19 @@ +{%- let inner_ffi_converter = inner_type|ffi_converter_name %} + +class {{ ffi_converter_name}}(FfiConverterRustBuffer): + @classmethod + def write(cls, value, buf): + items = len(value) + buf.writeI32(items) + for item in value: + {{ inner_ffi_converter }}.write(item, buf) + + @classmethod + def read(cls, buf): + count = buf.readI32() + if count < 0: + raise InternalError("Unexpected negative sequence length") + + return [ + {{ inner_ffi_converter }}.read(buf) for i in range(count) + ] diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/StringHelper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/StringHelper.py new file mode 100644 index 0000000000..f205f7c746 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/StringHelper.py @@ -0,0 +1,25 @@ +class FfiConverterString: + @staticmethod + def read(buf): + size = buf.readI32() + if size < 0: + raise InternalError("Unexpected negative string length") + utf8Bytes = buf.read(size) + return utf8Bytes.decode("utf-8") + + @staticmethod + def write(value, buf): + utf8Bytes = value.encode("utf-8") + buf.writeI32(len(utf8Bytes)) + buf.write(utf8Bytes) + + @staticmethod + def lift(buf): + with buf.consumeWithStream() as stream: + return stream.read(stream.remaining()).decode("utf-8") + + @staticmethod + def lower(value): + with RustBuffer.allocWithBuilder() as builder: + builder.write(value.encode("utf-8")) + return builder.finalize() diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/TimestampHelper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/TimestampHelper.py new file mode 100644 index 0000000000..ac87697551 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/TimestampHelper.py @@ -0,0 +1,30 @@ +# The Timestamp type. +# 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 FfiConverterTimestamp(FfiConverterRustBuffer): + @staticmethod + def read(buf): + seconds = buf.readI64() + microseconds = buf.readU32() / 1000 + # Use fromtimestamp(0) then add the seconds using a timedelta. This + # ensures that we get OverflowError rather than ValueError when + # seconds is too large. + if seconds >= 0: + return datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc) + datetime.timedelta(seconds=seconds, microseconds=microseconds) + else: + return datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc) - datetime.timedelta(seconds=-seconds, microseconds=microseconds) + + @staticmethod + def write(value, buf): + if value >= datetime.datetime.fromtimestamp(0, datetime.timezone.utc): + sign = 1 + delta = value - datetime.datetime.fromtimestamp(0, datetime.timezone.utc) + else: + sign = -1 + delta = datetime.datetime.fromtimestamp(0, datetime.timezone.utc) - value + + seconds = delta.seconds + delta.days * 24 * 3600 + nanoseconds = delta.microseconds * 1000 + buf.writeI64(sign * seconds) + buf.writeU32(nanoseconds) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py new file mode 100644 index 0000000000..2d0e09f22d --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py @@ -0,0 +1,13 @@ +{%- match func.return_type() -%} +{%- when Some with (return_type) %} + +def {{ func.name()|fn_name }}({%- call py::arg_list_decl(func) -%}): + {%- call py::setup_args(func) %} + return {{ return_type|lift_fn }}({% call py::to_ffi_call(func) %}) + +{% when None %} + +def {{ func.name()|fn_name }}({%- call py::arg_list_decl(func) -%}): + {%- call py::setup_args(func) %} + {% call py::to_ffi_call(func) %} +{% endmatch %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Types.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Types.py new file mode 100644 index 0000000000..ce65472a64 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Types.py @@ -0,0 +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 %} +{%- include "BooleanHelper.py" %} + +{%- when Type::Int8 %} +{%- include "Int8Helper.py" %} + +{%- when Type::Int16 %} +{%- include "Int16Helper.py" %} + +{%- when Type::Int32 %} +{%- include "Int32Helper.py" %} + +{%- when Type::Int64 %} +{%- include "Int64Helper.py" %} + +{%- when Type::UInt8 %} +{%- include "UInt8Helper.py" %} + +{%- when Type::UInt16 %} +{%- include "UInt16Helper.py" %} + +{%- when Type::UInt32 %} +{%- include "UInt32Helper.py" %} + +{%- when Type::UInt64 %} +{%- include "UInt64Helper.py" %} + +{%- when Type::Float32 %} +{%- include "Float32Helper.py" %} + +{%- when Type::Float64 %} +{%- include "Float64Helper.py" %} + +{%- when Type::String %} +{%- include "StringHelper.py" %} + +{%- when Type::Enum(name) %} +{%- include "EnumTemplate.py" %} + +{%- when Type::Error(name) %} +{%- include "ErrorTemplate.py" %} + +{%- when Type::Record(name) %} +{%- include "RecordTemplate.py" %} + +{%- when Type::Object(name) %} +{%- include "ObjectTemplate.py" %} + +{%- when Type::Timestamp %} +{%- include "TimestampHelper.py" %} + +{%- when Type::Duration %} +{%- include "DurationHelper.py" %} + +{%- when Type::Optional(inner_type) %} +{%- include "OptionalTemplate.py" %} + +{%- when Type::Sequence(inner_type) %} +{%- include "SequenceTemplate.py" %} + +{%- when Type::Map(key_type, value_type) %} +{%- include "MapTemplate.py" %} + +{%- when Type::CallbackInterface(id) %} +{%- include "CallbackInterfaceTemplate.py" %} + +{%- when Type::Custom { name, builtin } %} +{%- include "CustomType.py" %} + +{%- when Type::External { name, crate_name } %} +{%- include "ExternalTemplate.py" %} + +{%- else %} +{%- endmatch %} +{%- endfor %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt16Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt16Helper.py new file mode 100644 index 0000000000..f1cd114c0c --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt16Helper.py @@ -0,0 +1,8 @@ +class FfiConverterUInt16(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readU16() + + @staticmethod + def write(value, buf): + buf.writeU16(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt32Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt32Helper.py new file mode 100644 index 0000000000..290cd5fe82 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt32Helper.py @@ -0,0 +1,8 @@ +class FfiConverterUInt32(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readU32() + + @staticmethod + def write(value, buf): + buf.writeU32(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt64Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt64Helper.py new file mode 100644 index 0000000000..f57dec1d92 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt64Helper.py @@ -0,0 +1,8 @@ +class FfiConverterUInt64(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readU64() + + @staticmethod + def write(value, buf): + buf.writeU64(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt8Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt8Helper.py new file mode 100644 index 0000000000..3a1582351b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt8Helper.py @@ -0,0 +1,8 @@ +class FfiConverterUInt8(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readU8() + + @staticmethod + def write(value, buf): + buf.writeU8(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/macros.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/macros.py new file mode 100644 index 0000000000..d11fcae921 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/macros.py @@ -0,0 +1,101 @@ +{# +// 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_ffi_call` +#} + +{%- macro to_ffi_call(func) -%} + {%- match func.throws_type() -%} + {%- when Some with (e) -%} +rust_call_with_error({{ e|ffi_converter_name }}, + {%- else -%} +rust_call( + {%- endmatch -%} + _UniFFILib.{{ func.ffi_func().name() }}, + {%- call _arg_list_ffi_call(func) -%} +) +{%- endmacro -%} + +{%- macro to_ffi_call_with_prefix(prefix, func) -%} + {%- match func.throws_type() -%} + {%- when Some with (e) -%} +rust_call_with_error( + {{ e|ffi_converter_name }}, + {%- else -%} +rust_call( + {%- endmatch -%} + _UniFFILib.{{ func.ffi_func().name() }}, + {{- prefix }}, + {%- call _arg_list_ffi_call(func) -%} +) +{%- endmacro -%} + +{%- macro _arg_list_ffi_call(func) %} + {%- for arg in func.arguments() %} + {{ arg|lower_fn }}({{ arg.name()|var_name }}) + {%- if !loop.last %},{% endif %} + {%- endfor %} +{%- endmacro -%} + +{#- +// Arglist as used in Python declarations of methods, functions and constructors. +// Note the var_name and type_name filters. +-#} + +{% macro arg_list_decl(func) %} + {%- for arg in func.arguments() -%} + {{ arg.name()|var_name }} + {%- match arg.default_value() %} + {%- when Some with(literal) %} = DEFAULT + {%- else %} + {%- 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 %} + ctypes.POINTER(RustCallStatus), +{% endmacro -%} + +{# + # Setup function arguments by initializing default values and passing other + # values through coerce. + #} +{%- macro setup_args(func) %} + {%- for arg in func.arguments() %} + {%- match arg.default_value() %} + {%- when None %} + {{ arg.name()|var_name }} = {{ arg.name()|var_name|coerce_py(arg.type_().borrow()) -}} + {%- when Some with(literal) %} + if {{ arg.name()|var_name }} is DEFAULT: + {{ arg.name()|var_name }} = {{ literal|literal_py(arg.type_().borrow()) }} + else: + {{ arg.name()|var_name }} = {{ arg.name()|var_name|coerce_py(arg.type_().borrow()) -}} + {%- endmatch %} + {% endfor -%} +{%- endmacro -%} + +{# + # 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() %} + {%- match arg.default_value() %} + {%- when None %} + {{ arg.name()|var_name }} = {{ arg.name()|var_name|coerce_py(arg.type_().borrow()) -}} + {%- when Some with(literal) %} + if {{ arg.name()|var_name }} is DEFAULT: + {{ arg.name()|var_name }} = {{ literal|literal_py(arg.type_().borrow()) }} + else: + {{ arg.name()|var_name }} = {{ arg.name()|var_name|coerce_py(arg.type_().borrow()) -}} + {%- endmatch %} + {% endfor -%} +{%- endmacro -%} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/wrapper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/wrapper.py new file mode 100644 index 0000000000..d30d3c9d12 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/wrapper.py @@ -0,0 +1,71 @@ +# This file was autogenerated by some hot garbage in the `uniffi` crate. +# Trust me, you don't want to mess with it! + +# Tell mypy (a type checker) to ignore all errors from this file. +# See https://mypy.readthedocs.io/en/stable/config_file.html?highlight=ignore-errors#confval-ignore_errors +# mypy: ignore-errors + +# Common helper code. +# +# Ideally this would live in a separate .py file where it can be unittested etc +# in isolation, and perhaps even published as a re-useable package. +# +# However, it's important that the details of how this helper code works (e.g. the +# way that different builtin types are passed across the FFI) exactly match what's +# expected by the rust code on the other side of the interface. In practice right +# now that means coming from the exact some version of `uniffi` that was used to +# compile the rust component. The easiest way to ensure this is to bundle the Python +# helpers directly inline like we're doing here. + +import os +import sys +import ctypes +import enum +import struct +import contextlib +import datetime +{%- for req in self.imports() %} +{{ req.render() }} +{%- endfor %} + +# Used for default argument values +DEFAULT = object() + +{% include "RustBufferTemplate.py" %} +{% include "Helpers.py" %} +{% include "RustBufferHelper.py" %} + +# Contains loading, initialization code, +# and the FFI Function declarations in a com.sun.jna.Library. +{% include "NamespaceLibraryTemplate.py" %} + +# Public interface members begin here. +{{ type_helper_code }} + +{%- for func in ci.function_definitions() %} +{%- include "TopLevelFunctionTemplate.py" %} +{%- endfor %} + +__all__ = [ + "InternalError", + {%- for e in ci.enum_definitions() %} + "{{ e|type_name }}", + {%- endfor %} + {%- for record in ci.record_definitions() %} + "{{ record|type_name }}", + {%- endfor %} + {%- for func in ci.function_definitions() %} + "{{ func.name()|fn_name }}", + {%- endfor %} + {%- for obj in ci.object_definitions() %} + "{{ obj|type_name }}", + {%- endfor %} + {%- for e in ci.error_definitions() %} + "{{ e|type_name }}", + {%- endfor %} + {%- for c in ci.callback_interface_definitions() %} + "{{ c.name()|class_name }}", + {%- endfor %} +] + +{% import "macros.py" as py %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/test.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/test.rs new file mode 100644 index 0000000000..d050fc0389 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/test.rs @@ -0,0 +1,62 @@ +/* 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 anyhow::{Context, Result}; +use camino::Utf8Path; +use std::env; +use std::ffi::OsString; +use std::process::{Command, Stdio}; +use uniffi_testing::UniFFITestHelper; + +/// Run Python tests for a UniFFI test fixture +pub fn run_test(tmp_dir: &str, fixture_name: &str, script_file: &str) -> Result<()> { + let script_path = Utf8Path::new(".").join(script_file).canonicalize_utf8()?; + let test_helper = UniFFITestHelper::new(fixture_name).context("UniFFITestHelper::new")?; + let out_dir = test_helper + .create_out_dir(tmp_dir, &script_path) + .context("create_out_dir")?; + test_helper + .copy_cdylibs_to_out_dir(&out_dir) + .context("copy_cdylibs_to_out_dir")?; + generate_sources(&test_helper.cdylib_path()?, &out_dir, &test_helper) + .context("generate_sources")?; + + let pythonpath = env::var_os("PYTHONPATH").unwrap_or_else(|| OsString::from("")); + let pythonpath = env::join_paths( + env::split_paths(&pythonpath).chain(vec![out_dir.to_path_buf().into_std_path_buf()]), + )?; + + let status = Command::new("python3") + .current_dir(out_dir) + .env("PYTHONPATH", pythonpath) + .arg(script_path) + .stderr(Stdio::inherit()) + .stdout(Stdio::inherit()) + .spawn() + .context("Failed to spawn `python3` when running script")? + .wait() + .context("Failed to wait for `python3` when running script")?; + if !status.success() { + anyhow::bail!("running `python3` failed"); + } + Ok(()) +} + +fn generate_sources( + library_path: &Utf8Path, + out_dir: &Utf8Path, + test_helper: &UniFFITestHelper, +) -> Result<()> { + for source in test_helper.get_compile_sources()? { + crate::generate_bindings( + &source.udl_path, + source.config_path.as_deref(), + vec!["python"], + Some(out_dir), + Some(library_path), + false, + )?; + } + Ok(()) +} |