diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/rust/uniffi_bindgen/src/bindings | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/uniffi_bindgen/src/bindings')
154 files changed, 9450 insertions, 0 deletions
diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/callback_interface.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/callback_interface.rs new file mode 100644 index 0000000000..c0c7ab29b9 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/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!("Type{}", self.id) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!(); + } + + fn initialization_fn(&self, oracle: &dyn CodeOracle) -> Option<String> { + Some(format!("{}.register", self.ffi_converter_name(oracle))) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/compounds.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/compounds.rs new file mode 100644 index 0000000000..7d5f77141f --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/compounds.rs @@ -0,0 +1,94 @@ +/* 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}; +use paste::paste; + +fn render_literal(oracle: &dyn CodeOracle, literal: &Literal, inner: &TypeIdentifier) -> String { + match literal { + Literal::Null => "null".into(), + Literal::EmptySequence => "listOf()".into(), + Literal::EmptyMap => "mapOf()".into(), + + // For optionals + _ => oracle.find(inner).literal(oracle, literal), + } +} + +macro_rules! impl_code_type_for_compound { + ($T:ty, $type_label_pattern:literal, $canonical_name_pattern: literal) => { + paste! { + pub struct $T { + inner: TypeIdentifier, + } + + impl $T { + pub fn new(inner: TypeIdentifier) -> Self { + Self { inner } + } + fn inner(&self) -> &TypeIdentifier { + &self.inner + } + } + + impl CodeType for $T { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + format!($type_label_pattern, oracle.find(self.inner()).type_label(oracle)) + } + + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + format!($canonical_name_pattern, oracle.find(self.inner()).canonical_name(oracle)) + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + render_literal(oracle, literal, self.inner()) + } + } + } + } + } + +impl_code_type_for_compound!(OptionalCodeType, "{}?", "Optional{}"); +impl_code_type_for_compound!(SequenceCodeType, "List<{}>", "Sequence{}"); + +pub struct MapCodeType { + key: TypeIdentifier, + value: TypeIdentifier, +} + +impl MapCodeType { + pub fn new(key: TypeIdentifier, value: TypeIdentifier) -> Self { + Self { key, value } + } + + fn key(&self) -> &TypeIdentifier { + &self.key + } + + fn value(&self) -> &TypeIdentifier { + &self.value + } +} + +impl CodeType for MapCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + format!( + "Map<{}, {}>", + self.key().type_label(oracle), + self.value().type_label(oracle), + ) + } + + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + format!( + "Map{}{}", + self.key().type_label(oracle), + self.value().type_label(oracle), + ) + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + render_literal(oracle, literal, &self.value) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/custom.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/custom.rs new file mode 100644 index 0000000000..4a5636a228 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/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, Literal}; + +pub struct CustomCodeType { + name: String, +} + +impl CustomCodeType { + pub fn new(name: String) -> Self { + CustomCodeType { 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 literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!("Can't have a literal of a custom type"); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/enum_.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/enum_.rs new file mode 100644 index 0000000000..941008e370 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/enum_.rs @@ -0,0 +1,37 @@ +/* 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!(); + } + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/error.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/error.rs new file mode 100644 index 0000000000..3ab5f0ac9f --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/error.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, 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.error_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!(); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/external.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/external.rs new file mode 100644 index 0000000000..32d47cb3f1 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/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, Literal}; + +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 literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!("Can't have a literal of an external type"); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/miscellany.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/miscellany.rs new file mode 100644 index 0000000000..57b9a145e7 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/miscellany.rs @@ -0,0 +1,32 @@ +/* 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, $class_name:literal, $canonical_name:literal) => { + paste! { + pub struct $T; + + impl CodeType for $T { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + $class_name.into() + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + $canonical_name.into() + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!() + } + } + } + }; +} + +impl_code_type_for_miscellany!(TimestampCodeType, "java.time.Instant", "Timestamp"); + +impl_code_type_for_miscellany!(DurationCodeType, "java.time.Duration", "Duration"); diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs new file mode 100644 index 0000000000..8fe12f30c7 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs @@ -0,0 +1,417 @@ +/* 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::borrow::Borrow; +use std::cell::RefCell; +use std::collections::{BTreeSet, HashMap, HashSet}; + +use anyhow::{Context, Result}; +use askama::Template; +use heck::{ToLowerCamelCase, ToShoutySnakeCase, ToUpperCamelCase}; +use serde::{Deserialize, Serialize}; + +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; + +// config options to customize the generated Kotlin. +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct Config { + package_name: Option<String>, + cdylib_name: Option<String>, + #[serde(default)] + custom_types: HashMap<String, CustomTypeConfig>, + #[serde(default)] + external_packages: HashMap<String, String>, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct CustomTypeConfig { + imports: Option<Vec<String>>, + type_name: Option<String>, + into_custom: TemplateExpression, + from_custom: TemplateExpression, +} + +impl Config { + pub fn package_name(&self) -> String { + if let Some(package_name) = &self.package_name { + package_name.clone() + } else { + "uniffi".into() + } + } + + 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 { + package_name: Some(format!("uniffi.{}", ci.namespace())), + cdylib_name: Some(format!("uniffi_{}", ci.namespace())), + custom_types: HashMap::new(), + external_packages: HashMap::new(), + } + } +} + +impl MergeWith for Config { + fn merge_with(&self, other: &Self) -> Self { + Config { + package_name: self.package_name.merge_with(&other.package_name), + cdylib_name: self.cdylib_name.merge_with(&other.cdylib_name), + custom_types: self.custom_types.merge_with(&other.custom_types), + external_packages: self.external_packages.merge_with(&other.external_packages), + } + } +} + +// Generate kotlin bindings for the given ComponentInterface, as a string. +pub fn generate_bindings(config: &Config, ci: &ComponentInterface) -> Result<String> { + KotlinWrapper::new(config.clone(), ci) + .render() + .context("failed to render kotlin bindings") +} + +/// A struct to record a Kotlin import statement. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum ImportRequirement { + /// The name we are importing. + Import { name: String }, + /// Import the name with the specified local name. + ImportAs { name: String, as_name: String }, +} + +impl ImportRequirement { + /// Render the Kotlin import statement. + fn render(&self) -> String { + match &self { + ImportRequirement::Import { name } => format!("import {name}"), + ImportRequirement::ImportAs { name, as_name } => { + format!("import {name} as {as_name}") + } + } + } +} + +/// Renders Kotlin 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 = "kt", escape = "none", path = "Types.kt")] +pub struct TypeRenderer<'a> { + kotlin_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(kotlin_config: &'a Config, ci: &'a ComponentInterface) -> Self { + Self { + kotlin_config, + ci, + include_once_names: RefCell::new(HashSet::new()), + imports: RefCell::new(BTreeSet::new()), + } + } + + // Get the package name for an external type + fn external_type_package_name(&self, crate_name: &str) -> String { + match self.kotlin_config.external_packages.get(crate_name) { + Some(name) => name.clone(), + None => crate_name.to_string(), + } + } + + // The following methods are used by the `Types.kt` 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::Import { + name: name.to_owned(), + }); + "" + } + + // Like add_import, but arranges for `import name as as_name` + fn add_import_as(&self, name: &str, as_name: &str) -> &str { + self.imports + .borrow_mut() + .insert(ImportRequirement::ImportAs { + name: name.to_owned(), + as_name: as_name.to_owned(), + }); + "" + } +} + +#[derive(Template)] +#[template(syntax = "kt", escape = "none", path = "wrapper.kt")] +pub struct KotlinWrapper<'a> { + config: Config, + ci: &'a ComponentInterface, + type_helper_code: String, + type_imports: BTreeSet<ImportRequirement>, +} + +impl<'a> KotlinWrapper<'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 initialization_fns(&self) -> Vec<String> { + self.ci + .iter_types() + .filter_map(|t| t.initialization_fn(&KotlinCodeOracle)) + .collect() + } + + pub fn imports(&self) -> Vec<ImportRequirement> { + self.type_imports.iter().cloned().collect() + } +} + +#[derive(Clone)] +pub struct KotlinCodeOracle; + +impl KotlinCodeOracle { + // Map `Type` instances to a `Box<dyn CodeType>` for that type. + // + // There is a companion match in `templates/Types.kt` 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.kt` 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 KotlinCodeOracle { + fn find(&self, type_: &TypeIdentifier) -> Box<dyn CodeType> { + self.create_code_type(type_.clone()) + } + + /// Get the idiomatic Kotlin rendering of a class name (for enums, records, errors, etc). + fn class_name(&self, nm: &str) -> String { + nm.to_string().to_upper_camel_case() + } + + /// Get the idiomatic Kotlin rendering of a function name. + fn fn_name(&self, nm: &str) -> String { + format!("`{}`", nm.to_string().to_lower_camel_case()) + } + + /// Get the idiomatic Kotlin rendering of a variable name. + fn var_name(&self, nm: &str) -> String { + format!("`{}`", nm.to_string().to_lower_camel_case()) + } + + /// Get the idiomatic Kotlin rendering of an individual enum variant. + fn enum_variant_name(&self, nm: &str) -> String { + nm.to_string().to_shouty_snake_case() + } + + /// Get the idiomatic Kotlin rendering of an exception name + /// + /// This replaces "Error" at the end of the name with "Exception". Rust code typically uses + /// "Error" for any type of error but in the Java world, "Error" means a non-recoverable error + /// and is distinguished from an "Exception". + fn error_name(&self, nm: &str) -> String { + // errors are a class in kotlin. + let name = 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 { + // Note that unsigned integers in Kotlin are currently experimental, but java.nio.ByteBuffer does not + // support them yet. Thus, we use the signed variants to represent both signed and unsigned + // types from the component API. + FfiType::Int8 | FfiType::UInt8 => "Byte".to_string(), + FfiType::Int16 | FfiType::UInt16 => "Short".to_string(), + FfiType::Int32 | FfiType::UInt32 => "Int".to_string(), + FfiType::Int64 | FfiType::UInt64 => "Long".to_string(), + FfiType::Float32 => "Float".to_string(), + FfiType::Float64 => "Double".to_string(), + FfiType::RustArcPtr(_) => "Pointer".to_string(), + FfiType::RustBuffer(maybe_suffix) => match maybe_suffix { + Some(suffix) => format!("RustBuffer{}.ByValue", suffix), + None => "RustBuffer.ByValue".to_string(), + }, + FfiType::ForeignBytes => "ForeignBytes.ByValue".to_string(), + FfiType::ForeignCallback => "ForeignCallback".to_string(), + } + } +} + +pub mod filters { + use super::*; + + fn oracle() -> &'static KotlinCodeOracle { + &KotlinCodeOracle + } + + pub fn type_name(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(codetype.type_label(oracle())) + } + + pub fn canonical_name(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(codetype.canonical_name(oracle())) + } + + pub fn ffi_converter_name(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(codetype.ffi_converter_name(oracle())) + } + + pub fn lower_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(format!("{}.lower", codetype.ffi_converter_name(oracle()))) + } + + pub fn allocation_size_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(format!( + "{}.allocationSize", + codetype.ffi_converter_name(oracle()) + )) + } + + pub fn write_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(format!("{}.write", codetype.ffi_converter_name(oracle()))) + } + + pub fn lift_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(format!("{}.lift", codetype.ffi_converter_name(oracle()))) + } + + pub fn read_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(format!("{}.read", codetype.ffi_converter_name(oracle()))) + } + + pub fn render_literal( + literal: &Literal, + codetype: &impl CodeType, + ) -> Result<String, askama::Error> { + Ok(codetype.literal(oracle(), literal)) + } + + /// Get the Kotlin 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 Kotlin 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 Kotlin rendering of a function name. + pub fn fn_name(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().fn_name(nm)) + } + + /// Get the idiomatic Kotlin rendering of a variable name. + pub fn var_name(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().var_name(nm)) + } + + /// Get the idiomatic Kotlin rendering of an individual enum variant. + pub fn enum_variant(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().enum_variant_name(nm)) + } + + /// Get the idiomatic Kotlin rendering of an exception name, replacing + /// `Error` with `Exception`. + pub fn exception_name(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().error_name(nm)) + } + + /// Remove the "`" chars we put around function/variable names + /// + /// These are used to avoid name clashes with kotlin identifiers, but sometimes you want to + /// render the name unquoted. One example is the message property for errors where we want to + /// display the name for the user. + pub fn unquote(nm: &str) -> Result<String, askama::Error> { + Ok(nm.trim_matches('`').to_string()) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/object.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/object.rs new file mode 100644 index 0000000000..7a1cddbafd --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/object.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, 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!(); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/primitives.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/primitives.rs new file mode 100644 index 0000000000..afc8fc556d --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/primitives.rs @@ -0,0 +1,79 @@ +/* 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::{types::Type, Radix}; +use paste::paste; + +fn render_literal(_oracle: &dyn CodeOracle, literal: &Literal) -> String { + fn typed_number(type_: &Type, num_str: String) -> String { + match type_ { + // Bytes, Shorts and Ints can all be inferred from the type. + Type::Int8 | Type::Int16 | Type::Int32 => num_str, + Type::Int64 => format!("{num_str}L"), + + Type::UInt8 | Type::UInt16 | Type::UInt32 => format!("{num_str}u"), + Type::UInt64 => format!("{num_str}uL"), + + Type::Float32 => format!("{num_str}f"), + Type::Float64 => num_str, + _ => panic!("Unexpected literal: {num_str} is not a number"), + } + } + + match literal { + Literal::Boolean(v) => format!("{v}"), + Literal::String(s) => format!("\"{s}\""), + Literal::Int(i, radix, type_) => typed_number( + type_, + match radix { + Radix::Octal => format!("{i:#x}"), + Radix::Decimal => format!("{i}"), + Radix::Hexadecimal => format!("{i:#x}"), + }, + ), + Literal::UInt(i, radix, type_) => typed_number( + type_, + match radix { + Radix::Octal => format!("{i:#x}"), + Radix::Decimal => format!("{i}"), + Radix::Hexadecimal => format!("{i:#x}"), + }, + ), + Literal::Float(string, type_) => typed_number(type_, string.clone()), + + _ => unreachable!("Literal"), + } +} + +macro_rules! impl_code_type_for_primitive { + ($T:ty, $class_name:literal) => { + 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) + } + } + } + }; +} + +impl_code_type_for_primitive!(BooleanCodeType, "Boolean"); +impl_code_type_for_primitive!(StringCodeType, "String"); +impl_code_type_for_primitive!(Int8CodeType, "Byte"); +impl_code_type_for_primitive!(Int16CodeType, "Short"); +impl_code_type_for_primitive!(Int32CodeType, "Int"); +impl_code_type_for_primitive!(Int64CodeType, "Long"); +impl_code_type_for_primitive!(UInt8CodeType, "UByte"); +impl_code_type_for_primitive!(UInt16CodeType, "UShort"); +impl_code_type_for_primitive!(UInt32CodeType, "UInt"); +impl_code_type_for_primitive!(UInt64CodeType, "ULong"); +impl_code_type_for_primitive!(Float32CodeType, "Float"); +impl_code_type_for_primitive!(Float64CodeType, "Double"); diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/record.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/record.rs new file mode 100644 index 0000000000..f923f102c4 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/record.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, 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!(); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/mod.rs new file mode 100644 index 0000000000..c372eab83b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/mod.rs @@ -0,0 +1,43 @@ +/* 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::Result; +use camino::{Utf8Path, Utf8PathBuf}; +use fs_err::{self as fs, File}; +use std::{io::Write, process::Command}; + +pub mod gen_kotlin; +pub use gen_kotlin::{generate_bindings, Config}; +mod test; + +use super::super::interface::ComponentInterface; +pub use test::run_test; + +pub fn write_bindings( + config: &Config, + ci: &ComponentInterface, + out_dir: &Utf8Path, + try_format_code: bool, +) -> Result<()> { + let mut kt_file = full_bindings_path(config, out_dir); + fs::create_dir_all(&kt_file)?; + kt_file.push(format!("{}.kt", ci.namespace())); + let mut f = File::create(&kt_file)?; + write!(f, "{}", generate_bindings(config, ci)?)?; + if try_format_code { + if let Err(e) = Command::new("ktlint").arg("-F").arg(&kt_file).output() { + println!( + "Warning: Unable to auto-format {} using ktlint: {:?}", + kt_file.file_name().unwrap(), + e + ) + } + } + Ok(()) +} + +fn full_bindings_path(config: &Config, out_dir: &Utf8Path) -> Utf8PathBuf { + let package_path: Utf8PathBuf = config.package_name().split('.').collect(); + Utf8PathBuf::from(out_dir).join(package_path) +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/BooleanHelper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/BooleanHelper.kt new file mode 100644 index 0000000000..8cfa2ce000 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/BooleanHelper.kt @@ -0,0 +1,19 @@ +public object FfiConverterBoolean: FfiConverter<Boolean, Byte> { + override fun lift(value: Byte): Boolean { + return value.toInt() != 0 + } + + override fun read(buf: ByteBuffer): Boolean { + return lift(buf.get()) + } + + override fun lower(value: Boolean): Byte { + return if (value) 1.toByte() else 0.toByte() + } + + override fun allocationSize(value: Boolean) = 1 + + override fun write(value: Boolean, buf: ByteBuffer) { + buf.put(lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt new file mode 100644 index 0000000000..3f5d72a608 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt @@ -0,0 +1,74 @@ +internal typealias Handle = Long +internal class ConcurrentHandleMap<T>( + private val leftMap: MutableMap<Handle, T> = mutableMapOf(), + private val rightMap: MutableMap<T, Handle> = mutableMapOf() +) { + private val lock = java.util.concurrent.locks.ReentrantLock() + private val currentHandle = AtomicLong(0L) + private val stride = 1L + + fun insert(obj: T): Handle = + lock.withLock { + rightMap[obj] ?: + currentHandle.getAndAdd(stride) + .also { handle -> + leftMap[handle] = obj + rightMap[obj] = handle + } + } + + fun get(handle: Handle) = lock.withLock { + leftMap[handle] + } + + fun delete(handle: Handle) { + this.remove(handle) + } + + fun remove(handle: Handle): T? = + lock.withLock { + leftMap.remove(handle)?.let { obj -> + rightMap.remove(obj) + obj + } + } +} + +interface ForeignCallback : com.sun.jna.Callback { + public fun invoke(handle: Handle, method: Int, args: RustBuffer.ByValue, outBuf: RustBufferByReference): Int +} + +// 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. +internal const val IDX_CALLBACK_FREE = 0 + +public abstract class FfiConverterCallbackInterface<CallbackInterface>( + protected val foreignCallback: ForeignCallback +): FfiConverter<CallbackInterface, Handle> { + private val handleMap = ConcurrentHandleMap<CallbackInterface>() + + // Registers the foreign callback with the Rust side. + // This method is generated for each callback interface. + internal abstract fun register(lib: _UniFFILib) + + fun drop(handle: Handle): RustBuffer.ByValue { + return handleMap.remove(handle).let { RustBuffer.ByValue() } + } + + override fun lift(value: Handle): CallbackInterface { + return handleMap.get(value) ?: throw InternalException("No callback in handlemap; this is a Uniffi bug") + } + + override fun read(buf: ByteBuffer) = lift(buf.getLong()) + + override fun lower(value: CallbackInterface) = + handleMap.insert(value).also { + assert(handleMap.get(it) === value) { "Handle map is not returning the object we just placed there. This is a bug in the HandleMap." } + } + + override fun allocationSize(value: CallbackInterface) = 8 + + override fun write(value: CallbackInterface, buf: ByteBuffer) { + buf.putLong(lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt new file mode 100644 index 0000000000..c7c4d2b973 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt @@ -0,0 +1,130 @@ +{%- let cbi = ci.get_callback_interface_definition(name).unwrap() %} +{%- let type_name = cbi|type_name %} +{%- let foreign_callback = format!("ForeignCallback{}", canonical_type_name) %} + +{% if self.include_once_check("CallbackInterfaceRuntime.kt") %}{% include "CallbackInterfaceRuntime.kt" %}{% endif %} +{{- self.add_import("java.util.concurrent.atomic.AtomicLong") }} +{{- self.add_import("java.util.concurrent.locks.ReentrantLock") }} +{{- self.add_import("kotlin.concurrent.withLock") }} + +// Declaration and FfiConverters for {{ type_name }} Callback Interface + +public interface {{ type_name }} { + {% for meth in cbi.methods() -%} + fun {{ meth.name()|fn_name }}({% call kt::arg_list_decl(meth) %}) + {%- match meth.return_type() -%} + {%- when Some with (return_type) %}: {{ return_type|type_name -}} + {%- else -%} + {%- endmatch %} + {% endfor %} +} + +// The ForeignCallback that is passed to Rust. +internal class {{ foreign_callback }} : ForeignCallback { + @Suppress("TooGenericExceptionCaught") + override fun invoke(handle: Handle, method: Int, args: RustBuffer.ByValue, outBuf: RustBufferByReference): Int { + val cb = {{ ffi_converter_name }}.lift(handle) + return when (method) { + IDX_CALLBACK_FREE -> { + {{ ffi_converter_name }}.drop(handle) + // No return value. + // See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` + 0 + } + {% for meth in cbi.methods() -%} + {% let method_name = format!("invoke_{}", meth.name())|fn_name -%} + {{ loop.index }} -> { + // Call the method, write to outBuf and return a status code + // See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` for info + try { + {%- match meth.throws_type() %} + {%- when Some(error_type) %} + try { + val buffer = this.{{ method_name }}(cb, args) + // Success + outBuf.setValue(buffer) + 1 + } catch (e: {{ error_type|type_name }}) { + // Expected error + val buffer = {{ error_type|ffi_converter_name }}.lowerIntoRustBuffer(e) + outBuf.setValue(buffer) + -2 + } + {%- else %} + val buffer = this.{{ method_name }}(cb, args) + // Success + outBuf.setValue(buffer) + 1 + {%- endmatch %} + } catch (e: Throwable) { + // Unexpected error + try { + // Try to serialize the error into a string + outBuf.setValue({{ Type::String.borrow()|ffi_converter_name }}.lower(e.toString())) + } catch (e: Throwable) { + // If that fails, then it's time to give up and just return + } + -1 + } + } + {% endfor %} + else -> { + // An unexpected error happened. + // See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` + try { + // Try to serialize the error into a string + outBuf.setValue({{ Type::String.borrow()|ffi_converter_name }}.lower("Invalid Callback index")) + } catch (e: Throwable) { + // If that fails, then it's time to give up and just return + } + -1 + } + } + } + + {% for meth in cbi.methods() -%} + {% let method_name = format!("invoke_{}", meth.name())|fn_name %} + private fun {{ method_name }}(kotlinCallbackInterface: {{ type_name }}, args: RustBuffer.ByValue): RustBuffer.ByValue = + try { + {#- Unpacking args from the RustBuffer #} + {%- if meth.arguments().len() != 0 -%} + {#- Calling the concrete callback object #} + val buf = args.asByteBuffer() ?: throw InternalException("No ByteBuffer in RustBuffer; this is a Uniffi bug") + kotlinCallbackInterface.{{ meth.name()|fn_name }}( + {% for arg in meth.arguments() -%} + {{ arg|read_fn }}(buf) + {%- if !loop.last %}, {% endif %} + {% endfor -%} + ) + {% else %} + kotlinCallbackInterface.{{ meth.name()|fn_name }}() + {% endif -%} + + {#- Packing up the return value into a RustBuffer #} + {%- match meth.return_type() -%} + {%- when Some with (return_type) -%} + .let { + {{ return_type|ffi_converter_name }}.lowerIntoRustBuffer(it) + } + {%- else -%} + .let { RustBuffer.ByValue() } + {% endmatch -%} + // TODO catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + } finally { + RustBuffer.free(args) + } + + {% endfor %} +} + +// The ffiConverter which transforms the Callbacks in to Handles to pass to Rust. +public object {{ ffi_converter_name }}: FfiConverterCallbackInterface<{{ type_name }}>( + foreignCallback = {{ foreign_callback }}() +) { + override fun register(lib: _UniFFILib) { + rustCall() { status -> + lib.{{ cbi.ffi_init_callback().name() }}(this.foreignCallback, status) + } + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/CustomTypeTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/CustomTypeTemplate.kt new file mode 100644 index 0000000000..c7807f2f33 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/CustomTypeTemplate.kt @@ -0,0 +1,62 @@ +{%- match kotlin_config.custom_types.get(name.as_str()) %} +{%- when None %} +{#- Define the type using typealiases to the builtin #} +/** + * Typealias from the type name used in the UDL file to the builtin type. This + * is needed because the UDL type name is used in function/method signatures. + * It's also what we have an external type that references a custom type. + */ +public typealias {{ name }} = {{ builtin|type_name }} +public typealias {{ ffi_converter_name }} = {{ builtin|ffi_converter_name }} + +{%- when Some with (config) %} + +{%- let ffi_type_name=builtin.ffi_type().borrow()|ffi_type_name %} + +{# When the config specifies a different type name, create a typealias for it #} +{%- match config.type_name %} +{%- when Some(concrete_type_name) %} +/** + * Typealias from the type name used in the UDL file to the custom type. This + * is needed because the UDL type name is used in function/method signatures. + * It's also what we have an external type that references a custom type. + */ +public typealias {{ name }} = {{ concrete_type_name }} +{%- else %} +{%- endmatch %} + +{%- match config.imports %} +{%- when Some(imports) %} +{%- for import_name in imports %} +{{ self.add_import(import_name) }} +{%- endfor %} +{%- else %} +{%- endmatch %} + +public object {{ ffi_converter_name }}: FfiConverter<{{ name }}, {{ ffi_type_name }}> { + override fun lift(value: {{ ffi_type_name }}): {{ name }} { + val builtinValue = {{ builtin|lift_fn }}(value) + return {{ config.into_custom.render("builtinValue") }} + } + + override fun lower(value: {{ name }}): {{ ffi_type_name }} { + val builtinValue = {{ config.from_custom.render("value") }} + return {{ builtin|lower_fn }}(builtinValue) + } + + override fun read(buf: ByteBuffer): {{ name }} { + val builtinValue = {{ builtin|read_fn }}(buf) + return {{ config.into_custom.render("builtinValue") }} + } + + override fun allocationSize(value: {{ name }}): Int { + val builtinValue = {{ config.from_custom.render("value") }} + return {{ builtin|allocation_size_fn }}(builtinValue) + } + + override fun write(value: {{ name }}, buf: ByteBuffer) { + val builtinValue = {{ config.from_custom.render("value") }} + {{ builtin|write_fn }}(builtinValue, buf) + } +} +{%- endmatch %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/DurationHelper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/DurationHelper.kt new file mode 100644 index 0000000000..4237c6f9a8 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/DurationHelper.kt @@ -0,0 +1,36 @@ +public object FfiConverterDuration: FfiConverterRustBuffer<java.time.Duration> { + override fun read(buf: ByteBuffer): java.time.Duration { + // Type mismatch (should be u64) but we check for overflow/underflow below + val seconds = buf.getLong() + // Type mismatch (should be u32) but we check for overflow/underflow below + val nanoseconds = buf.getInt().toLong() + if (seconds < 0) { + throw java.time.DateTimeException("Duration exceeds minimum or maximum value supported by uniffi") + } + if (nanoseconds < 0) { + throw java.time.DateTimeException("Duration nanoseconds exceed minimum or maximum supported by uniffi") + } + return java.time.Duration.ofSeconds(seconds, nanoseconds) + } + + // 8 bytes for seconds, 4 bytes for nanoseconds + override fun allocationSize(value: java.time.Duration) = 12 + + override fun write(value: java.time.Duration, buf: ByteBuffer) { + if (value.seconds < 0) { + // Rust does not support negative Durations + throw IllegalArgumentException("Invalid duration, must be non-negative") + } + + if (value.nano < 0) { + // Java docs provide guarantee that nano will always be positive, so this should be impossible + // See: https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html + throw IllegalArgumentException("Invalid duration, nano value must be non-negative") + } + + // Type mismatch (should be u64) but since Rust doesn't support negative durations we should be OK + buf.putLong(value.seconds) + // Type mismatch (should be u32) but since values will always be between 0 and 999,999,999 it should be OK + buf.putInt(value.nano) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt new file mode 100644 index 0000000000..be198ac7b9 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt @@ -0,0 +1,107 @@ +{# +// Kotlin's `enum class` construct doesn't support variants with associated data, +// but is a little nicer for consumers than its `sealed class` enum pattern. +// So, we switch here, using `enum class` for enums with no associated data +// and `sealed class` for the general case. +#} +{%- let e = ci.get_enum_definition(name).unwrap() %} + +{%- if e.is_flat() %} + +enum class {{ type_name }} { + {% for variant in e.variants() -%} + {{ variant.name()|enum_variant }}{% if loop.last %};{% else %},{% endif %} + {%- endfor %} +} + +public object {{ e|ffi_converter_name }}: FfiConverterRustBuffer<{{ type_name }}> { + override fun read(buf: ByteBuffer) = try { + {{ type_name }}.values()[buf.getInt() - 1] + } catch (e: IndexOutOfBoundsException) { + throw RuntimeException("invalid enum value, something is very wrong!!", e) + } + + override fun allocationSize(value: {{ type_name }}) = 4 + + override fun write(value: {{ type_name }}, buf: ByteBuffer) { + buf.putInt(value.ordinal + 1) + } +} + +{% else %} + +sealed class {{ type_name }}{% if contains_object_references %}: Disposable {% endif %} { + {% for variant in e.variants() -%} + {% if !variant.has_fields() -%} + object {{ variant.name()|class_name }} : {{ type_name }}() + {% else -%} + data class {{ variant.name()|class_name }}( + {% for field in variant.fields() -%} + val {{ field.name()|var_name }}: {{ field|type_name}}{% if loop.last %}{% else %}, {% endif %} + {% endfor -%} + ) : {{ type_name }}() + {%- endif %} + {% endfor %} + + {% if contains_object_references %} + @Suppress("UNNECESSARY_SAFE_CALL") // codegen is much simpler if we unconditionally emit safe calls here + override fun destroy() { + when(this) { + {%- for variant in e.variants() %} + is {{ type_name }}.{{ variant.name()|class_name }} -> { + {%- if variant.has_fields() %} + {% call kt::destroy_fields(variant) %} + {% else -%} + // Nothing to destroy + {%- endif %} + } + {%- endfor %} + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } + {% endif %} +} + +public object {{ e|ffi_converter_name }} : FfiConverterRustBuffer<{{ type_name }}>{ + override fun read(buf: ByteBuffer): {{ type_name }} { + return when(buf.getInt()) { + {%- for variant in e.variants() %} + {{ loop.index }} -> {{ type_name }}.{{ variant.name()|class_name }}{% if variant.has_fields() %}( + {% for field in variant.fields() -%} + {{ field|read_fn }}(buf), + {% endfor -%} + ){%- endif -%} + {%- endfor %} + else -> throw RuntimeException("invalid enum value, something is very wrong!!") + } + } + + override fun allocationSize(value: {{ type_name }}) = when(value) { + {%- for variant in e.variants() %} + is {{ type_name }}.{{ variant.name()|class_name }} -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4 + {%- for field in variant.fields() %} + + {{ field|allocation_size_fn }}(value.{{ field.name()|var_name }}) + {%- endfor %} + ) + } + {%- endfor %} + } + + override fun write(value: {{ type_name }}, buf: ByteBuffer) { + when(value) { + {%- for variant in e.variants() %} + is {{ type_name }}.{{ variant.name()|class_name }} -> { + buf.putInt({{ loop.index }}) + {%- for field in variant.fields() %} + {{ field|write_fn }}(value.{{ field.name()|var_name }}, buf) + {%- endfor %} + Unit + } + {%- endfor %} + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } +} + +{% endif %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt new file mode 100644 index 0000000000..9ddef9bb73 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt @@ -0,0 +1,109 @@ +{%- let e = ci.get_error_definition(name).unwrap() %} + +{% if e.is_flat() %} +sealed class {{ type_name }}(message: String): Exception(message){% if contains_object_references %}, Disposable {% endif %} { + // Each variant is a nested class + // Flat enums carries a string error message, so no special implementation is necessary. + {% for variant in e.variants() -%} + class {{ variant.name()|exception_name }}(message: String) : {{ type_name }}(message) + {% endfor %} + + companion object ErrorHandler : CallStatusErrorHandler<{{ type_name }}> { + override fun lift(error_buf: RustBuffer.ByValue): {{ type_name }} = {{ e|lift_fn }}(error_buf) + } +} +{%- else %} +sealed class {{ type_name }}: Exception(){% if contains_object_references %}, Disposable {% endif %} { + // Each variant is a nested class + {% for variant in e.variants() -%} + {%- let variant_name = variant.name()|exception_name %} + class {{ variant_name }}( + {% for field in variant.fields() -%} + val {{ field.name()|var_name }}: {{ field|type_name}}{% if loop.last %}{% else %}, {% endif %} + {% endfor -%} + ) : {{ type_name }}() { + override val message + get() = "{%- for field in variant.fields() %}{{ field.name()|var_name|unquote }}=${ {{field.name()|var_name }} }{% if !loop.last %}, {% endif %}{% endfor %}" + } + {% endfor %} + + companion object ErrorHandler : CallStatusErrorHandler<{{ type_name }}> { + override fun lift(error_buf: RustBuffer.ByValue): {{ type_name }} = {{ e|lift_fn }}(error_buf) + } + + {% if contains_object_references %} + @Suppress("UNNECESSARY_SAFE_CALL") // codegen is much simpler if we unconditionally emit safe calls here + override fun destroy() { + when(this) { + {%- for variant in e.variants() %} + is {{ type_name }}.{{ variant.name()|exception_name }} -> { + {%- if variant.has_fields() %} + {% call kt::destroy_fields(variant) %} + {% else -%} + // Nothing to destroy + {%- endif %} + } + {%- endfor %} + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } + {% endif %} +} +{%- endif %} + +public object {{ e|ffi_converter_name }} : FfiConverterRustBuffer<{{ type_name }}> { + override fun read(buf: ByteBuffer): {{ type_name }} { + {% if e.is_flat() %} + return when(buf.getInt()) { + {%- for variant in e.variants() %} + {{ loop.index }} -> {{ type_name }}.{{ variant.name()|exception_name }}({{ TypeIdentifier::String.borrow()|read_fn }}(buf)) + {%- endfor %} + else -> throw RuntimeException("invalid error enum value, something is very wrong!!") + } + {% else %} + + return when(buf.getInt()) { + {%- for variant in e.variants() %} + {{ loop.index }} -> {{ type_name }}.{{ variant.name()|exception_name }}({% if variant.has_fields() %} + {% for field in variant.fields() -%} + {{ field|read_fn }}(buf), + {% endfor -%} + {%- endif -%}) + {%- endfor %} + else -> throw RuntimeException("invalid error enum value, something is very wrong!!") + } + {%- endif %} + } + + override fun allocationSize(value: {{ type_name }}): Int { + {%- if e.is_flat() %} + return 4 + {%- else %} + return when(value) { + {%- for variant in e.variants() %} + is {{ type_name }}.{{ variant.name()|exception_name }} -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + {%- for field in variant.fields() %} + + {{ field|allocation_size_fn }}(value.{{ field.name()|var_name }}) + {%- endfor %} + ) + {%- endfor %} + } + {%- endif %} + } + + override fun write(value: {{ type_name }}, buf: ByteBuffer) { + when(value) { + {%- for variant in e.variants() %} + is {{ type_name }}.{{ variant.name()|exception_name }} -> { + buf.putInt({{ loop.index }}) + {%- for field in variant.fields() %} + {{ field|write_fn }}(value.{{ field.name()|var_name }}, buf) + {%- endfor %} + Unit + } + {%- endfor %} + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } + +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ExternalTypeTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ExternalTypeTemplate.kt new file mode 100644 index 0000000000..21d96dcdf1 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ExternalTypeTemplate.kt @@ -0,0 +1,9 @@ +{%- let package_name=self.external_type_package_name(crate_name) %} +{%- let fully_qualified_type_name = "{}.{}"|format(package_name, name) %} +{%- let fully_qualified_ffi_converter_name = "{}.FfiConverterType{}"|format(package_name, name) %} +{%- let fully_qualified_rustbuffer_name = "{}.RustBuffer"|format(package_name) %} +{%- let local_rustbuffer_name = "RustBuffer{}"|format(name) %} + +{{- self.add_import(fully_qualified_type_name) }} +{{- self.add_import(fully_qualified_ffi_converter_name) }} +{{ self.add_import_as(fully_qualified_rustbuffer_name, local_rustbuffer_name) }} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/FfiConverterTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/FfiConverterTemplate.kt new file mode 100644 index 0000000000..3b2c9d225a --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/FfiConverterTemplate.kt @@ -0,0 +1,71 @@ +// The FfiConverter interface handles converter types to and from the FFI +// +// All implementing objects should be public to support external types. When a +// type is external we need to import it's FfiConverter. +public interface FfiConverter<KotlinType, FfiType> { + // Convert an FFI type to a Kotlin type + fun lift(value: FfiType): KotlinType + + // Convert an Kotlin type to an FFI type + fun lower(value: KotlinType): FfiType + + // Read a Kotlin type from a `ByteBuffer` + fun read(buf: ByteBuffer): KotlinType + + // Calculate bytes to allocate when creating a `RustBuffer` + // + // This must return at least as many bytes as the write() function will + // write. It can return more bytes than needed, for example when writing + // Strings we can't know the exact bytes needed until we the UTF-8 + // encoding, so we pessimistically allocate the largest size possible (3 + // bytes per codepoint). Allocating extra bytes is not really a big deal + // because the `RustBuffer` is short-lived. + fun allocationSize(value: KotlinType): Int + + // Write a Kotlin type to a `ByteBuffer` + fun write(value: KotlinType, buf: ByteBuffer) + + // Lower a value into a `RustBuffer` + // + // This method lowers a value into a `RustBuffer` rather than the normal + // FfiType. It's used by the callback interface code. Callback interface + // returns are always serialized into a `RustBuffer` regardless of their + // normal FFI type. + fun lowerIntoRustBuffer(value: KotlinType): RustBuffer.ByValue { + val rbuf = RustBuffer.alloc(allocationSize(value)) + try { + val bbuf = rbuf.data!!.getByteBuffer(0, rbuf.capacity.toLong()).also { + it.order(ByteOrder.BIG_ENDIAN) + } + write(value, bbuf) + rbuf.writeField("len", bbuf.position()) + return rbuf + } catch (e: Throwable) { + RustBuffer.free(rbuf) + throw e + } + } + + // Lift a value from a `RustBuffer`. + // + // This here mostly because of the symmetry with `lowerIntoRustBuffer()`. + // It's currently only used by the `FfiConverterRustBuffer` class below. + fun liftFromRustBuffer(rbuf: RustBuffer.ByValue): KotlinType { + val byteBuf = rbuf.asByteBuffer()!! + try { + val item = read(byteBuf) + if (byteBuf.hasRemaining()) { + throw RuntimeException("junk remaining in buffer after lifting, something is very wrong!!") + } + return item + } finally { + RustBuffer.free(rbuf) + } + } +} + +// FfiConverter that uses `RustBuffer` as the FfiType +public interface FfiConverterRustBuffer<KotlinType>: FfiConverter<KotlinType, RustBuffer.ByValue> { + override fun lift(value: RustBuffer.ByValue) = liftFromRustBuffer(value) + override fun lower(value: KotlinType) = lowerIntoRustBuffer(value) +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Float32Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Float32Helper.kt new file mode 100644 index 0000000000..eafec5d122 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Float32Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterFloat: FfiConverter<Float, Float> { + override fun lift(value: Float): Float { + return value + } + + override fun read(buf: ByteBuffer): Float { + return buf.getFloat() + } + + override fun lower(value: Float): Float { + return value + } + + override fun allocationSize(value: Float) = 4 + + override fun write(value: Float, buf: ByteBuffer) { + buf.putFloat(value) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Float64Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Float64Helper.kt new file mode 100644 index 0000000000..9fc2892c95 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Float64Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterDouble: FfiConverter<Double, Double> { + override fun lift(value: Double): Double { + return value + } + + override fun read(buf: ByteBuffer): Double { + return buf.getDouble() + } + + override fun lower(value: Double): Double { + return value + } + + override fun allocationSize(value: Double) = 8 + + override fun write(value: Double, buf: ByteBuffer) { + buf.putDouble(value) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt new file mode 100644 index 0000000000..d7e61ef4c7 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt @@ -0,0 +1,66 @@ +// 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. +// Error runtime. +@Structure.FieldOrder("code", "error_buf") +internal open class RustCallStatus : Structure() { + @JvmField var code: Int = 0 + @JvmField var error_buf: RustBuffer.ByValue = RustBuffer.ByValue() + + fun isSuccess(): Boolean { + return code == 0 + } + + fun isError(): Boolean { + return code == 1 + } + + fun isPanic(): Boolean { + return code == 2 + } +} + +class InternalException(message: String) : Exception(message) + +// Each top-level error class has a companion object that can lift the error from the call status's rust buffer +interface CallStatusErrorHandler<E> { + fun lift(error_buf: RustBuffer.ByValue): E; +} + +// Helpers for calling Rust +// In practice we usually need to be synchronized to call this safely, so it doesn't +// synchronize itself + +// Call a rust function that returns a Result<>. Pass in the Error class companion that corresponds to the Err +private inline fun <U, E: Exception> rustCallWithError(errorHandler: CallStatusErrorHandler<E>, callback: (RustCallStatus) -> U): U { + var status = RustCallStatus(); + val return_value = callback(status) + if (status.isSuccess()) { + return return_value + } else if (status.isError()) { + throw errorHandler.lift(status.error_buf) + } else if (status.isPanic()) { + // 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 (status.error_buf.len > 0) { + throw InternalException({{ TypeIdentifier::String.borrow()|lift_fn }}(status.error_buf)) + } else { + throw InternalException("Rust panic") + } + } else { + throw InternalException("Unknown rust call status: $status.code") + } +} + +// CallStatusErrorHandler implementation for times when we don't expect a CALL_ERROR +object NullCallStatusErrorHandler: CallStatusErrorHandler<InternalException> { + override fun lift(error_buf: RustBuffer.ByValue): InternalException { + RustBuffer.free(error_buf) + return InternalException("Unexpected CALL_ERROR") + } +} + +// Call a rust function that returns a plain value +private inline fun <U> rustCall(callback: (RustCallStatus) -> U): U { + return rustCallWithError(NullCallStatusErrorHandler, callback); +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int16Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int16Helper.kt new file mode 100644 index 0000000000..75564276be --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int16Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterShort: FfiConverter<Short, Short> { + override fun lift(value: Short): Short { + return value + } + + override fun read(buf: ByteBuffer): Short { + return buf.getShort() + } + + override fun lower(value: Short): Short { + return value + } + + override fun allocationSize(value: Short) = 2 + + override fun write(value: Short, buf: ByteBuffer) { + buf.putShort(value) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int32Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int32Helper.kt new file mode 100644 index 0000000000..b7a8131c8b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int32Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterInt: FfiConverter<Int, Int> { + override fun lift(value: Int): Int { + return value + } + + override fun read(buf: ByteBuffer): Int { + return buf.getInt() + } + + override fun lower(value: Int): Int { + return value + } + + override fun allocationSize(value: Int) = 4 + + override fun write(value: Int, buf: ByteBuffer) { + buf.putInt(value) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int64Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int64Helper.kt new file mode 100644 index 0000000000..601cfc7c2c --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int64Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterLong: FfiConverter<Long, Long> { + override fun lift(value: Long): Long { + return value + } + + override fun read(buf: ByteBuffer): Long { + return buf.getLong() + } + + override fun lower(value: Long): Long { + return value + } + + override fun allocationSize(value: Long) = 8 + + override fun write(value: Long, buf: ByteBuffer) { + buf.putLong(value) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int8Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int8Helper.kt new file mode 100644 index 0000000000..9237768dbf --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int8Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterByte: FfiConverter<Byte, Byte> { + override fun lift(value: Byte): Byte { + return value + } + + override fun read(buf: ByteBuffer): Byte { + return buf.get() + } + + override fun lower(value: Byte): Byte { + return value + } + + override fun allocationSize(value: Byte) = 1 + + override fun write(value: Byte, buf: ByteBuffer) { + buf.put(value) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt new file mode 100644 index 0000000000..c8cbecb68b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt @@ -0,0 +1,35 @@ +{%- let key_type_name = key_type|type_name %} +{%- let value_type_name = value_type|type_name %} +public object {{ ffi_converter_name }}: FfiConverterRustBuffer<Map<{{ key_type_name }}, {{ value_type_name }}>> { + override fun read(buf: ByteBuffer): Map<{{ key_type_name }}, {{ value_type_name }}> { + // TODO: Once Kotlin's `buildMap` API is stabilized we should use it here. + val items : MutableMap<{{ key_type_name }}, {{ value_type_name }}> = mutableMapOf() + val len = buf.getInt() + repeat(len) { + val k = {{ key_type|read_fn }}(buf) + val v = {{ value_type|read_fn }}(buf) + items[k] = v + } + return items + } + + override fun allocationSize(value: Map<{{ key_type_name }}, {{ value_type_name }}>): Int { + val spaceForMapSize = 4 + val spaceForChildren = value.map { (k, v) -> + {{ key_type|allocation_size_fn }}(k) + + {{ value_type|allocation_size_fn }}(v) + }.sum() + return spaceForMapSize + spaceForChildren + } + + override fun write(value: Map<{{ key_type_name }}, {{ value_type_name }}>, buf: ByteBuffer) { + buf.putInt(value.size) + // The parens on `(k, v)` here ensure we're calling the right method, + // which is important for compatibility with older android devices. + // Ref https://blog.danlew.net/2017/03/16/kotlin-puzzler-whose-line-is-it-anyways/ + value.forEach { (k, v) -> + {{ key_type|write_fn }}(k, buf) + {{ value_type|write_fn }}(v, buf) + } + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt new file mode 100644 index 0000000000..df96ee5cbb --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt @@ -0,0 +1,40 @@ +@Synchronized +private fun findLibraryName(componentName: String): String { + val libOverride = System.getProperty("uniffi.component.$componentName.libraryOverride") + if (libOverride != null) { + return libOverride + } + return "{{ config.cdylib_name() }}" +} + +private inline fun <reified Lib : Library> loadIndirect( + componentName: String +): Lib { + return Native.load<Lib>(findLibraryName(componentName), Lib::class.java) +} + +// A JNA Library to expose the extern-C FFI definitions. +// This is an implementation detail which will be called internally by the public API. + +internal interface _UniFFILib : Library { + companion object { + internal val INSTANCE: _UniFFILib by lazy { + loadIndirect<_UniFFILib>(componentName = "{{ ci.namespace() }}") + {% let initialization_fns = self.initialization_fns() %} + {%- if !initialization_fns.is_empty() -%} + .also { lib: _UniFFILib -> + {% for fn in initialization_fns -%} + {{ fn }}(lib) + {% endfor -%} + } + {% endif %} + } + } + + {% for func in ci.iter_ffi_function_definitions() -%} + fun {{ func.name() }}( + {%- call kt::arg_list_ffi_decl(func) %} + ){%- match func.return_type() -%}{%- when Some with (type_) %}: {{ type_.borrow()|ffi_type_name }}{% when None %}: Unit{% endmatch %} + + {% endfor %} +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ObjectRuntime.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ObjectRuntime.kt new file mode 100644 index 0000000000..b9352c690f --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ObjectRuntime.kt @@ -0,0 +1,161 @@ +// Interface implemented by anything that can contain an object reference. +// +// Such types expose a `destroy()` method that must be called to cleanly +// dispose of the contained objects. Failure to call this method may result +// in memory leaks. +// +// The easiest way to ensure this method is called is to use the `.use` +// helper method to execute a block and destroy the object at the end. +interface Disposable { + fun destroy() + companion object { + fun destroy(vararg args: Any?) { + args.filterIsInstance<Disposable>() + .forEach(Disposable::destroy) + } + } +} + +inline fun <T : Disposable?, R> T.use(block: (T) -> R) = + try { + block(this) + } finally { + try { + // N.B. our implementation is on the nullable type `Disposable?`. + this?.destroy() + } catch (e: Throwable) { + // swallow + } + } + +// The base class for all UniFFI Object types. +// +// This class provides core operations for working with the Rust `Arc<T>` pointer to +// the live Rust struct on the other side of the FFI. +// +// There's some subtlety here, because we have to be careful not to operate on a Rust +// struct after it has been dropped, and because we must expose a public API for freeing +// the Kotlin wrapper object in lieu of reliable finalizers. The core requirements are: +// +// * Each `FFIObject` instance holds an opaque pointer to the underlying Rust struct. +// Method calls need to read this pointer from the object's state and pass it in to +// the Rust FFI. +// +// * When an `FFIObject` is no longer needed, its pointer should be passed to a +// special destructor function provided by the Rust FFI, which will drop the +// underlying Rust struct. +// +// * Given an `FFIObject` instance, calling code is expected to call the special +// `destroy` method in order to free it after use, either by calling it explicitly +// or by using a higher-level helper like the `use` method. Failing to do so will +// leak the underlying Rust struct. +// +// * We can't assume that calling code will do the right thing, and must be prepared +// to handle Kotlin method calls executing concurrently with or even after a call to +// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`. +// +// * We must never allow Rust code to operate on the underlying Rust struct after +// the destructor has been called, and must never call the destructor more than once. +// Doing so may trigger memory unsafety. +// +// If we try to implement this with mutual exclusion on access to the pointer, there is the +// possibility of a race between a method call and a concurrent call to `destroy`: +// +// * Thread A starts a method call, reads the value of the pointer, but is interrupted +// before it can pass the pointer over the FFI to Rust. +// * Thread B calls `destroy` and frees the underlying Rust struct. +// * Thread A resumes, passing the already-read pointer value to Rust and triggering +// a use-after-free. +// +// One possible solution would be to use a `ReadWriteLock`, with each method call taking +// a read lock (and thus allowed to run concurrently) and the special `destroy` method +// taking a write lock (and thus blocking on live method calls). However, we aim not to +// generate methods with any hidden blocking semantics, and a `destroy` method that might +// block if called incorrectly seems to meet that bar. +// +// So, we achieve our goals by giving each `FFIObject` an associated `AtomicLong` counter to track +// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy` +// has been called. These are updated according to the following rules: +// +// * The initial value of the counter is 1, indicating a live object with no in-flight calls. +// The initial value for the flag is false. +// +// * At the start of each method call, we atomically check the counter. +// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted. +// If it is nonzero them we atomically increment it by 1 and proceed with the method call. +// +// * At the end of each method call, we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// * When `destroy` is called, we atomically flip the flag from false to true. +// If the flag was already true we silently fail. +// Otherwise we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc<T>` works, +// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`. +// +// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been +// called *and* all in-flight method calls have completed, avoiding violating any of the expectations +// of the underlying Rust code. +// +// In the future we may be able to replace some of this with automatic finalization logic, such as using +// the new "Cleaner" functionaility in Java 9. The above scheme has been designed to work even if `destroy` is +// invoked by garbage-collection machinery rather than by calling code (which by the way, it's apparently also +// possible for the JVM to finalize an object while there is an in-flight call to one of its methods [1], +// so there would still be some complexity here). +// +// Sigh...all of this for want of a robust finalization mechanism. +// +// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219 +// +abstract class FFIObject( + protected val pointer: Pointer +): Disposable, AutoCloseable { + + private val wasDestroyed = AtomicBoolean(false) + private val callCounter = AtomicLong(1) + + open protected fun freeRustArcPtr() { + // To be overridden in subclasses. + } + + override fun destroy() { + // Only allow a single call to this method. + // TODO: maybe we should log a warning if called more than once? + if (this.wasDestroyed.compareAndSet(false, true)) { + // This decrement always matches the initial count of 1 given at creation time. + if (this.callCounter.decrementAndGet() == 0L) { + this.freeRustArcPtr() + } + } + } + + @Synchronized + override fun close() { + this.destroy() + } + + internal inline fun <R> callWithPointer(block: (ptr: Pointer) -> R): R { + // Check and increment the call counter, to keep the object alive. + // This needs a compare-and-set retry loop in case of concurrent updates. + do { + val c = this.callCounter.get() + if (c == 0L) { + throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed") + } + if (c == Long.MAX_VALUE) { + throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow") + } + } while (! this.callCounter.compareAndSet(c, c + 1L)) + // Now we can safely do the method call without the pointer being freed concurrently. + try { + return block(this.pointer) + } finally { + // This decrement always matches the increment we performed above. + if (this.callCounter.decrementAndGet() == 0L) { + this.freeRustArcPtr() + } + } + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt new file mode 100644 index 0000000000..5d654d9fae --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt @@ -0,0 +1,100 @@ +{%- let obj = ci.get_object_definition(name).unwrap() %} +{%- if self.include_once_check("ObjectRuntime.kt") %}{% include "ObjectRuntime.kt" %}{% endif %} +{{- self.add_import("java.util.concurrent.atomic.AtomicLong") }} +{{- self.add_import("java.util.concurrent.atomic.AtomicBoolean") }} + +public interface {{ type_name }}Interface { + {% for meth in obj.methods() -%} + {%- match meth.throws_type() -%} + {%- when Some with (throwable) %} + @Throws({{ throwable|type_name }}::class) + {%- else -%} + {%- endmatch %} + fun {{ meth.name()|fn_name }}({% call kt::arg_list_decl(meth) %}) + {%- match meth.return_type() -%} + {%- when Some with (return_type) %}: {{ return_type|type_name -}} + {%- else -%} + {%- endmatch %} + {% endfor %} +} + +class {{ type_name }}( + pointer: Pointer +) : FFIObject(pointer), {{ type_name }}Interface { + + {%- match obj.primary_constructor() %} + {%- when Some with (cons) %} + constructor({% call kt::arg_list_decl(cons) -%}) : + this({% call kt::to_ffi_call(cons) %}) + {%- when None %} + {%- endmatch %} + + /** + * Disconnect the object from the underlying Rust object. + * + * It can be called more than once, but once called, interacting with the object + * causes an `IllegalStateException`. + * + * Clients **must** call this method once done with the object, or cause a memory leak. + */ + override protected fun freeRustArcPtr() { + rustCall() { status -> + _UniFFILib.INSTANCE.{{ obj.ffi_object_free().name() }}(this.pointer, status) + } + } + + {% for meth in obj.methods() -%} + {%- match meth.throws_type() -%} + {%- when Some with (throwable) %} + @Throws({{ throwable|type_name }}::class) + {%- else -%} + {%- endmatch %} + {%- match meth.return_type() -%} + + {%- when Some with (return_type) -%} + override fun {{ meth.name()|fn_name }}({% call kt::arg_list_protocol(meth) %}): {{ return_type|type_name }} = + callWithPointer { + {%- call kt::to_ffi_call_with_prefix("it", meth) %} + }.let { + {{ return_type|lift_fn }}(it) + } + + {%- when None -%} + override fun {{ meth.name()|fn_name }}({% call kt::arg_list_protocol(meth) %}) = + callWithPointer { + {%- call kt::to_ffi_call_with_prefix("it", meth) %} + } + {% endmatch %} + {% endfor %} + + {% if !obj.alternate_constructors().is_empty() -%} + companion object { + {% for cons in obj.alternate_constructors() -%} + fun {{ cons.name()|fn_name }}({% call kt::arg_list_decl(cons) %}): {{ type_name }} = + {{ type_name }}({% call kt::to_ffi_call(cons) %}) + {% endfor %} + } + {% endif %} +} + +public object {{ obj|ffi_converter_name }}: FfiConverter<{{ type_name }}, Pointer> { + override fun lower(value: {{ type_name }}): Pointer = value.callWithPointer { it } + + override fun lift(value: Pointer): {{ type_name }} { + return {{ type_name }}(value) + } + + override fun read(buf: ByteBuffer): {{ type_name }} { + // The Rust code always writes pointers as 8 bytes, and will + // fail to compile if they don't fit. + return lift(Pointer(buf.getLong())) + } + + override fun allocationSize(value: {{ type_name }}) = 8 + + override fun write(value: {{ type_name }}, buf: ByteBuffer) { + // The Rust code always expects pointers written as 8 bytes, + // and will fail to compile if they don't fit. + buf.putLong(Pointer.nativeValue(lower(value))) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/OptionalTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/OptionalTemplate.kt new file mode 100644 index 0000000000..6830b51863 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/OptionalTemplate.kt @@ -0,0 +1,27 @@ +{%- let inner_type_name = inner_type|type_name %} + +public object {{ ffi_converter_name }}: FfiConverterRustBuffer<{{ inner_type_name }}?> { + override fun read(buf: ByteBuffer): {{ inner_type_name }}? { + if (buf.get().toInt() == 0) { + return null + } + return {{ inner_type|read_fn }}(buf) + } + + override fun allocationSize(value: {{ inner_type_name }}?): Int { + if (value == null) { + return 1 + } else { + return 1 + {{ inner_type|allocation_size_fn }}(value) + } + } + + override fun write(value: {{ inner_type_name }}?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + {{ inner_type|write_fn }}(value, buf) + } + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt new file mode 100644 index 0000000000..a17f4d42eb --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt @@ -0,0 +1,41 @@ +{%- let rec = ci.get_record_definition(name).unwrap() %} + +data class {{ type_name }} ( + {%- for field in rec.fields() %} + var {{ field.name()|var_name }}: {{ field|type_name -}} + {%- match field.default_value() %} + {%- when Some with(literal) %} = {{ literal|render_literal(field) }} + {%- else %} + {%- endmatch -%} + {% if !loop.last %}, {% endif %} + {%- endfor %} +) {% if contains_object_references %}: Disposable {% endif %}{ + {% if contains_object_references %} + @Suppress("UNNECESSARY_SAFE_CALL") // codegen is much simpler if we unconditionally emit safe calls here + override fun destroy() { + {% call kt::destroy_fields(rec) %} + } + {% endif %} +} + +public object {{ rec|ffi_converter_name }}: FfiConverterRustBuffer<{{ type_name }}> { + override fun read(buf: ByteBuffer): {{ type_name }} { + return {{ type_name }}( + {%- for field in rec.fields() %} + {{ field|read_fn }}(buf), + {%- endfor %} + ) + } + + override fun allocationSize(value: {{ type_name }}) = ( + {%- for field in rec.fields() %} + {{ field|allocation_size_fn }}(value.{{ field.name()|var_name }}){% if !loop.last %} +{% endif%} + {%- endfor %} + ) + + override fun write(value: {{ type_name }}, buf: ByteBuffer) { + {%- 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/kotlin/templates/RustBufferTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferTemplate.kt new file mode 100644 index 0000000000..6cc218e940 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferTemplate.kt @@ -0,0 +1,66 @@ +// This is a helper for safely working with byte buffers returned from the Rust code. +// A rust-owned buffer is represented by its capacity, its current length, and a +// pointer to the underlying data. + +@Structure.FieldOrder("capacity", "len", "data") +open class RustBuffer : Structure() { + @JvmField var capacity: Int = 0 + @JvmField var len: Int = 0 + @JvmField var data: Pointer? = null + + class ByValue : RustBuffer(), Structure.ByValue + class ByReference : RustBuffer(), Structure.ByReference + + companion object { + internal fun alloc(size: Int = 0) = rustCall() { status -> + _UniFFILib.INSTANCE.{{ ci.ffi_rustbuffer_alloc().name() }}(size, status).also { + if(it.data == null) { + throw RuntimeException("RustBuffer.alloc() returned null data pointer (size=${size})") + } + } + } + + internal fun free(buf: RustBuffer.ByValue) = rustCall() { status -> + _UniFFILib.INSTANCE.{{ ci.ffi_rustbuffer_free().name() }}(buf, status) + } + } + + @Suppress("TooGenericExceptionThrown") + fun asByteBuffer() = + this.data?.getByteBuffer(0, this.len.toLong())?.also { + it.order(ByteOrder.BIG_ENDIAN) + } +} + +/** + * The equivalent of the `*mut RustBuffer` type. + * Required for callbacks taking in an out pointer. + * + * Size is the sum of all values in the struct. + */ +class RustBufferByReference : ByReference(16) { + /** + * Set the pointed-to `RustBuffer` to the given value. + */ + fun setValue(value: RustBuffer.ByValue) { + // NOTE: The offsets are as they are in the C-like struct. + val pointer = getPointer() + pointer.setInt(0, value.capacity) + pointer.setInt(4, value.len) + pointer.setPointer(8, value.data) + } +} + +// This is a helper for safely passing byte references into the rust code. +// It's not actually used at the moment, because there aren't many things that you +// can take a direct pointer to in the JVM, and if we're going to copy something +// then we might as well copy it into a `RustBuffer`. But it's here for API +// completeness. + +@Structure.FieldOrder("len", "data") +open class ForeignBytes : Structure() { + @JvmField var len: Int = 0 + @JvmField var data: Pointer? = null + + class ByValue : ForeignBytes(), Structure.ByValue +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/SequenceTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/SequenceTemplate.kt new file mode 100644 index 0000000000..b5c699ba3b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/SequenceTemplate.kt @@ -0,0 +1,23 @@ +{%- let inner_type_name = inner_type|type_name %} + +public object {{ ffi_converter_name }}: FfiConverterRustBuffer<List<{{ inner_type_name }}>> { + override fun read(buf: ByteBuffer): List<{{ inner_type_name }}> { + val len = buf.getInt() + return List<{{ inner_type_name }}>(len) { + {{ inner_type|read_fn }}(buf) + } + } + + override fun allocationSize(value: List<{{ inner_type_name }}>): Int { + val sizeForLength = 4 + val sizeForItems = value.map { {{ inner_type|allocation_size_fn }}(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write(value: List<{{ inner_type_name }}>, buf: ByteBuffer) { + buf.putInt(value.size) + value.forEach { + {{ inner_type|write_fn }}(it, buf) + } + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/StringHelper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/StringHelper.kt new file mode 100644 index 0000000000..d3443215c1 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/StringHelper.kt @@ -0,0 +1,45 @@ +public object FfiConverterString: FfiConverter<String, RustBuffer.ByValue> { + // Note: we don't inherit from FfiConverterRustBuffer, because we use a + // special encoding when lowering/lifting. We can use `RustBuffer.len` to + // store our length and avoid writing it out to the buffer. + override fun lift(value: RustBuffer.ByValue): String { + try { + val byteArr = ByteArray(value.len) + value.asByteBuffer()!!.get(byteArr) + return byteArr.toString(Charsets.UTF_8) + } finally { + RustBuffer.free(value) + } + } + + override fun read(buf: ByteBuffer): String { + val len = buf.getInt() + val byteArr = ByteArray(len) + buf.get(byteArr) + return byteArr.toString(Charsets.UTF_8) + } + + override fun lower(value: String): RustBuffer.ByValue { + val byteArr = value.toByteArray(Charsets.UTF_8) + // Ideally we'd pass these bytes to `ffi_bytebuffer_from_bytes`, but doing so would require us + // to copy them into a JNA `Memory`. So we might as well directly copy them into a `RustBuffer`. + val rbuf = RustBuffer.alloc(byteArr.size) + rbuf.asByteBuffer()!!.put(byteArr) + return rbuf + } + + // We aren't sure exactly how many bytes our string will be once it's UTF-8 + // encoded. Allocate 3 bytes per unicode codepoint which will always be + // enough. + override fun allocationSize(value: String): Int { + val sizeForLength = 4 + val sizeForString = value.length * 3 + return sizeForLength + sizeForString + } + + override fun write(value: String, buf: ByteBuffer) { + val byteArr = value.toByteArray(Charsets.UTF_8) + buf.putInt(byteArr.size) + buf.put(byteArr) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/TimestampHelper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/TimestampHelper.kt new file mode 100644 index 0000000000..21069d7ce8 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/TimestampHelper.kt @@ -0,0 +1,38 @@ +public object FfiConverterTimestamp: FfiConverterRustBuffer<java.time.Instant> { + override fun read(buf: ByteBuffer): java.time.Instant { + val seconds = buf.getLong() + // Type mismatch (should be u32) but we check for overflow/underflow below + val nanoseconds = buf.getInt().toLong() + if (nanoseconds < 0) { + throw java.time.DateTimeException("Instant nanoseconds exceed minimum or maximum supported by uniffi") + } + if (seconds >= 0) { + return java.time.Instant.EPOCH.plus(java.time.Duration.ofSeconds(seconds, nanoseconds)) + } else { + return java.time.Instant.EPOCH.minus(java.time.Duration.ofSeconds(-seconds, nanoseconds)) + } + } + + // 8 bytes for seconds, 4 bytes for nanoseconds + override fun allocationSize(value: java.time.Instant) = 12 + + override fun write(value: java.time.Instant, buf: ByteBuffer) { + var epochOffset = java.time.Duration.between(java.time.Instant.EPOCH, value) + + var sign = 1 + if (epochOffset.isNegative()) { + sign = -1 + epochOffset = epochOffset.negated() + } + + if (epochOffset.nano < 0) { + // Java docs provide guarantee that nano will always be positive, so this should be impossible + // See: https://docs.oracle.com/javase/8/docs/api/java/time/Instant.html + throw IllegalArgumentException("Invalid timestamp, nano value must be non-negative") + } + + buf.putLong(sign * epochOffset.seconds) + // Type mismatch (should be u32) but since values will always be between 0 and 999,999,999 it should be OK + buf.putInt(epochOffset.nano) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt new file mode 100644 index 0000000000..180a64066a --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt @@ -0,0 +1,17 @@ +{%- match func.throws_type() -%} +{%- when Some with (throwable) %} +@Throws({{ throwable|type_name }}::class) +{%- else -%} +{%- endmatch %} +{%- match func.return_type() -%} +{%- when Some with (return_type) %} + +fun {{ func.name()|fn_name }}({%- call kt::arg_list_decl(func) -%}): {{ return_type|type_name }} { + return {{ return_type|lift_fn }}({% call kt::to_ffi_call(func) %}) +} + +{% when None %} + +fun {{ func.name()|fn_name }}({% call kt::arg_list_decl(func) %}) = + {% call kt::to_ffi_call(func) %} +{% endmatch %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt new file mode 100644 index 0000000000..068bc05de5 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt @@ -0,0 +1,94 @@ +{%- import "macros.kt" as kt %} + +{%- 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 %} +{%- let contains_object_references = ci.item_contains_object_references(type_) %} + +{# + # Map `Type` instances to an include statement for that type. + # + # There is a companion match in `KotlinCodeOracle::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.kt" %} + +{%- when Type::Int8 %} +{%- include "Int8Helper.kt" %} + +{%- when Type::Int16 %} +{%- include "Int16Helper.kt" %} + +{%- when Type::Int32 %} +{%- include "Int32Helper.kt" %} + +{%- when Type::Int64 %} +{%- include "Int64Helper.kt" %} + +{%- when Type::UInt8 %} +{%- include "UInt8Helper.kt" %} + +{%- when Type::UInt16 %} +{%- include "UInt16Helper.kt" %} + +{%- when Type::UInt32 %} +{%- include "UInt32Helper.kt" %} + +{%- when Type::UInt64 %} +{%- include "UInt64Helper.kt" %} + +{%- when Type::Float32 %} +{%- include "Float32Helper.kt" %} + +{%- when Type::Float64 %} +{%- include "Float64Helper.kt" %} + +{%- when Type::String %} +{%- include "StringHelper.kt" %} + +{%- when Type::Enum(name) %} +{% include "EnumTemplate.kt" %} + +{%- when Type::Error(name) %} +{% include "ErrorTemplate.kt" %} + +{%- when Type::Object(name) %} +{% include "ObjectTemplate.kt" %} + +{%- when Type::Record(name) %} +{% include "RecordTemplate.kt" %} + +{%- when Type::Optional(inner_type) %} +{% include "OptionalTemplate.kt" %} + +{%- when Type::Sequence(inner_type) %} +{% include "SequenceTemplate.kt" %} + +{%- when Type::Map(key_type, value_type) %} +{% include "MapTemplate.kt" %} + +{%- when Type::CallbackInterface(name) %} +{% include "CallbackInterfaceTemplate.kt" %} + +{%- when Type::Timestamp %} +{% include "TimestampHelper.kt" %} + +{%- when Type::Duration %} +{% include "DurationHelper.kt" %} + +{%- when Type::Custom { name, builtin } %} +{% include "CustomTypeTemplate.kt" %} + +{%- when Type::External { crate_name, name } %} +{% include "ExternalTypeTemplate.kt" %} + +{%- else %} +{%- endmatch %} +{%- endfor %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt16Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt16Helper.kt new file mode 100644 index 0000000000..279a8fa91b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt16Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterUShort: FfiConverter<UShort, Short> { + override fun lift(value: Short): UShort { + return value.toUShort() + } + + override fun read(buf: ByteBuffer): UShort { + return lift(buf.getShort()) + } + + override fun lower(value: UShort): Short { + return value.toShort() + } + + override fun allocationSize(value: UShort) = 2 + + override fun write(value: UShort, buf: ByteBuffer) { + buf.putShort(value.toShort()) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt32Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt32Helper.kt new file mode 100644 index 0000000000..da7b5b28d6 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt32Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterUInt: FfiConverter<UInt, Int> { + override fun lift(value: Int): UInt { + return value.toUInt() + } + + override fun read(buf: ByteBuffer): UInt { + return lift(buf.getInt()) + } + + override fun lower(value: UInt): Int { + return value.toInt() + } + + override fun allocationSize(value: UInt) = 4 + + override fun write(value: UInt, buf: ByteBuffer) { + buf.putInt(value.toInt()) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt64Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt64Helper.kt new file mode 100644 index 0000000000..44d27ad36b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt64Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterULong: FfiConverter<ULong, Long> { + override fun lift(value: Long): ULong { + return value.toULong() + } + + override fun read(buf: ByteBuffer): ULong { + return lift(buf.getLong()) + } + + override fun lower(value: ULong): Long { + return value.toLong() + } + + override fun allocationSize(value: ULong) = 8 + + override fun write(value: ULong, buf: ByteBuffer) { + buf.putLong(value.toLong()) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt8Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt8Helper.kt new file mode 100644 index 0000000000..b6d176603e --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt8Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterUByte: FfiConverter<UByte, Byte> { + override fun lift(value: Byte): UByte { + return value.toUByte() + } + + override fun read(buf: ByteBuffer): UByte { + return lift(buf.get()) + } + + override fun lower(value: UByte): Byte { + return value.toByte() + } + + override fun allocationSize(value: UByte) = 1 + + override fun write(value: UByte, buf: ByteBuffer) { + buf.put(value.toByte()) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt new file mode 100644 index 0000000000..32182d5e45 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt @@ -0,0 +1,82 @@ +{# +// 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) %} + rustCallWithError({{ e|type_name}}) + {%- else %} + rustCall() + {%- endmatch %} { _status -> + _UniFFILib.INSTANCE.{{ func.ffi_func().name() }}({% call _arg_list_ffi_call(func) -%}{% if func.arguments().len() > 0 %},{% endif %} _status) +} +{%- endmacro -%} + +{%- macro to_ffi_call_with_prefix(prefix, func) %} + {%- match func.throws_type() %} + {%- when Some with (e) %} + rustCallWithError({{ e|type_name}}) + {%- else %} + rustCall() + {%- endmatch %} { _status -> + _UniFFILib.INSTANCE.{{ func.ffi_func().name() }}( + {{- prefix }}, {% call _arg_list_ffi_call(func) %}{% if func.arguments().len() > 0 %}, {% endif %} _status) +} +{%- 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 kotlin 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 }}: {{ arg|type_name -}} + {%- match arg.default_value() %} + {%- when Some with(literal) %} = {{ literal|render_literal(arg) }} + {%- else %} + {%- endmatch %} + {%- if !loop.last %}, {% endif -%} + {%- endfor %} +{%- endmacro %} + +{% macro arg_list_protocol(func) %} + {%- for arg in func.arguments() -%} + {{ arg.name()|var_name }}: {{ arg|type_name -}} + {%- 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.name()|var_name }}: {{ arg.type_().borrow()|ffi_type_name -}}, + {%- endfor %} + _uniffi_out_err: RustCallStatus +{%- endmacro -%} + +// Macro for destroying fields +{%- macro destroy_fields(member) %} + Disposable.destroy( + {%- for field in member.fields() %} + this.{{ field.name()|var_name }}{%- if !loop.last %}, {% endif -%} + {% endfor -%}) +{%- endmacro -%} + +{%- macro ffi_function_definition(func) %} +fun {{ func.name()|fn_name }}( + {%- call arg_list_ffi_decl(func) %} +){%- match func.return_type() -%}{%- when Some with (type_) %}: {{ type_|ffi_type_name }}{% when None %}: Unit{% endmatch %} +{% endmacro %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/wrapper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/wrapper.kt new file mode 100644 index 0000000000..ef42d8e616 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/wrapper.kt @@ -0,0 +1,48 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +@file:Suppress("NAME_SHADOWING") + +package {{ config.package_name() }}; + +// Common helper code. +// +// Ideally this would live in a separate .kt file where it can be unittested etc +// in isolation, and perhaps even published as a re-useable package. +// +// However, it's important that the detils 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 Kotlin +// helpers directly inline like we're doing here. + +import com.sun.jna.Library +import com.sun.jna.Native +import com.sun.jna.Pointer +import com.sun.jna.Structure +import com.sun.jna.ptr.ByReference +import java.nio.ByteBuffer +import java.nio.ByteOrder + +{%- for req in self.imports() %} +{{ req.render() }} +{%- endfor %} + +{% include "RustBufferTemplate.kt" %} +{% include "FfiConverterTemplate.kt" %} +{% include "Helpers.kt" %} + +// Contains loading, initialization code, +// and the FFI Function declarations in a com.sun.jna.Library. +{% include "NamespaceLibraryTemplate.kt" %} + +// Public interface members begin here. +{{ type_helper_code }} + +{%- for func in ci.function_definitions() %} +{%- include "TopLevelFunctionTemplate.kt" %} +{%- endfor %} + + +{% import "macros.kt" as kt %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/test.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/test.rs new file mode 100644 index 0000000000..e8172ed15b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/test.rs @@ -0,0 +1,107 @@ +/* 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::{bail, Context, Result}; +use camino::{Utf8Path, Utf8PathBuf}; +use std::env; +use std::process::Command; +use uniffi_testing::UniFFITestHelper; + +/// Run Kotlin 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); + 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_fixture_library_to_out_dir")?; + generate_sources(&test_helper.cdylib_path()?, &out_dir, &test_helper) + .context("generate_sources")?; + let jar_file = build_jar(fixture_name, &out_dir).context("build_jar")?; + + let status = Command::new("kotlinc") + .arg("-classpath") + .arg(calc_classpath(vec![&out_dir, &jar_file])) + // Enable runtime assertions, for easy testing etc. + .arg("-J-ea") + // Our test scripts should not produce any warnings. + .arg("-Werror") + .arg("-script") + .arg(script_path) + .spawn() + .context("Failed to spawn `kotlinc` to run Kotlin script")? + .wait() + .context("Failed to wait for `kotlinc` when running Kotlin script")?; + if !status.success() { + anyhow::bail!("running `kotlinc` failed") + } + Ok(()) +} + +fn generate_sources( + library_path: &Utf8Path, + out_dir: &Utf8Path, + test_helper: &UniFFITestHelper, +) -> Result<()> { + for source in test_helper.get_compile_sources()? { + println!( + "Generating bindings: {:?} {:?} {:?}", + source.udl_path, source.config_path, out_dir + ); + crate::generate_bindings( + &source.udl_path, + source.config_path.as_deref(), + vec!["kotlin"], + Some(out_dir), + Some(library_path), + false, + )?; + } + Ok(()) +} + +/// Generate kotlin bindings for the given namespace, then use the kotlin +/// command-line tools to compile them into a .jar file. +fn build_jar(fixture_name: &str, out_dir: &Utf8Path) -> Result<Utf8PathBuf> { + let mut jar_file = Utf8PathBuf::from(out_dir); + jar_file.push(format!("{}.jar", fixture_name)); + let sources = glob::glob(out_dir.join("**/*.kt").as_str())? + .flatten() + .map(|p| String::from(p.to_string_lossy())) + .collect::<Vec<String>>(); + if sources.is_empty() { + bail!("No kotlin sources found in {out_dir}") + } + println!("building jar from: {:?}", sources); + + let status = Command::new("kotlinc") + // Our generated bindings should not produce any warnings; fail tests if they do. + .arg("-Werror") + .arg("-d") + .arg(&jar_file) + .arg("-classpath") + .arg(calc_classpath(vec![])) + .args(sources) + .spawn() + .context("Failed to spawn `kotlinc` to compile the bindings")? + .wait() + .context("Failed to wait for `kotlinc` when compiling the bindings")?; + if !status.success() { + bail!("running `kotlinc` failed") + } + Ok(jar_file) +} + +fn calc_classpath(extra_paths: Vec<&Utf8Path>) -> String { + extra_paths + .into_iter() + .map(|p| p.to_string()) + // Add the system classpath as a component, using the fact that env::var returns an Option, + // which implement Iterator + .chain(env::var("CLASSPATH")) + .collect::<Vec<String>>() + .join(":") +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/mod.rs new file mode 100644 index 0000000000..10f6e5fe2c --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/mod.rs @@ -0,0 +1,121 @@ +/* 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/. */ + +//! Generate foreign language bindings for a uniffi component. +//! +//! This module contains all the code for generating foreign language bindings, +//! along with some helpers for executing foreign language scripts or tests. + +use anyhow::{bail, Result}; +use camino::Utf8Path; +use serde::{Deserialize, Serialize}; + +use crate::interface::ComponentInterface; +use crate::MergeWith; + +pub mod kotlin; +pub mod python; +pub mod ruby; +pub mod swift; + +/// Enumeration of all foreign language targets currently supported by this crate. +/// +/// The functions in this module will delegate to a language-specific backend based +/// on the provided `TargetLanguage`. For convenience of calling code we also provide +/// a few `TryFrom` implementations to help guess the correct target language from +/// e.g. a file extension of command-line argument. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub enum TargetLanguage { + Kotlin, + Swift, + Python, + Ruby, +} + +impl TryFrom<&str> for TargetLanguage { + type Error = anyhow::Error; + fn try_from(value: &str) -> Result<Self> { + Ok(match value.to_ascii_lowercase().as_str() { + "kotlin" | "kt" | "kts" => TargetLanguage::Kotlin, + "swift" => TargetLanguage::Swift, + "python" | "py" => TargetLanguage::Python, + "ruby" | "rb" => TargetLanguage::Ruby, + _ => bail!("Unknown or unsupported target language: \"{}\"", value), + }) + } +} + +impl TryFrom<&std::ffi::OsStr> for TargetLanguage { + type Error = anyhow::Error; + fn try_from(value: &std::ffi::OsStr) -> Result<Self> { + match value.to_str() { + None => bail!("Unreadable target language"), + Some(s) => s.try_into(), + } + } +} + +impl TryFrom<String> for TargetLanguage { + type Error = anyhow::Error; + fn try_from(value: String) -> Result<Self> { + TryFrom::try_from(value.as_str()) + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Config { + #[serde(default)] + kotlin: kotlin::Config, + #[serde(default)] + swift: swift::Config, + #[serde(default)] + python: python::Config, + #[serde(default)] + ruby: ruby::Config, +} + +impl From<&ComponentInterface> for Config { + fn from(ci: &ComponentInterface) -> Self { + Config { + kotlin: ci.into(), + swift: ci.into(), + python: ci.into(), + ruby: ci.into(), + } + } +} + +impl MergeWith for Config { + fn merge_with(&self, other: &Self) -> Self { + Config { + kotlin: self.kotlin.merge_with(&other.kotlin), + swift: self.swift.merge_with(&other.swift), + python: self.python.merge_with(&other.python), + ruby: self.ruby.merge_with(&other.ruby), + } + } +} + +/// Generate foreign language bindings from a compiled `uniffi` library. +pub fn write_bindings( + config: &Config, + ci: &ComponentInterface, + out_dir: &Utf8Path, + language: TargetLanguage, + try_format_code: bool, +) -> Result<()> { + match language { + TargetLanguage::Kotlin => { + kotlin::write_bindings(&config.kotlin, ci, out_dir, try_format_code)? + } + TargetLanguage::Swift => { + swift::write_bindings(&config.swift, ci, out_dir, try_format_code)? + } + TargetLanguage::Python => { + python::write_bindings(&config.python, ci, out_dir, try_format_code)? + } + TargetLanguage::Ruby => ruby::write_bindings(&config.ruby, ci, out_dir, try_format_code)?, + } + Ok(()) +} 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(()) +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs new file mode 100644 index 0000000000..9d0fc55bdc --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs @@ -0,0 +1,273 @@ +/* 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::Result; +use askama::Template; +use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase}; +use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; + +use crate::interface::*; +use crate::MergeWith; + +const RESERVED_WORDS: &[&str] = &[ + "alias", "and", "BEGIN", "begin", "break", "case", "class", "def", "defined?", "do", "else", + "elsif", "END", "end", "ensure", "false", "for", "if", "module", "next", "nil", "not", "or", + "redo", "rescue", "retry", "return", "self", "super", "then", "true", "undef", "unless", + "until", "when", "while", "yield", "__FILE__", "__LINE__", +]; + +fn is_reserved_word(word: &str) -> bool { + RESERVED_WORDS.contains(&word) +} + +// Some config options for it the caller wants to customize the generated ruby. +// Note that this can only be used to control details of the ruby *that do not affect the underlying component*, +// since the details of the underlying component are entirely determined by the `ComponentInterface`. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Config { + cdylib_name: Option<String>, + cdylib_path: Option<String>, +} + +impl Config { + pub fn cdylib_name(&self) -> String { + self.cdylib_name + .clone() + .unwrap_or_else(|| "uniffi".to_string()) + } + + pub fn custom_cdylib_path(&self) -> bool { + self.cdylib_path.is_some() + } + + pub fn cdylib_path(&self) -> String { + self.cdylib_path.clone().unwrap_or_default() + } +} + +impl From<&ComponentInterface> for Config { + fn from(ci: &ComponentInterface) -> Self { + Config { + cdylib_name: Some(format!("uniffi_{}", ci.namespace())), + cdylib_path: None, + } + } +} + +impl MergeWith for Config { + fn merge_with(&self, other: &Self) -> Self { + Config { + cdylib_name: self.cdylib_name.merge_with(&other.cdylib_name), + cdylib_path: self.cdylib_path.merge_with(&other.cdylib_path), + } + } +} + +#[derive(Template)] +#[template(syntax = "rb", escape = "none", path = "wrapper.rb")] +pub struct RubyWrapper<'a> { + config: Config, + ci: &'a ComponentInterface, +} +impl<'a> RubyWrapper<'a> { + pub fn new(config: Config, ci: &'a ComponentInterface) -> Self { + Self { config, ci } + } +} + +mod filters { + use super::*; + + pub fn type_ffi(type_: &FfiType) -> Result<String, askama::Error> { + Ok(match type_ { + FfiType::Int8 => ":int8".to_string(), + FfiType::UInt8 => ":uint8".to_string(), + FfiType::Int16 => ":int16".to_string(), + FfiType::UInt16 => ":uint16".to_string(), + FfiType::Int32 => ":int32".to_string(), + FfiType::UInt32 => ":uint32".to_string(), + FfiType::Int64 => ":int64".to_string(), + FfiType::UInt64 => ":uint64".to_string(), + FfiType::Float32 => ":float".to_string(), + FfiType::Float64 => ":double".to_string(), + FfiType::RustArcPtr(_) => ":pointer".to_string(), + FfiType::RustBuffer(_) => "RustBuffer.by_value".to_string(), + FfiType::ForeignBytes => "ForeignBytes".to_string(), + FfiType::ForeignCallback => unimplemented!("Callback interfaces are not implemented"), + }) + } + + pub fn literal_rb(literal: &Literal) -> Result<String, askama::Error> { + Ok(match literal { + Literal::Boolean(v) => { + if *v { + "true".into() + } else { + "false".into() + } + } + // use the double-quote form to match with the other languages, and quote escapes. + Literal::String(s) => format!("\"{s}\""), + Literal::Null => "nil".into(), + Literal::EmptySequence => "[]".into(), + Literal::EmptyMap => "{}".into(), + Literal::Enum(v, type_) => match type_ { + Type::Enum(name) => format!("{}::{}", class_name_rb(name)?, enum_name_rb(v)?), + _ => panic!("Unexpected type in enum literal: {type_:?}"), + }, + // https://docs.ruby-lang.org/en/2.0.0/syntax/literals_rdoc.html + Literal::Int(i, radix, _) => match radix { + Radix::Octal => format!("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(), + }) + } + + pub fn class_name_rb(nm: &str) -> Result<String, askama::Error> { + Ok(nm.to_string().to_upper_camel_case()) + } + + pub fn fn_name_rb(nm: &str) -> Result<String, askama::Error> { + Ok(nm.to_string().to_snake_case()) + } + + pub fn var_name_rb(nm: &str) -> Result<String, askama::Error> { + let nm = nm.to_string(); + let prefix = if is_reserved_word(&nm) { "_" } else { "" }; + + Ok(format!("{prefix}{}", nm.to_snake_case())) + } + + pub fn enum_name_rb(nm: &str) -> Result<String, askama::Error> { + Ok(nm.to_string().to_shouty_snake_case()) + } + + pub fn coerce_rb(nm: &str, type_: &Type) -> Result<String, askama::Error> { + Ok(match type_ { + Type::Int8 + | Type::UInt8 + | Type::Int16 + | Type::UInt16 + | Type::Int32 + | Type::UInt32 + | Type::Int64 + | Type::UInt64 => format!("{nm}.to_i"), // TODO: check max/min value + Type::Float32 | Type::Float64 => format!("{nm}.to_f"), + Type::Boolean => format!("{nm} ? true : false"), + Type::Object(_) | Type::Enum(_) | Type::Error(_) | Type::Record(_) => nm.to_string(), + Type::String => format!("{nm}.to_s"), + Type::Timestamp | Type::Duration => nm.to_string(), + Type::CallbackInterface(_) => panic!("No support for coercing callback interfaces yet"), + Type::Optional(t) => format!("({nm} ? {} : nil)", coerce_rb(nm, t)?), + Type::Sequence(t) => { + let coerce_code = coerce_rb("v", t)?; + if coerce_code == "v" { + nm.to_string() + } else { + format!("{nm}.map {{ |v| {coerce_code} }}") + } + } + Type::Map(_k, t) => { + let k_coerce_code = coerce_rb("k", &Type::String)?; + let v_coerce_code = coerce_rb("v", t)?; + + if k_coerce_code == "k" && v_coerce_code == "v" { + nm.to_string() + } else { + format!( + "{}.each.with_object({{}}) {{ |(k, v), res| res[{}] = {} }}", + nm, k_coerce_code, v_coerce_code, + ) + } + } + Type::External { .. } => panic!("No support for external types, yet"), + Type::Custom { .. } => panic!("No support for custom types, yet"), + Type::Unresolved { name } => { + unreachable!("Type `{name}` must be resolved before calling coerce_rb") + } + }) + } + + pub fn lower_rb(nm: &str, type_: &Type) -> Result<String, askama::Error> { + Ok(match type_ { + Type::Int8 + | Type::UInt8 + | Type::Int16 + | Type::UInt16 + | Type::Int32 + | Type::UInt32 + | Type::Int64 + | Type::UInt64 + | Type::Float32 + | Type::Float64 => nm.to_string(), + Type::Boolean => format!("({nm} ? 1 : 0)"), + Type::String => format!("RustBuffer.allocFromString({nm})"), + Type::Object(name) => format!("({}._uniffi_lower {nm})", class_name_rb(name)?), + Type::CallbackInterface(_) => panic!("No support for lowering callback interfaces yet"), + Type::Error(_) => panic!("No support for lowering errors, yet"), + Type::Enum(_) + | Type::Record(_) + | Type::Optional(_) + | Type::Sequence(_) + | Type::Timestamp + | Type::Duration + | Type::Map(_, _) => format!( + "RustBuffer.alloc_from_{}({})", + class_name_rb(&type_.canonical_name())?, + nm + ), + Type::External { .. } => panic!("No support for lowering external types, yet"), + Type::Custom { .. } => panic!("No support for lowering custom types, yet"), + Type::Unresolved { name } => { + unreachable!("Type `{name}` must be resolved before calling lower_rb") + } + }) + } + + pub fn lift_rb(nm: &str, type_: &Type) -> Result<String, askama::Error> { + Ok(match type_ { + Type::Int8 + | Type::UInt8 + | Type::Int16 + | Type::UInt16 + | Type::Int32 + | Type::UInt32 + | Type::Int64 + | Type::UInt64 => format!("{nm}.to_i"), + Type::Float32 | Type::Float64 => format!("{nm}.to_f"), + Type::Boolean => format!("1 == {nm}"), + Type::String => format!("{nm}.consumeIntoString"), + Type::Object(name) => format!("{}._uniffi_allocate({nm})", class_name_rb(name)?), + Type::CallbackInterface(_) => panic!("No support for lifting callback interfaces, yet"), + Type::Error(_) => panic!("No support for lowering errors, yet"), + Type::Enum(_) + | Type::Record(_) + | Type::Optional(_) + | Type::Sequence(_) + | Type::Timestamp + | Type::Duration + | Type::Map(_, _) => format!( + "{}.consumeInto{}", + nm, + class_name_rb(&type_.canonical_name())? + ), + Type::External { .. } => panic!("No support for lifting external types, yet"), + Type::Custom { .. } => panic!("No support for lifting custom types, yet"), + Type::Unresolved { name } => { + unreachable!("Type `{name}` must be resolved before calling lift_rb") + } + }) + } +} + +#[cfg(test)] +mod tests; diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/tests.rs b/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/tests.rs new file mode 100644 index 0000000000..9ae5d1816f --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/tests.rs @@ -0,0 +1,47 @@ +use super::{is_reserved_word, Config}; + +#[test] +fn when_reserved_word() { + assert!(is_reserved_word("end")); +} + +#[test] +fn when_not_reserved_word() { + assert!(!is_reserved_word("ruby")); +} + +#[test] +fn cdylib_name() { + let config = Config { + cdylib_name: None, + cdylib_path: None, + }; + + assert_eq!("uniffi", config.cdylib_name()); + + let config = Config { + cdylib_name: Some("todolist".to_string()), + cdylib_path: None, + }; + + assert_eq!("todolist", config.cdylib_name()); +} + +#[test] +fn cdylib_path() { + let config = Config { + cdylib_name: None, + cdylib_path: None, + }; + + assert_eq!("", config.cdylib_path()); + assert!(!config.custom_cdylib_path()); + + let config = Config { + cdylib_name: None, + cdylib_path: Some("/foo/bar".to_string()), + }; + + assert_eq!("/foo/bar", config.cdylib_path()); + assert!(config.custom_cdylib_path()); +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/ruby/mod.rs new file mode 100644 index 0000000000..e0d789f42f --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/mod.rs @@ -0,0 +1,50 @@ +/* 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::{Context, Result}; +use camino::Utf8Path; +use fs_err::File; + +pub mod gen_ruby; +mod test; +pub use gen_ruby::{Config, RubyWrapper}; +pub use test::run_test; + +use super::super::interface::ComponentInterface; + +// Generate ruby 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 rb_file = out_dir.join(format!("{}.rb", ci.namespace())); + let mut f = File::create(&rb_file)?; + write!(f, "{}", generate_ruby_bindings(config, ci)?)?; + + if try_format_code { + if let Err(e) = Command::new("rubocop").arg("-A").arg(&rb_file).output() { + println!( + "Warning: Unable to auto-format {} using rubocop: {:?}", + rb_file.file_name().unwrap(), + e + ) + } + } + + Ok(()) +} + +// Generate ruby bindings for the given ComponentInterface, as a string. + +pub fn generate_ruby_bindings(config: &Config, ci: &ComponentInterface) -> Result<String> { + use askama::Template; + RubyWrapper::new(config.clone(), ci) + .render() + .context("failed to render ruby bindings") +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/EnumTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/EnumTemplate.rb new file mode 100644 index 0000000000..23b701f6a7 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/EnumTemplate.rb @@ -0,0 +1,59 @@ +{% if e.is_flat() %} + +class {{ e.name()|class_name_rb }} + {% for variant in e.variants() -%} + {{ variant.name()|enum_name_rb }} = {{ loop.index }} + {% endfor %} +end + +{% else %} + +class {{ e.name()|class_name_rb }} + def initialize + raise RuntimeError, '{{ e.name()|class_name_rb }} cannot be instantiated directly' + end + + # Each enum variant is a nested class of the enum itself. + {% for variant in e.variants() -%} + class {{ variant.name()|enum_name_rb }} + {% if variant.has_fields() %} + attr_reader {% for field in variant.fields() %}:{{ field.name()|var_name_rb }}{% if loop.last %}{% else %}, {% endif %}{%- endfor %} + {% endif %} + def initialize({% for field in variant.fields() %}{{ field.name()|var_name_rb }}{% if loop.last %}{% else %}, {% endif %}{% endfor %}) + {% if variant.has_fields() %} + {%- for field in variant.fields() %} + @{{ field.name()|var_name_rb }} = {{ field.name()|var_name_rb }} + {%- endfor %} + {% else %} + {% endif %} + end + + def to_s + "{{ e.name()|class_name_rb }}::{{ variant.name()|enum_name_rb }}({% for field in variant.fields() %}{{ field.name() }}=#{@{{ field.name() }}}{% if loop.last %}{% else %}, {% endif %}{% endfor %})" + end + + def ==(other) + if !other.{{ variant.name()|var_name_rb }}? + return false + end + {%- for field in variant.fields() %} + if @{{ field.name()|var_name_rb }} != other.{{ field.name()|var_name_rb }} + return false + end + {%- endfor %} + + true + end + + # For each variant, we have an `NAME?` method for easily checking + # whether an instance is that variant. + {% for variant in e.variants() %} + def {{ variant.name()|var_name_rb }}? + instance_of? {{ e.name()|class_name_rb }}::{{ variant.name()|enum_name_rb }} + end + {% endfor %} + end + {% endfor %} +end + +{% endif %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/ErrorTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/ErrorTemplate.rb new file mode 100644 index 0000000000..3a64e9ffeb --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/ErrorTemplate.rb @@ -0,0 +1,117 @@ +class RustCallStatus < FFI::Struct + layout :code, :int8, + :error_buf, RustBuffer + + def code + self[:code] + end + + def error_buf + self[:error_buf] + end + + def to_s + "RustCallStatus(code=#{self[:code]})" + end +end + +# These match the values from the uniffi::rustcalls module +CALL_SUCCESS = 0 +CALL_ERROR = 1 +CALL_PANIC = 2 +{%- for e in ci.error_definitions() %} +{% if e.is_flat() %} +class {{ e.name()|class_name_rb }} + {%- for variant in e.variants() %} + {{ variant.name()|class_name_rb }} = Class.new StandardError + {%- endfor %} +{% else %} +module {{ e.name()|class_name_rb }} + {%- for variant in e.variants() %} + class {{ variant.name()|class_name_rb }} < StandardError + def initialize({% for field in variant.fields() %}{{ field.name()|var_name_rb }}{% if !loop.last %}, {% endif %}{% endfor %}) + {%- for field in variant.fields() %} + @{{ field.name()|var_name_rb }} = {{ field.name()|var_name_rb }} + {%- endfor %} + super() + end + {%- if variant.has_fields() %} + + attr_reader {% for field in variant.fields() %}:{{ field.name()|var_name_rb }}{% if !loop.last %}, {% endif %}{% endfor %} + {% endif %} + + def to_s + "#{self.class.name}({% for field in variant.fields() %}{{ field.name()|var_name_rb }}=#{@{{ field.name()|var_name_rb }}.inspect}{% if !loop.last %}, {% endif %}{% endfor %})" + end + end + {%- endfor %} +{% endif %} +end +{%- endfor %} + +# Map error modules to the RustBuffer method name that reads them +ERROR_MODULE_TO_READER_METHOD = { +{%- for e in ci.error_definitions() %} +{%- let typ=ci.get_type(e.name()).unwrap() %} +{%- let canonical_type_name = typ.canonical_name().borrow()|class_name_rb %} + {{ e.name()|class_name_rb }} => :read{{ canonical_type_name }}, +{%- endfor %} +} + +private_constant :ERROR_MODULE_TO_READER_METHOD, :CALL_SUCCESS, :CALL_ERROR, :CALL_PANIC, + :RustCallStatus + +def self.consume_buffer_into_error(error_module, rust_buffer) + rust_buffer.consumeWithStream do |stream| + reader_method = ERROR_MODULE_TO_READER_METHOD[error_module] + return stream.send(reader_method) + end +end + +class InternalError < StandardError +end + +def self.rust_call(fn_name, *args) + # Call a rust function + rust_call_with_error(nil, fn_name, *args) +end + +def self.rust_call_with_error(error_module, fn_name, *args) + # Call a rust function and handle errors + # + # Use this when the rust function returns a Result<>. error_module must be the error_module that corresponds to that Result. + + + # Note: RustCallStatus.new zeroes out the struct, which is exactly what we + # want to pass to Rust (code=0, error_buf=RustBuffer(len=0, capacity=0, + # data=NULL)) + status = RustCallStatus.new + args << status + + result = UniFFILib.public_send(fn_name, *args) + + case status.code + when CALL_SUCCESS + result + when CALL_ERROR + if error_module.nil? + status.error_buf.free + raise InternalError, "CALL_ERROR with no error_module set" + else + raise consume_buffer_into_error(error_module, status.error_buf) + end + when 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 status.error_buf.len > 0 + raise InternalError, status.error_buf.consumeIntoString() + else + raise InternalError, "Rust panic" + end + else + raise InternalError, "Unknown call status: #{status.code}" + end +end + +private_class_method :consume_buffer_into_error diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/NamespaceLibraryTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/NamespaceLibraryTemplate.rb new file mode 100644 index 0000000000..858b42bf91 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/NamespaceLibraryTemplate.rb @@ -0,0 +1,17 @@ +# This is how we find and load the dynamic library provided by the component. +# For now we just look it up by name. +module UniFFILib + extend FFI::Library + + {% if config.custom_cdylib_path() %} + ffi_lib {{ config.cdylib_path() }} + {% else %} + ffi_lib '{{ config.cdylib_name() }}' + {% endif %} + + {% for func in ci.iter_ffi_function_definitions() -%} + attach_function :{{ func.name() }}, + {%- call rb::arg_list_ffi_decl(func) %}, + {% match func.return_type() %}{% when Some with (type_) %}{{ type_|type_ffi }}{% when None %}:void{% endmatch %} + {% endfor %} +end diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/ObjectTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/ObjectTemplate.rb new file mode 100644 index 0000000000..677c5c729b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/ObjectTemplate.rb @@ -0,0 +1,73 @@ +class {{ obj.name()|class_name_rb }} + + # A private helper for initializing instances of the class from a raw pointer, + # bypassing any initialization logic and ensuring they are GC'd properly. + def self._uniffi_allocate(pointer) + pointer.autorelease = false + inst = allocate + inst.instance_variable_set :@pointer, pointer + ObjectSpace.define_finalizer(inst, _uniffi_define_finalizer_by_pointer(pointer, inst.object_id)) + return inst + end + + # A private helper for registering an object finalizer. + # N.B. it's important that this does not capture a reference + # to the actual instance, only its underlying pointer. + def self._uniffi_define_finalizer_by_pointer(pointer, object_id) + Proc.new do |_id| + {{ ci.namespace()|class_name_rb }}.rust_call( + :{{ obj.ffi_object_free().name() }}, + pointer + ) + end + end + + # A private helper for lowering instances into a raw pointer. + # This does an explicit typecheck, because accidentally lowering a different type of + # object in a place where this type is expected, could lead to memory unsafety. + def self._uniffi_lower(inst) + if not inst.is_a? self + raise TypeError.new "Expected a {{ obj.name()|class_name_rb }} instance, got #{inst}" + end + return inst.instance_variable_get :@pointer + end + + {%- match obj.primary_constructor() %} + {%- when Some with (cons) %} + def initialize({% call rb::arg_list_decl(cons) -%}) + {%- call rb::coerce_args_extra_indent(cons) %} + pointer = {% call rb::to_ffi_call(cons) %} + @pointer = pointer + ObjectSpace.define_finalizer(self, self.class._uniffi_define_finalizer_by_pointer(pointer, self.object_id)) + end + {%- when None %} + {%- endmatch %} + + {% for cons in obj.alternate_constructors() -%} + def self.{{ cons.name()|fn_name_rb }}({% call rb::arg_list_decl(cons) %}) + {%- call rb::coerce_args_extra_indent(cons) %} + # Call the (fallible) function before creating any half-baked object instances. + # Lightly yucky way to bypass the usual "initialize" logic + # and just create a new instance with the required pointer. + return _uniffi_allocate({% call rb::to_ffi_call(cons) %}) + end + {% endfor %} + + {% for meth in obj.methods() -%} + {%- match meth.return_type() -%} + + {%- when Some with (return_type) -%} + def {{ meth.name()|fn_name_rb }}({% call rb::arg_list_decl(meth) %}) + {%- call rb::coerce_args_extra_indent(meth) %} + result = {% call rb::to_ffi_call_with_prefix("@pointer", meth) %} + return {{ "result"|lift_rb(return_type) }} + end + + {%- when None -%} + def {{ meth.name()|fn_name_rb }}({% call rb::arg_list_decl(meth) %}) + {%- call rb::coerce_args_extra_indent(meth) %} + {% call rb::to_ffi_call_with_prefix("@pointer", meth) %} + end + {% endmatch %} + {% endfor %} +end diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RecordTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RecordTemplate.rb new file mode 100644 index 0000000000..c940b31060 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RecordTemplate.rb @@ -0,0 +1,20 @@ +# Record type {{ rec.name() }} +class {{ rec.name()|class_name_rb }} + attr_reader {% for field in rec.fields() %}:{{ field.name()|var_name_rb }}{% if loop.last %}{% else %}, {% endif %}{%- endfor %} + + def initialize({% for field in rec.fields() %}{{ field.name()|var_name_rb }}{% if loop.last %}{% else %}, {% endif %}{% endfor %}) + {%- for field in rec.fields() %} + @{{ field.name()|var_name_rb }} = {{ field.name()|var_name_rb }} + {%- endfor %} + end + + def ==(other) + {%- for field in rec.fields() %} + if @{{ field.name()|var_name_rb }} != other.{{ field.name()|var_name_rb }} + return false + end + {%- endfor %} + + true + end +end diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferBuilder.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferBuilder.rb new file mode 100644 index 0000000000..e4b3910b6c --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferBuilder.rb @@ -0,0 +1,246 @@ + +# Helper for structured writing of values into a RustBuffer. +class RustBufferBuilder + def initialize + @rust_buf = RustBuffer.alloc 16 + @rust_buf.len = 0 + end + + def finalize + rbuf = @rust_buf + + @rust_buf = nil + + rbuf + end + + def discard + return if @rust_buf.nil? + + rbuf = finalize + rbuf.free + end + + def write(value) + reserve(value.bytes.size) do + @rust_buf.data.put_array_of_char @rust_buf.len, value.bytes + end + end + + {% for typ in ci.iter_types() -%} + {%- let canonical_type_name = typ.canonical_name().borrow()|class_name_rb -%} + {%- match typ -%} + + {% when Type::Int8 -%} + + def write_I8(v) + pack_into(1, 'c', v) + end + + {% when Type::UInt8 -%} + + def write_U8(v) + pack_into(1, 'c', v) + end + + {% when Type::Int16 -%} + + def write_I16(v) + pack_into(2, 's>', v) + end + + {% when Type::UInt16 -%} + + def write_U16(v) + pack_into(2, 'S>', v) + end + + {% when Type::Int32 -%} + + def write_I32(v) + pack_into(4, 'l>', v) + end + + {% when Type::UInt32 -%} + + def write_U32(v) + pack_into(4, 'L>', v) + end + + {% when Type::Int64 -%} + + def write_I64(v) + pack_into(8, 'q>', v) + end + + {% when Type::UInt64 -%} + + def write_U64(v) + pack_into(8, 'Q>', v) + end + + {% when Type::Float32 -%} + + def write_F32(v) + pack_into(4, 'g', v) + end + + {% when Type::Float64 -%} + + def write_F64(v) + pack_into(8, 'G', v) + end + + {% when Type::Boolean -%} + + def write_Bool(v) + pack_into(1, 'c', v ? 1 : 0) + end + + {% when Type::String -%} + + def write_String(v) + v = v.to_s + pack_into 4, 'l>', v.bytes.size + write v + end + + {% when Type::Timestamp -%} + # The Timestamp type. + ONE_SECOND_IN_NANOSECONDS = 10**9 + + def write_{{ canonical_type_name }}(v) + seconds = v.tv_sec + nanoseconds = v.tv_nsec + + # UniFFi conventions assume that nanoseconds part has to represent nanoseconds portion of + # duration between epoch and the timestamp moment. Ruby `Time#tv_nsec` returns the number of + # nanoseconds for the subsecond part, which is sort of opposite to "duration" meaning. + # Hence we need to convert value returned by `Time#tv_nsec` back and forth with the following + # logic: + if seconds < 0 && nanoseconds != 0 + # In order to get duration nsec we shift by 1 second: + nanoseconds = ONE_SECOND_IN_NANOSECONDS - nanoseconds + + # Then we compensate 1 second shift: + seconds += 1 + end + + pack_into 8, 'q>', seconds + pack_into 4, 'L>', nanoseconds + end + + {% when Type::Duration -%} + # The Duration type. + + def write_{{ canonical_type_name }}(v) + seconds = v.tv_sec + nanoseconds = v.tv_nsec + + raise ArgumentError, 'Invalid duration, must be non-negative' if seconds < 0 + + pack_into 8, 'Q>', seconds + pack_into 4, 'L>', nanoseconds + end + + {% when Type::Object with (object_name) -%} + # The Object type {{ object_name }}. + + def write_{{ canonical_type_name }}(obj) + pointer = {{ object_name|class_name_rb}}._uniffi_lower obj + pack_into(8, 'Q>', pointer.address) + end + + {% when Type::Enum with (enum_name) -%} + {%- let e = ci.get_enum_definition(enum_name).unwrap() -%} + # The Enum type {{ enum_name }}. + + def write_{{ canonical_type_name }}(v) + {%- if e.is_flat() %} + pack_into(4, 'l>', v) + {%- else -%} + {%- for variant in e.variants() %} + if v.{{ variant.name()|var_name_rb }}? + pack_into(4, 'l>', {{ loop.index }}) + {%- for field in variant.fields() %} + self.write_{{ field.type_().canonical_name().borrow()|class_name_rb }}(v.{{ field.name() }}) + {%- endfor %} + end + {%- endfor %} + {%- endif %} + end + + {% when Type::Record with (record_name) -%} + {%- let rec = ci.get_record_definition(record_name).unwrap() -%} + # The Record type {{ record_name }}. + + def write_{{ canonical_type_name }}(v) + {%- for field in rec.fields() %} + self.write_{{ field.type_().canonical_name().borrow()|class_name_rb }}(v.{{ field.name()|var_name_rb }}) + {%- endfor %} + end + + {% when Type::Optional with (inner_type) -%} + # The Optional<T> type for {{ inner_type.canonical_name() }}. + + def write_{{ canonical_type_name }}(v) + if v.nil? + pack_into(1, 'c', 0) + else + pack_into(1, 'c', 1) + self.write_{{ inner_type.canonical_name().borrow()|class_name_rb }}(v) + end + end + + {% when Type::Sequence with (inner_type) -%} + # The Sequence<T> type for {{ inner_type.canonical_name() }}. + + def write_{{ canonical_type_name }}(items) + pack_into(4, 'l>', items.size) + + items.each do |item| + self.write_{{ inner_type.canonical_name().borrow()|class_name_rb }}(item) + end + end + + {% when Type::Map with (k, inner_type) -%} + # The Map<T> type for {{ inner_type.canonical_name() }}. + + def write_{{ canonical_type_name }}(items) + pack_into(4, 'l>', items.size) + + items.each do |k, v| + write_String(k) + self.write_{{ inner_type.canonical_name().borrow()|class_name_rb }}(v) + end + end + + {%- else -%} + # This type is not yet supported in the Ruby backend. + def write_{{ canonical_type_name }}(v) + raise InternalError('RustBufferStream.write() not implemented yet for {{ canonical_type_name }}') + end + + {%- endmatch -%} + {%- endfor %} + + private + + def reserve(num_bytes) + if @rust_buf.len + num_bytes > @rust_buf.capacity + @rust_buf = RustBuffer.reserve(@rust_buf, num_bytes) + end + + yield + + @rust_buf.len += num_bytes + end + + def pack_into(size, format, value) + reserve(size) do + @rust_buf.data.put_array_of_char @rust_buf.len, [value].pack(format).bytes + end + end +end + +private_constant :RustBufferBuilder diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferStream.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferStream.rb new file mode 100644 index 0000000000..f48fbffad4 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferStream.rb @@ -0,0 +1,301 @@ + +# Helper for structured reading of values from a RustBuffer. +class RustBufferStream + + def initialize(rbuf) + @rbuf = rbuf + @offset = 0 + end + + def remaining + @rbuf.len - @offset + end + + def read(size) + raise InternalError, 'read past end of rust buffer' if @offset + size > @rbuf.len + + data = @rbuf.data.get_bytes @offset, size + + @offset += size + + data + end + + {% for typ in ci.iter_types() -%} + {%- let canonical_type_name = typ.canonical_name().borrow()|class_name_rb -%} + {%- match typ -%} + + {% when Type::Int8 -%} + + def readI8 + unpack_from 1, 'c' + end + + {% when Type::UInt8 -%} + + def readU8 + unpack_from 1, 'c' + end + + {% when Type::Int16 -%} + + def readI16 + unpack_from 2, 's>' + end + + {% when Type::UInt16 -%} + + def readU16 + unpack_from 2, 'S>' + end + + {% when Type::Int32 -%} + + def readI32 + unpack_from 4, 'l>' + end + + {% when Type::UInt32 -%} + + def readU32 + unpack_from 4, 'L>' + end + + {% when Type::Int64 -%} + + def readI64 + unpack_from 8, 'q>' + end + + {% when Type::UInt64 -%} + + def readU64 + unpack_from 8, 'Q>' + end + + {% when Type::Float32 -%} + + def readF32 + unpack_from 4, 'g' + end + + {% when Type::Float64 -%} + + def readF64 + unpack_from 8, 'G' + end + + {% when Type::Boolean -%} + + def readBool + v = unpack_from 1, 'c' + + return false if v == 0 + return true if v == 1 + + raise InternalError, 'Unexpected byte for Boolean type' + end + + {% when Type::String -%} + + def readString + size = unpack_from 4, 'l>' + + raise InternalError, 'Unexpected negative string length' if size.negative? + + read(size).force_encoding(Encoding::UTF_8) + end + + {% when Type::Timestamp -%} + # The Timestamp type. + ONE_SECOND_IN_NANOSECONDS = 10**9 + + def read{{ canonical_type_name }} + seconds = unpack_from 8, 'q>' + nanoseconds = unpack_from 4, 'L>' + + # UniFFi conventions assume that nanoseconds part has to represent nanoseconds portion of + # duration between epoch and the timestamp moment. Ruby `Time#tv_nsec` returns the number of + # nanoseconds for the subsecond part, which is sort of opposite to "duration" meaning. + # Hence we need to convert value returned by `Time#tv_nsec` back and forth with the following + # logic: + if seconds < 0 && nanoseconds != 0 + # In order to get duration nsec we shift by 1 second: + nanoseconds = ONE_SECOND_IN_NANOSECONDS - nanoseconds + + # Then we compensate 1 second shift: + seconds -= 1 + end + + Time.at(seconds, nanoseconds, :nanosecond, in: '+00:00').utc + end + + {% when Type::Duration -%} + # The Duration type. + + def read{{ canonical_type_name }} + seconds = unpack_from 8, 'q>' + nanoseconds = unpack_from 4, 'L>' + + Time.at(seconds, nanoseconds, :nanosecond, in: '+00:00').utc + end + + {% when Type::Object with (object_name) -%} + # The Object type {{ object_name }}. + + def read{{ canonical_type_name }} + pointer = FFI::Pointer.new unpack_from 8, 'Q>' + return {{ object_name|class_name_rb }}._uniffi_allocate(pointer) + end + + {% when Type::Enum with (enum_name) -%} + {%- let e = ci.get_enum_definition(enum_name).unwrap() -%} + # The Enum type {{ enum_name }}. + + def read{{ canonical_type_name }} + variant = unpack_from 4, 'l>' + {% if e.is_flat() -%} + {%- for variant in e.variants() %} + if variant == {{ loop.index }} + return {{ enum_name|class_name_rb }}::{{ variant.name()|enum_name_rb }} + end + {%- endfor %} + + raise InternalError, 'Unexpected variant tag for {{ canonical_type_name }}' + {%- else -%} + {%- for variant in e.variants() %} + if variant == {{ loop.index }} + {%- if variant.has_fields() %} + return {{ enum_name|class_name_rb }}::{{ variant.name()|enum_name_rb }}.new( + {%- for field in variant.fields() %} + self.read{{ field.type_().canonical_name().borrow()|class_name_rb }}(){% if loop.last %}{% else %},{% endif %} + {%- endfor %} + ) + {%- else %} + return {{ enum_name|class_name_rb }}::{{ variant.name()|enum_name_rb }}.new + {% endif %} + end + {%- endfor %} + raise InternalError, 'Unexpected variant tag for {{ canonical_type_name }}' + {%- endif %} + end + + {% when Type::Error with (error_name) -%} + {%- let e = ci.get_error_definition(error_name).unwrap().wrapped_enum() %} + + # The Error type {{ error_name }} + + def read{{ canonical_type_name }} + variant = unpack_from 4, 'l>' + {% if e.is_flat() -%} + {%- for variant in e.variants() %} + if variant == {{ loop.index }} + return {{ error_name|class_name_rb }}::{{ variant.name()|class_name_rb }}.new( + readString() + ) + end + {%- endfor %} + + raise InternalError, 'Unexpected variant tag for {{ canonical_type_name }}' + {%- else -%} + {%- for variant in e.variants() %} + if variant == {{ loop.index }} + {%- if variant.has_fields() %} + return {{ error_name|class_name_rb }}::{{ variant.name()|class_name_rb }}.new( + {%- for field in variant.fields() %} + read{{ field.type_().canonical_name().borrow()|class_name_rb }}(){% if loop.last %}{% else %},{% endif %} + {%- endfor %} + ) + {%- else %} + return {{ error_name|class_name_rb }}::{{ variant.name()|class_name_rb }}.new + {%- endif %} + end + {%- endfor %} + + raise InternalError, 'Unexpected variant tag for {{ canonical_type_name }}' + {%- endif %} + end + + {% when Type::Record with (record_name) -%} + {%- let rec = ci.get_record_definition(record_name).unwrap() -%} + # The Record type {{ record_name }}. + + def read{{ canonical_type_name }} + {{ rec.name()|class_name_rb }}.new( + {%- for field in rec.fields() %} + read{{ field.type_().canonical_name().borrow()|class_name_rb }}{% if loop.last %}{% else %},{% endif %} + {%- endfor %} + ) + end + + {% when Type::Optional with (inner_type) -%} + # The Optional<T> type for {{ inner_type.canonical_name() }}. + + def read{{ canonical_type_name }} + flag = unpack_from 1, 'c' + + if flag == 0 + return nil + elsif flag == 1 + return read{{ inner_type.canonical_name().borrow()|class_name_rb }} + else + raise InternalError, 'Unexpected flag byte for {{ canonical_type_name }}' + end + end + + {% when Type::Sequence with (inner_type) -%} + # The Sequence<T> type for {{ inner_type.canonical_name() }}. + + def read{{ canonical_type_name }} + count = unpack_from 4, 'l>' + + raise InternalError, 'Unexpected negative sequence length' if count.negative? + + items = [] + + count.times do + items.append read{{ inner_type.canonical_name().borrow()|class_name_rb }} + end + + items + end + + {% when Type::Map with (k, inner_type) -%} + # The Map<T> type for {{ inner_type.canonical_name() }}. + + def read{{ canonical_type_name }} + count = unpack_from 4, 'l>' + raise InternalError, 'Unexpected negative map size' if count.negative? + + items = {} + count.times do + key = readString + items[key] = read{{ inner_type.canonical_name().borrow()|class_name_rb }} + end + + items + end + {%- else -%} + # This type is not yet supported in the Ruby backend. + def read{{ canonical_type_name }} + raise InternalError, 'RustBufferStream.read not implemented yet for {{ canonical_type_name }}' + end + + {%- endmatch -%} + {%- endfor %} + + def unpack_from(size, format) + raise InternalError, 'read past end of rust buffer' if @offset + size > @rbuf.len + + value = @rbuf.data.get_bytes(@offset, size).unpack format + + @offset += size + + # TODO: verify this + raise 'more than one element!!!' if value.size > 1 + + value[0] + end +end + +private_constant :RustBufferStream diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferTemplate.rb new file mode 100644 index 0000000000..deed864572 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferTemplate.rb @@ -0,0 +1,218 @@ +class RustBuffer < FFI::Struct + layout :capacity, :int32, + :len, :int32, + :data, :pointer + + def self.alloc(size) + return {{ ci.namespace()|class_name_rb }}.rust_call(:{{ ci.ffi_rustbuffer_alloc().name() }}, size) + end + + def self.reserve(rbuf, additional) + return {{ ci.namespace()|class_name_rb }}.rust_call(:{{ ci.ffi_rustbuffer_reserve().name() }}, rbuf, additional) + end + + def free + {{ ci.namespace()|class_name_rb }}.rust_call(:{{ ci.ffi_rustbuffer_free().name() }}, self) + end + + def capacity + self[:capacity] + end + + def len + self[:len] + end + + def len=(value) + self[:len] = value + end + + def data + self[:data] + end + + def to_s + "RustBuffer(capacity=#{capacity}, len=#{len}, data=#{data.read_bytes len})" + end + + # The allocated buffer will be automatically freed if an error occurs, ensuring that + # we don't accidentally leak it. + def self.allocWithBuilder + builder = RustBufferBuilder.new + + begin + yield builder + rescue => e + builder.discard + raise e + end + end + + # The RustBuffer will be freed once the context-manager exits, ensuring that we don't + # leak it even if an error occurs. + def consumeWithStream + stream = RustBufferStream.new self + + yield stream + + raise RuntimeError, 'junk data left in buffer after consuming' if stream.remaining != 0 + ensure + free + end + + {%- for typ in ci.iter_types() -%} + {%- let canonical_type_name = typ.canonical_name() -%} + {%- match typ -%} + + {% when Type::String -%} + # The primitive String type. + + def self.allocFromString(value) + RustBuffer.allocWithBuilder do |builder| + builder.write value.encode('utf-8') + return builder.finalize + end + end + + def consumeIntoString + consumeWithStream do |stream| + return stream.read(stream.remaining).force_encoding(Encoding::UTF_8) + end + end + + {% when Type::Timestamp -%} + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {% when Type::Duration -%} + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {% when Type::Record with (record_name) -%} + {%- let rec = ci.get_record_definition(record_name).unwrap() -%} + # The Record type {{ record_name }}. + + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {% when Type::Enum with (enum_name) -%} + {%- let e = ci.get_enum_definition(enum_name).unwrap() -%} + # The Enum type {{ enum_name }}. + + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {% when Type::Optional with (inner_type) -%} + # The Optional<T> type for {{ inner_type.canonical_name() }}. + + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize() + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {% when Type::Sequence with (inner_type) -%} + # The Sequence<T> type for {{ inner_type.canonical_name() }}. + + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize() + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {% when Type::Map with (k, inner_type) -%} + # The Map<T> type for {{ inner_type.canonical_name() }}. + + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {%- else -%} + {#- No code emitted for types that don't lower into a RustBuffer -#} + {%- endmatch -%} + {%- endfor %} +end + +module UniFFILib + class ForeignBytes < FFI::Struct + layout :len, :int32, + :data, :pointer + + def len + self[:len] + end + + def data + self[:data] + end + + def to_s + "ForeignBytes(len=#{len}, data=#{data.read_bytes(len)})" + end + end +end + +private_constant :UniFFILib diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/TopLevelFunctionTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/TopLevelFunctionTemplate.rb new file mode 100644 index 0000000000..13214cf31b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/TopLevelFunctionTemplate.rb @@ -0,0 +1,16 @@ +{%- match func.return_type() -%} +{%- when Some with (return_type) %} + +def self.{{ func.name()|fn_name_rb }}({%- call rb::arg_list_decl(func) -%}) + {%- call rb::coerce_args(func) %} + result = {% call rb::to_ffi_call(func) %} + return {{ "result"|lift_rb(return_type) }} +end + +{% when None %} + +def self.{{ func.name()|fn_name_rb }}({%- call rb::arg_list_decl(func) -%}) + {%- call rb::coerce_args(func) %} + {% call rb::to_ffi_call(func) %} +end +{% endmatch %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/macros.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/macros.rb new file mode 100644 index 0000000000..c2faf63104 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/macros.rb @@ -0,0 +1,73 @@ +{# +// 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` (we use `var_name_rb` in `lower_rb`) +#} + +{%- macro to_ffi_call(func) -%} + {%- match func.throws_name() -%} + {%- when Some with (e) -%} + {{ ci.namespace()|class_name_rb }}.rust_call_with_error({{ e|class_name_rb }}, + {%- else -%} + {{ ci.namespace()|class_name_rb }}.rust_call( + {%- endmatch -%} + :{{ func.ffi_func().name() }}, + {%- call _arg_list_ffi_call(func) -%} +) +{%- endmacro -%} + +{%- macro to_ffi_call_with_prefix(prefix, func) -%} + {%- match func.throws_name() -%} + {%- when Some with (e) -%} + {{ ci.namespace()|class_name_rb }}.rust_call_with_error({{ e|class_name_rb }}, + {%- else -%} + {{ ci.namespace()|class_name_rb }}.rust_call( + {%- endmatch -%} + :{{ func.ffi_func().name() }}, + {{- prefix }}, + {%- call _arg_list_ffi_call(func) -%} +) +{%- endmacro -%} + +{%- macro _arg_list_ffi_call(func) %} + {%- for arg in func.arguments() %} + {{- arg.name()|lower_rb(arg.type_().borrow()) }} + {%- if !loop.last %},{% endif %} + {%- endfor %} +{%- endmacro -%} + +{#- +// Arglist as used in Ruby declarations of methods, functions and constructors. +// Note the var_name_rb and type_rb filters. +-#} + +{% macro arg_list_decl(func) %} + {%- for arg in func.arguments() -%} + {{ arg.name()|var_name_rb }} + {%- match arg.default_value() %} + {%- when Some with(literal) %} = {{ literal|literal_rb }} + {%- else %} + {%- endmatch %} + {%- if !loop.last %}, {% endif -%} + {%- endfor %} +{%- endmacro %} + +{#- +// Arglist as used in the UniFFILib function declarations. +// Note unfiltered name but type_ffi filters. +-#} +{%- macro arg_list_ffi_decl(func) %} + [{%- for arg in func.arguments() -%}{{ arg.type_().borrow()|type_ffi }}, {% endfor -%} RustCallStatus.by_ref] +{%- endmacro -%} + +{%- macro coerce_args(func) %} + {%- for arg in func.arguments() %} + {{ arg.name() }} = {{ arg.name()|coerce_rb(arg.type_().borrow()) -}} + {% endfor -%} +{%- endmacro -%} + +{%- macro coerce_args_extra_indent(func) %} + {%- for arg in func.arguments() %} + {{ arg.name() }} = {{ arg.name()|coerce_rb(arg.type_().borrow()) }} + {%- endfor %} +{%- endmacro -%} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/wrapper.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/wrapper.rb new file mode 100644 index 0000000000..72cb60f73f --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/wrapper.rb @@ -0,0 +1,47 @@ +# This file was autogenerated by some hot garbage in the `uniffi` crate. +# Trust me, you don't want to mess with it! + +# Common helper code. +# +# Ideally this would live in a separate .rb file where it can be unittested etc +# in isolation, and perhaps even published as a re-useable package. +# +# However, it's important that the detils 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 Ruby +# helpers directly inline like we're doing here. + +require 'ffi' + +module {{ ci.namespace()|class_name_rb }} + {% include "RustBufferTemplate.rb" %} + {% include "RustBufferStream.rb" %} + {% include "RustBufferBuilder.rb" %} + + # Error definitions + {% include "ErrorTemplate.rb" %} + + {% include "NamespaceLibraryTemplate.rb" %} + + # Public interface members begin here. + + {% for e in ci.enum_definitions() %} + {% include "EnumTemplate.rb" %} + {%- endfor -%} + + {%- for rec in ci.record_definitions() %} + {% include "RecordTemplate.rb" %} + {% endfor %} + + {% for func in ci.function_definitions() %} + {% include "TopLevelFunctionTemplate.rb" %} + {% endfor %} + + {% for obj in ci.object_definitions() %} + {% include "ObjectTemplate.rb" %} + {% endfor %} +end + +{% import "macros.rb" as rb %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/test.rs b/third_party/rust/uniffi_bindgen/src/bindings/ruby/test.rs new file mode 100644 index 0000000000..5c445bfae5 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/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::{bail, Context, Result}; +use camino::Utf8Path; +use std::env; +use std::ffi::OsString; +use std::process::{Command, Stdio}; +use uniffi_testing::UniFFITestHelper; + +/// Run Ruby 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 rubypath = env::var_os("RUBYLIB").unwrap_or_else(|| OsString::from("")); + let rubypath = env::join_paths( + env::split_paths(&rubypath).chain(vec![out_dir.to_path_buf().into_std_path_buf()]), + )?; + + let status = Command::new("ruby") + .current_dir(out_dir) + .env("RUBYLIB", rubypath) + .arg(script_path) + .stderr(Stdio::inherit()) + .stdout(Stdio::inherit()) + .spawn() + .context("Failed to spawn `ruby` when running script")? + .wait() + .context("Failed to wait for `ruby` when running script")?; + if !status.success() { + bail!("running `ruby` 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!["ruby"], + Some(out_dir), + Some(library_path), + false, + )?; + } + Ok(()) +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/callback_interface.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/callback_interface.rs new file mode 100644 index 0000000000..828a8823ee --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/callback_interface.rs @@ -0,0 +1,25 @@ +/* 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 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.type_label(oracle)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/compounds.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/compounds.rs new file mode 100644 index 0000000000..f91be9ce54 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/compounds.rs @@ -0,0 +1,98 @@ +/* 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 { + format!("{}?", oracle.find(&self.inner).type_label(oracle)) + } + + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + format!("Option{}", oracle.find(&self.inner).canonical_name(oracle)) + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + match literal { + Literal::Null => "nil".into(), + _ => oracle.find(&self.inner).literal(oracle, literal), + } + } +} + +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 { + format!("[{}]", oracle.find(&self.inner).type_label(oracle)) + } + + 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(), + _ => unreachable!(), + } + } +} + +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 { + format!( + "[{}: {}]", + oracle.find(&self.key).type_label(oracle), + oracle.find(&self.value).type_label(oracle) + ) + } + + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + format!( + "Dictionary{}{}", + 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(), + _ => unreachable!(), + } + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/custom.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/custom.rs new file mode 100644 index 0000000000..bf1806e5e2 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/custom.rs @@ -0,0 +1,25 @@ +/* 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 { + CustomCodeType { 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) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/enum_.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/enum_.rs new file mode 100644 index 0000000000..04d8422f6a --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/enum_.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 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!(".{}", oracle.enum_variant_name(v)) + } else { + unreachable!(); + } + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/error.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/error.rs new file mode 100644 index 0000000000..4219f24c39 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/error.rs @@ -0,0 +1,25 @@ +/* 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 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) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/external.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/external.rs new file mode 100644 index 0000000000..c9a925cdaa --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/external.rs @@ -0,0 +1,35 @@ +/* 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 { + ExternalCodeType { 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) + } + + // lower and lift need to call public function which were generated for + // the original types. + fn lower(&self, oracle: &dyn CodeOracle) -> String { + format!("{}_lower", self.ffi_converter_name(oracle)) + } + + fn lift(&self, oracle: &dyn CodeOracle) -> String { + format!("{}_lift", self.ffi_converter_name(oracle)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/miscellany.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/miscellany.rs new file mode 100644 index 0000000000..4f57734934 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/miscellany.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 TimestampCodeType; + +impl CodeType for TimestampCodeType { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + "Date".into() + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + "Timestamp".into() + } +} + +pub struct DurationCodeType; + +impl CodeType for DurationCodeType { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + "TimeInterval".into() + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + "Duration".into() + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs new file mode 100644 index 0000000000..abbe0b3539 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs @@ -0,0 +1,476 @@ +/* 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::borrow::Borrow; +use std::cell::RefCell; +use std::collections::{BTreeSet, HashMap, HashSet}; + +use anyhow::{Context, Result}; +use askama::Template; +use heck::{ToLowerCamelCase, ToUpperCamelCase}; +use serde::{Deserialize, Serialize}; + +use super::Bindings; +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; + +/// Config options for the caller to customize the generated Swift. +/// +/// Note that this can only be used to control details of the Swift *that do not affect the underlying component*, +/// since the details of the underlying component are entirely determined by the `ComponentInterface`. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Config { + cdylib_name: Option<String>, + module_name: Option<String>, + ffi_module_name: Option<String>, + ffi_module_filename: Option<String>, + generate_module_map: Option<bool>, + omit_argument_labels: Option<bool>, + #[serde(default)] + custom_types: HashMap<String, CustomTypeConfig>, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct CustomTypeConfig { + imports: Option<Vec<String>>, + type_name: Option<String>, + into_custom: TemplateExpression, + from_custom: TemplateExpression, +} + +impl Config { + /// The name of the Swift module containing the high-level foreign-language bindings. + pub fn module_name(&self) -> String { + match self.module_name.as_ref() { + Some(name) => name.clone(), + None => "uniffi".into(), + } + } + + /// The name of the lower-level C module containing the FFI declarations. + pub fn ffi_module_name(&self) -> String { + match self.ffi_module_name.as_ref() { + Some(name) => name.clone(), + None => format!("{}FFI", self.module_name()), + } + } + + /// The filename stem for the lower-level C module containing the FFI declarations. + pub fn ffi_module_filename(&self) -> String { + match self.ffi_module_filename.as_ref() { + Some(name) => name.clone(), + None => self.ffi_module_name(), + } + } + + /// The name of the `.modulemap` file for the lower-level C module with FFI declarations. + pub fn modulemap_filename(&self) -> String { + format!("{}.modulemap", self.ffi_module_filename()) + } + + /// The name of the `.h` file for the lower-level C module with FFI declarations. + pub fn header_filename(&self) -> String { + format!("{}.h", self.ffi_module_filename()) + } + + /// The name of the compiled Rust library containing the FFI implementation. + pub fn cdylib_name(&self) -> String { + if let Some(cdylib_name) = &self.cdylib_name { + cdylib_name.clone() + } else { + "uniffi".into() + } + } + + /// Whether to generate a `.modulemap` file for the lower-level C module with FFI declarations. + pub fn generate_module_map(&self) -> bool { + self.generate_module_map.unwrap_or(true) + } + + /// Whether to omit argument labels in Swift function definitions. + pub fn omit_argument_labels(&self) -> bool { + self.omit_argument_labels.unwrap_or(false) + } +} + +impl From<&ComponentInterface> for Config { + fn from(ci: &ComponentInterface) -> Self { + Config { + module_name: Some(ci.namespace().into()), + cdylib_name: Some(format!("uniffi_{}", ci.namespace())), + ..Default::default() + } + } +} + +impl MergeWith for Config { + fn merge_with(&self, other: &Self) -> Self { + Config { + module_name: self.module_name.merge_with(&other.module_name), + ffi_module_name: self.ffi_module_name.merge_with(&other.ffi_module_name), + cdylib_name: self.cdylib_name.merge_with(&other.cdylib_name), + ffi_module_filename: self + .ffi_module_filename + .merge_with(&other.ffi_module_filename), + generate_module_map: self + .generate_module_map + .merge_with(&other.generate_module_map), + omit_argument_labels: self + .omit_argument_labels + .merge_with(&other.omit_argument_labels), + custom_types: self.custom_types.merge_with(&other.custom_types), + } + } +} + +/// Generate UniFFI component bindings for Swift, as strings in memory. +/// +pub fn generate_bindings(config: &Config, ci: &ComponentInterface) -> Result<Bindings> { + let header = BridgingHeader::new(config, ci) + .render() + .context("failed to render Swift bridging header")?; + let library = SwiftWrapper::new(config.clone(), ci) + .render() + .context("failed to render Swift library")?; + let modulemap = if config.generate_module_map() { + Some( + ModuleMap::new(config, ci) + .render() + .context("failed to render Swift modulemap")?, + ) + } else { + None + }; + Ok(Bindings { + library, + header, + modulemap, + }) +} + +/// Renders Swift 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 = "kt", escape = "none", path = "Types.swift")] +pub struct TypeRenderer<'a> { + 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<String>>, +} + +impl<'a> TypeRenderer<'a> { + fn new(config: &'a Config, ci: &'a ComponentInterface) -> Self { + Self { + config, + ci, + include_once_names: RefCell::new(HashSet::new()), + imports: RefCell::new(BTreeSet::new()), + } + } + + // The following methods are used by the `Types.kt` 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(name.to_owned()); + "" + } +} + +/// Template for generating the `.h` file that defines the low-level C FFI. +/// +/// This file defines only the low-level structs and functions that are exposed +/// by the compiled Rust code. It gets wrapped into a higher-level API by the +/// code from [`SwiftWrapper`]. +#[derive(Template)] +#[template(syntax = "c", escape = "none", path = "BridgingHeaderTemplate.h")] +pub struct BridgingHeader<'config, 'ci> { + _config: &'config Config, + ci: &'ci ComponentInterface, +} + +impl<'config, 'ci> BridgingHeader<'config, 'ci> { + pub fn new(config: &'config Config, ci: &'ci ComponentInterface) -> Self { + Self { + _config: config, + ci, + } + } +} + +/// Template for generating the `.modulemap` file that exposes the low-level C FFI. +/// +/// This file defines how the low-level C FFI from [`BridgingHeader`] gets exposed +/// as a Swift module that can be called by other Swift code. In our case, its only +/// job is to define the *name* of the Swift module that will contain the FFI functions +/// so that it can be imported by the higher-level code in from [`SwiftWrapper`]. +#[derive(Template)] +#[template(syntax = "c", escape = "none", path = "ModuleMapTemplate.modulemap")] +pub struct ModuleMap<'config, 'ci> { + config: &'config Config, + _ci: &'ci ComponentInterface, +} + +impl<'config, 'ci> ModuleMap<'config, 'ci> { + pub fn new(config: &'config Config, _ci: &'ci ComponentInterface) -> Self { + Self { config, _ci } + } +} + +#[derive(Template)] +#[template(syntax = "swift", escape = "none", path = "wrapper.swift")] +pub struct SwiftWrapper<'a> { + config: Config, + ci: &'a ComponentInterface, + type_helper_code: String, + type_imports: BTreeSet<String>, +} +impl<'a> SwiftWrapper<'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<String> { + self.type_imports.iter().cloned().collect() + } + + pub fn initialization_fns(&self) -> Vec<String> { + self.ci + .iter_types() + .into_iter() + .filter_map(|t| t.initialization_fn(&SwiftCodeOracle)) + .collect() + } +} + +#[derive(Clone)] +pub struct SwiftCodeOracle; + +impl SwiftCodeOracle { + // Map `Type` instances to a `Box<dyn CodeType>` for that type. + // + // There is a companion match in `templates/Types.swift` 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.swift` 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 SwiftCodeOracle { + fn find(&self, type_: &TypeIdentifier) -> Box<dyn CodeType> { + self.create_code_type(type_.clone()) + } + + /// Get the idiomatic Swift rendering of a class name (for enums, records, errors, etc). + fn class_name(&self, nm: &str) -> String { + nm.to_string().to_upper_camel_case() + } + + /// Get the idiomatic Swift rendering of a function name. + fn fn_name(&self, nm: &str) -> String { + format!("`{}`", nm.to_string().to_lower_camel_case()) + } + + /// Get the idiomatic Swift rendering of a variable name. + fn var_name(&self, nm: &str) -> String { + format!("`{}`", nm.to_string().to_lower_camel_case()) + } + + /// Get the idiomatic Swift rendering of an individual enum variant. + fn enum_variant_name(&self, nm: &str) -> String { + format!("`{}`", nm.to_string().to_lower_camel_case()) + } + + /// Get the idiomatic Swift rendering of an exception name. + fn error_name(&self, nm: &str) -> String { + format!("`{}`", self.class_name(nm)) + } + + fn ffi_type_label(&self, ffi_type: &FfiType) -> String { + match ffi_type { + FfiType::Int8 => "int8_t".into(), + FfiType::UInt8 => "uint8_t".into(), + FfiType::Int16 => "int16_t".into(), + FfiType::UInt16 => "uint16_t".into(), + FfiType::Int32 => "int32_t".into(), + FfiType::UInt32 => "uint32_t".into(), + FfiType::Int64 => "int64_t".into(), + FfiType::UInt64 => "uint64_t".into(), + FfiType::Float32 => "float".into(), + FfiType::Float64 => "double".into(), + FfiType::RustArcPtr(_) => "void*_Nonnull".into(), + FfiType::RustBuffer(_) => "RustBuffer".into(), + FfiType::ForeignBytes => "ForeignBytes".into(), + FfiType::ForeignCallback => "ForeignCallback _Nonnull".to_string(), + } + } +} + +pub mod filters { + use super::*; + + fn oracle() -> &'static SwiftCodeOracle { + &SwiftCodeOracle + } + + pub fn type_name(codetype: &impl CodeType) -> Result<String, askama::Error> { + let oracle = oracle(); + Ok(codetype.type_label(oracle)) + } + + pub fn canonical_name(codetype: &impl CodeType) -> Result<String, askama::Error> { + let oracle = oracle(); + Ok(codetype.canonical_name(oracle)) + } + + pub fn ffi_converter_name(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(codetype.ffi_converter_name(oracle())) + } + + pub fn lower_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(codetype.lower(oracle())) + } + + pub fn write_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(codetype.write(oracle())) + } + + pub fn lift_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(codetype.lift(oracle())) + } + + pub fn read_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(codetype.read(oracle())) + } + + pub fn literal_swift( + literal: &Literal, + codetype: &impl CodeType, + ) -> Result<String, askama::Error> { + let oracle = oracle(); + Ok(codetype.literal(oracle, literal)) + } + + /// Get the Swift 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 type that a type is lowered into. This is subtly different than `type_ffi`, see + /// #1106 for details + pub fn type_ffi_lowered(ffi_type: &FfiType) -> Result<String, askama::Error> { + Ok(match ffi_type { + FfiType::Int8 => "Int8".into(), + FfiType::UInt8 => "UInt8".into(), + FfiType::Int16 => "Int16".into(), + FfiType::UInt16 => "UInt16".into(), + FfiType::Int32 => "Int32".into(), + FfiType::UInt32 => "UInt32".into(), + FfiType::Int64 => "Int64".into(), + FfiType::UInt64 => "UInt64".into(), + FfiType::Float32 => "float".into(), + FfiType::Float64 => "double".into(), + FfiType::RustArcPtr(_) => "void*_Nonnull".into(), + FfiType::RustBuffer(_) => "RustBuffer".into(), + FfiType::ForeignBytes => "ForeignBytes".into(), + FfiType::ForeignCallback => "ForeignCallback _Nonnull".to_string(), + }) + } + + /// Get the idiomatic Swift 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 Swift rendering of a function name. + pub fn fn_name(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().fn_name(nm)) + } + + /// Get the idiomatic Swift rendering of a variable name. + pub fn var_name(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().var_name(nm)) + } + + /// Get the idiomatic Swift rendering of an individual enum variant. + pub fn enum_variant_swift(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().enum_variant_name(nm)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/object.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/object.rs new file mode 100644 index 0000000000..d227f8e7cd --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/object.rs @@ -0,0 +1,25 @@ +/* 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 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) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/primitives.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/primitives.rs new file mode 100644 index 0000000000..9989e0a421 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/primitives.rs @@ -0,0 +1,87 @@ +/* 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::{types::Type, Radix}; +use paste::paste; + +fn render_literal(oracle: &dyn CodeOracle, literal: &Literal) -> String { + fn typed_number(oracle: &dyn CodeOracle, type_: &Type, num_str: String) -> String { + match type_ { + // special case Int32. + Type::Int32 => num_str, + // otherwise use constructor e.g. UInt8(x) + Type::Int8 + | Type::UInt8 + | Type::Int16 + | Type::UInt16 + | Type::UInt32 + | Type::Int64 + | Type::UInt64 + | Type::Float32 + | Type::Float64 => + // XXX we should pass in the codetype itself. + { + format!("{}({num_str})", oracle.find(type_).type_label(oracle)) + } + _ => panic!("Unexpected literal: {num_str} is not a number"), + } + } + + match literal { + Literal::Boolean(v) => format!("{v}"), + Literal::String(s) => format!("\"{s}\""), + Literal::Int(i, radix, type_) => typed_number( + oracle, + type_, + match radix { + Radix::Octal => format!("0o{i:o}"), + Radix::Decimal => format!("{i}"), + Radix::Hexadecimal => format!("{i:#x}"), + }, + ), + Literal::UInt(i, radix, type_) => typed_number( + oracle, + type_, + match radix { + Radix::Octal => format!("0o{i:o}"), + Radix::Decimal => format!("{i}"), + Radix::Hexadecimal => format!("{i:#x}"), + }, + ), + Literal::Float(string, type_) => typed_number(oracle, type_, string.clone()), + _ => unreachable!("Literal"), + } +} + +macro_rules! impl_code_type_for_primitive { + ($T:ty, $class_name:literal) => { + 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) + } + } + } + }; +} + +impl_code_type_for_primitive!(BooleanCodeType, "Bool"); +impl_code_type_for_primitive!(StringCodeType, "String"); +impl_code_type_for_primitive!(Int8CodeType, "Int8"); +impl_code_type_for_primitive!(Int16CodeType, "Int16"); +impl_code_type_for_primitive!(Int32CodeType, "Int32"); +impl_code_type_for_primitive!(Int64CodeType, "Int64"); +impl_code_type_for_primitive!(UInt8CodeType, "UInt8"); +impl_code_type_for_primitive!(UInt16CodeType, "UInt16"); +impl_code_type_for_primitive!(UInt32CodeType, "UInt32"); +impl_code_type_for_primitive!(UInt64CodeType, "UInt64"); +impl_code_type_for_primitive!(Float32CodeType, "Float"); +impl_code_type_for_primitive!(Float64CodeType, "Double"); diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/record.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/record.rs new file mode 100644 index 0000000000..0943bcaada --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/record.rs @@ -0,0 +1,25 @@ +/* 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 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) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/mod.rs new file mode 100644 index 0000000000..5fd1c404cb --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/mod.rs @@ -0,0 +1,99 @@ +/* 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/. */ + +//! # Swift bindings backend for UniFFI +//! +//! This module generates Swift bindings from a [`ComponentInterface`] definition, +//! using Swift's builtin support for loading C header files. +//! +//! Conceptually, the generated bindings are split into two Swift modules, one for the low-level +//! C FFI layer and one for the higher-level Swift bindings. For a UniFFI component named "example" +//! we generate: +//! +//! * A C header file `exampleFFI.h` declaring the low-level structs and functions for calling +//! into Rust, along with a corresponding `exampleFFI.modulemap` to expose them to Swift. +//! +//! * A Swift source file `example.swift` that imports the `exampleFFI` module and wraps it +//! to provide the higher-level Swift API. +//! +//! Most of the concepts in a [`ComponentInterface`] have an obvious counterpart in Swift, +//! with the details documented in inline comments where appropriate. +//! +//! To handle lifting/lowering/serializing types across the FFI boundary, the Swift code +//! defines a `protocol ViaFfi` that is analogous to the `uniffi::ViaFfi` Rust trait. +//! Each type that can traverse the FFI conforms to the `ViaFfi` protocol, which specifies: +//! +//! * The corresponding low-level type. +//! * How to lift from and lower into into that type. +//! * How to read from and write into a byte buffer. +//! + +use std::{io::Write, process::Command}; + +use anyhow::Result; +use camino::Utf8Path; +use fs_err::File; + +pub mod gen_swift; +pub use gen_swift::{generate_bindings, Config}; +mod test; + +use super::super::interface::ComponentInterface; +pub use test::run_test; + +/// The Swift bindings generated from a [`ComponentInterface`]. +/// +pub struct Bindings { + /// The contents of the generated `.swift` file, as a string. + library: String, + /// The contents of the generated `.h` file, as a string. + header: String, + /// The contents of the generated `.modulemap` file, as a string. + modulemap: Option<String>, +} + +/// Write UniFFI component bindings for Swift as files on disk. +/// +/// Unlike other target languages, binding to Rust code from Swift involves more than just +/// generating a `.swift` file. We also need to produce a `.h` file with the C-level API +/// declarations, and a `.modulemap` file to tell Swift how to use it. +pub fn write_bindings( + config: &Config, + ci: &ComponentInterface, + out_dir: &Utf8Path, + try_format_code: bool, +) -> Result<()> { + let Bindings { + header, + library, + modulemap, + } = generate_bindings(config, ci)?; + + let source_file = out_dir.join(format!("{}.swift", config.module_name())); + let mut l = File::create(&source_file)?; + write!(l, "{library}")?; + + let mut h = File::create(out_dir.join(config.header_filename()))?; + write!(h, "{header}")?; + + if let Some(modulemap) = modulemap { + let mut m = File::create(out_dir.join(config.modulemap_filename()))?; + write!(m, "{modulemap}")?; + } + + if try_format_code { + if let Err(e) = Command::new("swiftformat") + .arg(source_file.as_str()) + .output() + { + println!( + "Warning: Unable to auto-format {} using swiftformat: {:?}", + source_file.file_name().unwrap(), + e + ) + } + } + + Ok(()) +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/BooleanHelper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/BooleanHelper.swift new file mode 100644 index 0000000000..465e519628 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/BooleanHelper.swift @@ -0,0 +1,20 @@ +fileprivate struct FfiConverterBool : FfiConverter { + typealias FfiType = Int8 + typealias SwiftType = Bool + + public static func lift(_ value: Int8) throws -> Bool { + return value != 0 + } + + public static func lower(_ value: Bool) -> Int8 { + return value ? 1 : 0 + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bool { + return try lift(readInt(&buf)) + } + + public static func write(_ value: Bool, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h new file mode 100644 index 0000000000..50b95d96e8 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h @@ -0,0 +1,55 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +#pragma once + +#include <stdbool.h> +#include <stdint.h> + +// The following structs are used to implement the lowest level +// of the FFI, and thus useful to multiple uniffied crates. +// We ensure they are declared exactly once, with a header guard, UNIFFI_SHARED_H. +#ifdef UNIFFI_SHARED_H + // We also try to prevent mixing versions of shared uniffi header structs. + // If you add anything to the #else block, you must increment the version suffix in UNIFFI_SHARED_HEADER_V4 + #ifndef UNIFFI_SHARED_HEADER_V4 + #error Combining helper code from multiple versions of uniffi is not supported + #endif // ndef UNIFFI_SHARED_HEADER_V4 +#else +#define UNIFFI_SHARED_H +#define UNIFFI_SHARED_HEADER_V4 +// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️ + +typedef struct RustBuffer +{ + int32_t capacity; + int32_t len; + uint8_t *_Nullable data; +} RustBuffer; + +typedef int32_t (*ForeignCallback)(uint64_t, int32_t, RustBuffer, RustBuffer *_Nonnull); + +typedef struct ForeignBytes +{ + int32_t len; + const uint8_t *_Nullable data; +} ForeignBytes; + +// Error definitions +typedef struct RustCallStatus { + int8_t code; + RustBuffer errorBuf; +} RustCallStatus; + +// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️ +#endif // def UNIFFI_SHARED_H + +{% for func in ci.iter_ffi_function_definitions() -%} + {%- match func.return_type() -%}{%- when Some with (type_) %}{{ type_|ffi_type_name }}{% when None %}void{% endmatch %} {{ func.name() }}( + {% call swift::arg_list_ffi_decl(func) %} + ); +{% endfor -%} + +{% import "macros.swift" as swift %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceRuntime.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceRuntime.swift new file mode 100644 index 0000000000..9ab9bdbe48 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceRuntime.swift @@ -0,0 +1,60 @@ +fileprivate extension NSLock { + func withLock<T>(f: () throws -> T) rethrows -> T { + self.lock() + defer { self.unlock() } + return try f() + } +} + +fileprivate typealias UniFFICallbackHandle = UInt64 +fileprivate class UniFFICallbackHandleMap<T> { + private var leftMap: [UniFFICallbackHandle: T] = [:] + private var counter: [UniFFICallbackHandle: UInt64] = [:] + private var rightMap: [ObjectIdentifier: UniFFICallbackHandle] = [:] + + private let lock = NSLock() + private var currentHandle: UniFFICallbackHandle = 0 + private let stride: UniFFICallbackHandle = 1 + + func insert(obj: T) -> UniFFICallbackHandle { + lock.withLock { + let id = ObjectIdentifier(obj as AnyObject) + let handle = rightMap[id] ?? { + currentHandle += stride + let handle = currentHandle + leftMap[handle] = obj + rightMap[id] = handle + return handle + }() + counter[handle] = (counter[handle] ?? 0) + 1 + return handle + } + } + + func get(handle: UniFFICallbackHandle) -> T? { + lock.withLock { + leftMap[handle] + } + } + + func delete(handle: UniFFICallbackHandle) { + remove(handle: handle) + } + + @discardableResult + func remove(handle: UniFFICallbackHandle) -> T? { + lock.withLock { + defer { counter[handle] = (counter[handle] ?? 1) - 1 } + guard counter[handle] == 1 else { return leftMap[handle] } + let obj = leftMap.removeValue(forKey: handle) + if let obj = obj { + rightMap.removeValue(forKey: ObjectIdentifier(obj as AnyObject)) + } + 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. +private let IDX_CALLBACK_FREE: Int32 = 0 diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceTemplate.swift new file mode 100644 index 0000000000..7842511a83 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceTemplate.swift @@ -0,0 +1,154 @@ +{%- let cbi = ci.get_callback_interface_definition(name).unwrap() %} +{%- let foreign_callback = format!("foreignCallback{}", canonical_type_name) %} +{%- if self.include_once_check("CallbackInterfaceRuntime.swift") %}{%- include "CallbackInterfaceRuntime.swift" %}{%- endif %} + +// Declaration and FfiConverters for {{ type_name }} Callback Interface + +public protocol {{ type_name }} : AnyObject { + {% for meth in cbi.methods() -%} + func {{ meth.name()|fn_name }}({% call swift::arg_list_protocol(meth) %}) {% call swift::throws(meth) -%} + {%- match meth.return_type() -%} + {%- when Some with (return_type) %} -> {{ return_type|type_name -}} + {%- else -%} + {%- endmatch %} + {% endfor %} +} + +// The ForeignCallback that is passed to Rust. +fileprivate let {{ foreign_callback }} : ForeignCallback = + { (handle: UniFFICallbackHandle, method: Int32, args: RustBuffer, out_buf: UnsafeMutablePointer<RustBuffer>) -> Int32 in + {% for meth in cbi.methods() -%} + {%- let method_name = format!("invoke_{}", meth.name())|fn_name -%} + + func {{ method_name }}(_ swiftCallbackInterface: {{ type_name }}, _ args: RustBuffer) throws -> RustBuffer { + defer { args.deallocate() } + {#- Unpacking args from the RustBuffer #} + {%- if meth.arguments().len() != 0 -%} + {#- Calling the concrete callback object #} + + var reader = createReader(data: Data(rustBuffer: args)) + {% if meth.return_type().is_some() %}let result = {% endif -%} + {% if meth.throws() %}try {% endif -%} + swiftCallbackInterface.{{ meth.name()|fn_name }}( + {% for arg in meth.arguments() -%} + {% if !config.omit_argument_labels() %}{{ arg.name()|var_name }}: {% endif %} try {{ arg|read_fn }}(from: &reader) + {%- if !loop.last %}, {% endif %} + {% endfor -%} + ) + {% else %} + {% if meth.return_type().is_some() %}let result = {% endif -%} + {% if meth.throws() %}try {% endif -%} + swiftCallbackInterface.{{ meth.name()|fn_name }}() + {% endif -%} + + {#- Packing up the return value into a RustBuffer #} + {%- match meth.return_type() -%} + {%- when Some with (return_type) -%} + var writer = [UInt8]() + {{ return_type|write_fn }}(result, into: &writer) + return RustBuffer(bytes: writer) + {%- else -%} + return RustBuffer() + {% endmatch -%} + // TODO catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + + } + {% endfor %} + + let cb: {{ cbi|type_name }} + do { + cb = try {{ ffi_converter_name }}.lift(handle) + } catch { + out_buf.pointee = {{ Type::String.borrow()|lower_fn }}("{{ cbi.name() }}: Invalid handle") + return -1 + } + + switch method { + case IDX_CALLBACK_FREE: + {{ ffi_converter_name }}.drop(handle: 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 -%} + case {{ loop.index }}: + do { + out_buf.pointee = try {{ method_name }}(cb, args) + // Value written to out buffer. + // See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` + return 1 + {%- match meth.throws_type() %} + {%- when Some(error_type) %} + } catch let error as {{ error_type|type_name }} { + out_buf.pointee = {{ error_type|lower_fn }}(error) + return -2 + {%- else %} + {%- endmatch %} + } catch let error { + out_buf.pointee = {{ Type::String.borrow()|lower_fn }}(String(describing: error)) + 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 InternalError. + // https://github.com/mozilla/uniffi-rs/issues/351 + default: + // An unexpected error happened. + // See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` + return -1 + } + } + +// FfiConverter protocol for callback interfaces +fileprivate struct {{ ffi_converter_name }} { + // Initialize our callback method with the scaffolding code + private static var callbackInitialized = false + private static func initCallback() { + try! rustCall { (err: UnsafeMutablePointer<RustCallStatus>) in + {{ cbi.ffi_init_callback().name() }}({{ foreign_callback }}, err) + } + } + private static func ensureCallbackinitialized() { + if !callbackInitialized { + initCallback() + callbackInitialized = true + } + } + + static func drop(handle: UniFFICallbackHandle) { + handleMap.remove(handle: handle) + } + + private static var handleMap = UniFFICallbackHandleMap<{{ type_name }}>() +} + +extension {{ ffi_converter_name }} : FfiConverter { + typealias SwiftType = {{ type_name }} + // We can use Handle as the FfiType because it's a typealias to UInt64 + typealias FfiType = UniFFICallbackHandle + + public static func lift(_ handle: UniFFICallbackHandle) throws -> SwiftType { + ensureCallbackinitialized(); + guard let callback = handleMap.get(handle: handle) else { + throw UniffiInternalError.unexpectedStaleHandle + } + return callback + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + ensureCallbackinitialized(); + let handle: UniFFICallbackHandle = try readInt(&buf) + return try lift(handle) + } + + public static func lower(_ v: SwiftType) -> UniFFICallbackHandle { + ensureCallbackinitialized(); + return handleMap.insert(obj: v) + } + + public static func write(_ v: SwiftType, into buf: inout [UInt8]) { + ensureCallbackinitialized(); + writeInt(&buf, lower(v)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/CustomType.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/CustomType.swift new file mode 100644 index 0000000000..cb63f0b97b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/CustomType.swift @@ -0,0 +1,86 @@ +{%- let ffi_type_name=builtin.ffi_type().borrow()|type_ffi_lowered %} +{%- match config.custom_types.get(name.as_str()) %} +{%- when None %} +{#- No config, just forward all methods to our builtin type #} +/** + * Typealias from the type name used in the UDL file to the builtin type. This + * is needed because the UDL type name is used in function/method signatures. + */ +public typealias {{ name }} = {{ builtin|type_name }} +public struct FfiConverterType{{ name }}: FfiConverter { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ name }} { + return try {{ builtin|read_fn }}(from: &buf) + } + + public static func write(_ value: {{ name }}, into buf: inout [UInt8]) { + return {{ builtin|write_fn }}(value, into: &buf) + } + + public static func lift(_ value: {{ ffi_type_name }}) throws -> {{ name }} { + return try {{ builtin|lift_fn }}(value) + } + + public static func lower(_ value: {{ name }}) -> {{ ffi_type_name }} { + return {{ builtin|lower_fn }}(value) + } +} + +{%- when Some with (config) %} + +{# When the config specifies a different type name, create a typealias for it #} +{%- match config.type_name %} +{%- when Some with (concrete_type_name) %} +/** + * Typealias from the type name used in the UDL file to the custom type. This + * is needed because the UDL type name is used in function/method signatures. + */ +public typealias {{ name }} = {{ concrete_type_name }} +{%- else %} +{%- endmatch %} + +{%- match config.imports %} +{%- when Some(imports) %} +{%- for import_name in imports %} +{{ self.add_import(import_name) }} +{%- endfor %} +{%- else %} +{%- endmatch %} + +public struct FfiConverterType{{ name }}: FfiConverter { + {#- Custom type config supplied, use it to convert the builtin type #} + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ name }} { + let builtinValue = try {{ builtin|read_fn }}(from: &buf) + return {{ config.into_custom.render("builtinValue") }} + } + + public static func write(_ value: {{ name }}, into buf: inout [UInt8]) { + let builtinValue = {{ config.from_custom.render("value") }} + return {{ builtin|write_fn }}(builtinValue, into: &buf) + } + + public static func lift(_ value: {{ ffi_type_name }}) throws -> {{ name }} { + let builtinValue = try {{ builtin|lift_fn }}(value) + return {{ config.into_custom.render("builtinValue") }} + } + + public static func lower(_ value: {{ name }}) -> {{ ffi_type_name }} { + let builtinValue = {{ config.from_custom.render("value") }} + return {{ builtin|lower_fn }}(builtinValue) + } +} + +{# +We always write these public functions just incase the type is used as +an external type by another crate. +#} +public func FfiConverterType{{ name }}_lift(_ buf: RustBuffer) throws -> {{ name }} { + return try FfiConverterType{{ name }}.lift(buf) +} + +public func FfiConverterType{{ name }}_lower(_ value: {{ name }}) -> RustBuffer { + return FfiConverterType{{ name }}.lower(value) +} + +{%- endmatch %} + diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/DurationHelper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/DurationHelper.swift new file mode 100644 index 0000000000..c2aa49e9d1 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/DurationHelper.swift @@ -0,0 +1,24 @@ +fileprivate struct FfiConverterDuration: FfiConverterRustBuffer { + typealias SwiftType = TimeInterval + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> TimeInterval { + let seconds: UInt64 = try readInt(&buf) + let nanoseconds: UInt32 = try readInt(&buf) + return Double(seconds) + (Double(nanoseconds) / 1.0e9) + } + + public static func write(_ value: TimeInterval, into buf: inout [UInt8]) { + if value.rounded(.down) > Double(Int64.max) { + fatalError("Duration overflow, exceeds max bounds supported by Uniffi") + } + + if value < 0 { + fatalError("Invalid duration, must be non-negative") + } + + let seconds = UInt64(value) + let nanoseconds = UInt32((value - Double(seconds)) * 1.0e9) + writeInt(&buf, seconds) + writeInt(&buf, nanoseconds) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/EnumTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/EnumTemplate.swift new file mode 100644 index 0000000000..74f0a3effe --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/EnumTemplate.swift @@ -0,0 +1,60 @@ +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. +{%- let e = ci.get_enum_definition(name).unwrap() %} +public enum {{ type_name }} { + {% for variant in e.variants() %} + case {{ variant.name()|enum_variant_swift }}{% if variant.fields().len() > 0 %}({% call swift::field_list_decl(variant) %}){% endif -%} + {% endfor %} +} + +public struct {{ ffi_converter_name }}: FfiConverterRustBuffer { + typealias SwiftType = {{ type_name }} + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ type_name }} { + let variant: Int32 = try readInt(&buf) + switch variant { + {% for variant in e.variants() %} + case {{ loop.index }}: return .{{ variant.name()|enum_variant_swift }}{% if variant.has_fields() %}( + {%- for field in variant.fields() %} + {{ field.name()|var_name }}: try {{ field|read_fn }}(from: &buf) + {%- if !loop.last %}, {% endif %} + {%- endfor %} + ){%- endif %} + {% endfor %} + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: {{ type_name }}, into buf: inout [UInt8]) { + switch value { + {% for variant in e.variants() %} + {% if variant.has_fields() %} + case let .{{ variant.name()|enum_variant_swift }}({% for field in variant.fields() %}{{ field.name()|var_name }}{%- if loop.last -%}{%- else -%},{%- endif -%}{% endfor %}): + writeInt(&buf, Int32({{ loop.index }})) + {% for field in variant.fields() -%} + {{ field|write_fn }}({{ field.name()|var_name }}, into: &buf) + {% endfor -%} + {% else %} + case .{{ variant.name()|enum_variant_swift }}: + writeInt(&buf, Int32({{ loop.index }})) + {% endif %} + {%- endfor %} + } + } +} + +{# +We always write these public functions just in case the enum is used as +an external type by another crate. +#} +public func {{ ffi_converter_name }}_lift(_ buf: RustBuffer) throws -> {{ type_name }} { + return try {{ ffi_converter_name }}.lift(buf) +} + +public func {{ ffi_converter_name }}_lower(_ value: {{ type_name }}) -> RustBuffer { + return {{ ffi_converter_name }}.lower(value) +} + +{% if !contains_object_references %} +extension {{ type_name }}: Equatable, Hashable {} +{% endif %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift new file mode 100644 index 0000000000..b2c4bfe492 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift @@ -0,0 +1,83 @@ +{%- let e = ci.get_error_definition(name).unwrap() %} +public enum {{ type_name }} { + + {% if e.is_flat() %} + {% for variant in e.variants() %} + // Simple error enums only carry a message + case {{ variant.name()|class_name }}(message: String) + {% endfor %} + + {%- else %} + {% for variant in e.variants() %} + case {{ variant.name()|class_name }}{% if variant.fields().len() > 0 %}({% call swift::field_list_decl(variant) %}){% endif -%} + {% endfor %} + + {%- endif %} +} + +public struct {{ ffi_converter_name }}: FfiConverterRustBuffer { + typealias SwiftType = {{ type_name }} + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ type_name }} { + let variant: Int32 = try readInt(&buf) + switch variant { + + {% if e.is_flat() %} + + {% for variant in e.variants() %} + case {{ loop.index }}: return .{{ variant.name()|class_name }}( + message: try {{ Type::String.borrow()|read_fn }}(from: &buf) + ) + {% endfor %} + + {% else %} + + {% for variant in e.variants() %} + case {{ loop.index }}: return .{{ variant.name()|class_name }}{% if variant.has_fields() -%}( + {% for field in variant.fields() -%} + {{ field.name()|var_name }}: try {{ field|read_fn }}(from: &buf) + {%- if !loop.last %}, {% endif %} + {% endfor -%} + ){% endif -%} + {% endfor %} + + {% endif -%} + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: {{ type_name }}, into buf: inout [UInt8]) { + switch value { + + {% if e.is_flat() %} + + {% for variant in e.variants() %} + case let .{{ variant.name()|class_name }}(message): + writeInt(&buf, Int32({{ loop.index }})) + {{ Type::String.borrow()|write_fn }}(message, into: &buf) + {%- endfor %} + + {% else %} + + {% for variant in e.variants() %} + {% if variant.has_fields() %} + case let .{{ variant.name()|class_name }}({% for field in variant.fields() %}{{ field.name()|var_name }}{%- if loop.last -%}{%- else -%},{%- endif -%}{% endfor %}): + writeInt(&buf, Int32({{ loop.index }})) + {% for field in variant.fields() -%} + {{ field|write_fn }}({{ field.name()|var_name }}, into: &buf) + {% endfor -%} + {% else %} + case .{{ variant.name()|class_name }}: + writeInt(&buf, Int32({{ loop.index }})) + {% endif %} + {%- endfor %} + + {%- endif %} + } + } +} + +{% if !contains_object_references %} +extension {{ type_name }}: Equatable, Hashable {} +{% endif %} +extension {{ type_name }}: Error { } diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Float32Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Float32Helper.swift new file mode 100644 index 0000000000..fb986beab6 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Float32Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterFloat: FfiConverterPrimitive { + typealias FfiType = Float + typealias SwiftType = Float + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Float { + return try lift(readFloat(&buf)) + } + + public static func write(_ value: Float, into buf: inout [UInt8]) { + writeFloat(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Float64Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Float64Helper.swift new file mode 100644 index 0000000000..74421c045c --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Float64Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterDouble: FfiConverterPrimitive { + typealias FfiType = Double + typealias SwiftType = Double + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Double { + return try lift(readDouble(&buf)) + } + + public static func write(_ value: Double, into buf: inout [UInt8]) { + writeDouble(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift new file mode 100644 index 0000000000..18e2397733 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift @@ -0,0 +1,84 @@ +// An error type for FFI errors. These errors occur at the UniFFI level, not +// the library level. +fileprivate enum UniffiInternalError: LocalizedError { + case bufferOverflow + case incompleteData + case unexpectedOptionalTag + case unexpectedEnumCase + case unexpectedNullPointer + case unexpectedRustCallStatusCode + case unexpectedRustCallError + case unexpectedStaleHandle + case rustPanic(_ message: String) + + public var errorDescription: String? { + switch self { + case .bufferOverflow: return "Reading the requested value would read past the end of the buffer" + case .incompleteData: return "The buffer still has data after lifting its containing value" + case .unexpectedOptionalTag: return "Unexpected optional tag; should be 0 or 1" + case .unexpectedEnumCase: return "Raw enum value doesn't match any cases" + case .unexpectedNullPointer: return "Raw pointer value was null" + case .unexpectedRustCallStatusCode: return "Unexpected RustCallStatus code" + case .unexpectedRustCallError: return "CALL_ERROR but no errorClass specified" + case .unexpectedStaleHandle: return "The object in the handle map has been dropped already" + case let .rustPanic(message): return message + } + } +} + +fileprivate let CALL_SUCCESS: Int8 = 0 +fileprivate let CALL_ERROR: Int8 = 1 +fileprivate let CALL_PANIC: Int8 = 2 + +fileprivate extension RustCallStatus { + init() { + self.init( + code: CALL_SUCCESS, + errorBuf: RustBuffer.init( + capacity: 0, + len: 0, + data: nil + ) + ) + } +} + +private func rustCall<T>(_ callback: (UnsafeMutablePointer<RustCallStatus>) -> T) throws -> T { + try makeRustCall(callback, errorHandler: { + $0.deallocate() + return UniffiInternalError.unexpectedRustCallError + }) +} + +private func rustCallWithError<T, F: FfiConverter> + (_ errorFfiConverter: F.Type, _ callback: (UnsafeMutablePointer<RustCallStatus>) -> T) throws -> T + where F.SwiftType: Error, F.FfiType == RustBuffer + { + try makeRustCall(callback, errorHandler: { return try errorFfiConverter.lift($0) }) +} + +private func makeRustCall<T>(_ callback: (UnsafeMutablePointer<RustCallStatus>) -> T, errorHandler: (RustBuffer) throws -> Error) throws -> T { + var callStatus = RustCallStatus.init() + let returnedVal = callback(&callStatus) + switch callStatus.code { + case CALL_SUCCESS: + return returnedVal + + case CALL_ERROR: + throw try errorHandler(callStatus.errorBuf) + + case 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 callStatus.errorBuf.len > 0 { + throw UniffiInternalError.rustPanic(try {{ Type::String.borrow()|lift_fn }}(callStatus.errorBuf)) + } else { + callStatus.errorBuf.deallocate() + throw UniffiInternalError.rustPanic("Rust panic") + } + + default: + throw UniffiInternalError.unexpectedRustCallStatusCode + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int16Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int16Helper.swift new file mode 100644 index 0000000000..ac57fc5e58 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int16Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterInt16: FfiConverterPrimitive { + typealias FfiType = Int16 + typealias SwiftType = Int16 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Int16 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: Int16, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int32Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int32Helper.swift new file mode 100644 index 0000000000..0ccfc13e4e --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int32Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterInt32: FfiConverterPrimitive { + typealias FfiType = Int32 + typealias SwiftType = Int32 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Int32 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: Int32, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int64Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int64Helper.swift new file mode 100644 index 0000000000..d7d4082933 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int64Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterInt64: FfiConverterPrimitive { + typealias FfiType = Int64 + typealias SwiftType = Int64 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Int64 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: Int64, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int8Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int8Helper.swift new file mode 100644 index 0000000000..f2387e4340 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int8Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterInt8: FfiConverterPrimitive { + typealias FfiType = Int8 + typealias SwiftType = Int8 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Int8 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: Int8, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/MapTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/MapTemplate.swift new file mode 100644 index 0000000000..05713aca26 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/MapTemplate.swift @@ -0,0 +1,22 @@ +fileprivate struct {{ ffi_converter_name }}: FfiConverterRustBuffer { + public static func write(_ value: {{ type_name }}, into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for (key, value) in value { + {{ key_type|write_fn }}(key, into: &buf) + {{ value_type|write_fn }}(value, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ type_name }} { + let len: Int32 = try readInt(&buf) + var dict = {{ type_name }}() + dict.reserveCapacity(Int(len)) + for _ in 0..<len { + let key = try {{ key_type|read_fn }}(from: &buf) + let value = try {{ value_type|read_fn }}(from: &buf) + dict[key] = value + } + return dict + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/ModuleMapTemplate.modulemap b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/ModuleMapTemplate.modulemap new file mode 100644 index 0000000000..f5f73ceb1b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/ModuleMapTemplate.modulemap @@ -0,0 +1,6 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! +module {{ config.ffi_module_name() }} { + header "{{ config.header_filename() }}" + export * +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift new file mode 100644 index 0000000000..ab0b22b1d8 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift @@ -0,0 +1,88 @@ +{%- let obj = ci.get_object_definition(name).unwrap() %} +public protocol {{ obj.name() }}Protocol { + {% for meth in obj.methods() -%} + func {{ meth.name()|fn_name }}({% call swift::arg_list_protocol(meth) %}) {% call swift::throws(meth) -%} + {%- match meth.return_type() -%} + {%- when Some with (return_type) %} -> {{ return_type|type_name -}} + {%- else -%} + {%- endmatch %} + {% endfor %} +} + +public class {{ type_name }}: {{ obj.name() }}Protocol { + fileprivate let pointer: UnsafeMutableRawPointer + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. + required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + {%- match obj.primary_constructor() %} + {%- when Some with (cons) %} + public convenience init({% call swift::arg_list_decl(cons) -%}) {% call swift::throws(cons) %} { + self.init(unsafeFromRawPointer: {% call swift::to_ffi_call(cons) %}) + } + {%- when None %} + {%- endmatch %} + + deinit { + try! rustCall { {{ obj.ffi_object_free().name() }}(pointer, $0) } + } + + {% for cons in obj.alternate_constructors() %} + public static func {{ cons.name()|fn_name }}({% call swift::arg_list_decl(cons) %}) {% call swift::throws(cons) %} -> {{ type_name }} { + return {{ type_name }}(unsafeFromRawPointer: {% call swift::to_ffi_call(cons) %}) + } + {% endfor %} + + {# // TODO: Maybe merge the two templates (i.e the one with a return type and the one without) #} + {% for meth in obj.methods() -%} + {%- match meth.return_type() -%} + + {%- when Some with (return_type) -%} + public func {{ meth.name()|fn_name }}({% call swift::arg_list_decl(meth) %}) {% call swift::throws(meth) %} -> {{ return_type|type_name }} { + return {% call swift::try(meth) %} {{ return_type|lift_fn }}( + {% call swift::to_ffi_call_with_prefix("self.pointer", meth) %} + ) + } + + {%- when None -%} + public func {{ meth.name()|fn_name }}({% call swift::arg_list_decl(meth) %}) {% call swift::throws(meth) %} { + {% call swift::to_ffi_call_with_prefix("self.pointer", meth) %} + } + {%- endmatch %} + {% endfor %} +} + + +public struct {{ ffi_converter_name }}: FfiConverter { + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = {{ type_name }} + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ type_name }} { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if (ptr == nil) { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: {{ type_name }}, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> {{ type_name }} { + return {{ type_name}}(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: {{ type_name }}) -> UnsafeMutableRawPointer { + return value.pointer + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/OptionalTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/OptionalTemplate.swift new file mode 100644 index 0000000000..1dac65be63 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/OptionalTemplate.swift @@ -0,0 +1,20 @@ +fileprivate struct {{ ffi_converter_name }}: FfiConverterRustBuffer { + typealias SwiftType = {{ type_name }} + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + {{ inner_type|write_fn }}(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try {{ inner_type|read_fn }}(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/RecordTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/RecordTemplate.swift new file mode 100644 index 0000000000..7f2de44052 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/RecordTemplate.swift @@ -0,0 +1,62 @@ +{%- let rec = ci.get_record_definition(name).unwrap() %} +public struct {{ type_name }} { + {%- for field in rec.fields() %} + public var {{ field.name()|var_name }}: {{ field|type_name }} + {%- endfor %} + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init({% call swift::field_list_decl(rec) %}) { + {%- for field in rec.fields() %} + self.{{ field.name()|var_name }} = {{ field.name()|var_name }} + {%- endfor %} + } +} + +{% if !contains_object_references %} +extension {{ type_name }}: Equatable, Hashable { + public static func ==(lhs: {{ type_name }}, rhs: {{ type_name }}) -> Bool { + {%- for field in rec.fields() %} + if lhs.{{ field.name()|var_name }} != rhs.{{ field.name()|var_name }} { + return false + } + {%- endfor %} + return true + } + + public func hash(into hasher: inout Hasher) { + {%- for field in rec.fields() %} + hasher.combine({{ field.name()|var_name }}) + {%- endfor %} + } +} +{% endif %} + +public struct {{ ffi_converter_name }}: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ type_name }} { + return try {{ type_name }}( + {%- for field in rec.fields() %} + {{ field.name()|var_name }}: {{ field|read_fn }}(from: &buf) + {%- if !loop.last %}, {% endif %} + {%- endfor %} + ) + } + + public static func write(_ value: {{ type_name }}, into buf: inout [UInt8]) { + {%- for field in rec.fields() %} + {{ field|write_fn }}(value.{{ field.name()|var_name }}, into: &buf) + {%- endfor %} + } +} + +{# +We always write these public functions just in case the struct is used as +an external type by another crate. +#} +public func {{ ffi_converter_name }}_lift(_ buf: RustBuffer) throws -> {{ type_name }} { + return try {{ ffi_converter_name }}.lift(buf) +} + +public func {{ ffi_converter_name }}_lower(_ value: {{ type_name }}) -> RustBuffer { + return {{ ffi_converter_name }}.lower(value) +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/RustBufferTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/RustBufferTemplate.swift new file mode 100644 index 0000000000..2f737b6635 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/RustBufferTemplate.swift @@ -0,0 +1,183 @@ +fileprivate extension RustBuffer { + // Allocate a new buffer, copying the contents of a `UInt8` array. + init(bytes: [UInt8]) { + let rbuf = bytes.withUnsafeBufferPointer { ptr in + RustBuffer.from(ptr) + } + self.init(capacity: rbuf.capacity, len: rbuf.len, data: rbuf.data) + } + + static func from(_ ptr: UnsafeBufferPointer<UInt8>) -> RustBuffer { + try! rustCall { {{ ci.ffi_rustbuffer_from_bytes().name() }}(ForeignBytes(bufferPointer: ptr), $0) } + } + + // Frees the buffer in place. + // The buffer must not be used after this is called. + func deallocate() { + try! rustCall { {{ ci.ffi_rustbuffer_free().name() }}(self, $0) } + } +} + +fileprivate extension ForeignBytes { + init(bufferPointer: UnsafeBufferPointer<UInt8>) { + self.init(len: Int32(bufferPointer.count), data: bufferPointer.baseAddress) + } +} + +// For every type used in the interface, we provide helper methods for conveniently +// lifting and lowering that type from C-compatible data, and for reading and writing +// values of that type in a buffer. + +// Helper classes/extensions that don't change. +// Someday, this will be in a library of its own. + +fileprivate extension Data { + init(rustBuffer: RustBuffer) { + // TODO: This copies the buffer. Can we read directly from a + // Rust buffer? + self.init(bytes: rustBuffer.data!, count: Int(rustBuffer.len)) + } +} + +// Define reader functionality. Normally this would be defined in a class or +// struct, but we use standalone functions instead in order to make external +// types work. +// +// With external types, one swift source file needs to be able to call the read +// method on another source file's FfiConverter, but then what visibility +// should Reader have? +// - If Reader is fileprivate, then this means the read() must also +// be fileprivate, which doesn't work with external types. +// - If Reader is internal/public, we'll get compile errors since both source +// files will try define the same type. +// +// Instead, the read() method and these helper functions input a tuple of data + +fileprivate func createReader(data: Data) -> (data: Data, offset: Data.Index) { + (data: data, offset: 0) +} + +// Reads an integer at the current offset, in big-endian order, and advances +// the offset on success. Throws if reading the integer would move the +// offset past the end of the buffer. +fileprivate func readInt<T: FixedWidthInteger>(_ reader: inout (data: Data, offset: Data.Index)) throws -> T { + let range = reader.offset..<reader.offset + MemoryLayout<T>.size + guard reader.data.count >= range.upperBound else { + throw UniffiInternalError.bufferOverflow + } + if T.self == UInt8.self { + let value = reader.data[reader.offset] + reader.offset += 1 + return value as! T + } + var value: T = 0 + let _ = withUnsafeMutableBytes(of: &value, { reader.data.copyBytes(to: $0, from: range)}) + reader.offset = range.upperBound + return value.bigEndian +} + +// Reads an arbitrary number of bytes, to be used to read +// raw bytes, this is useful when lifting strings +fileprivate func readBytes(_ reader: inout (data: Data, offset: Data.Index), count: Int) throws -> Array<UInt8> { + let range = reader.offset..<(reader.offset+count) + guard reader.data.count >= range.upperBound else { + throw UniffiInternalError.bufferOverflow + } + var value = [UInt8](repeating: 0, count: count) + value.withUnsafeMutableBufferPointer({ buffer in + reader.data.copyBytes(to: buffer, from: range) + }) + reader.offset = range.upperBound + return value +} + +// Reads a float at the current offset. +fileprivate func readFloat(_ reader: inout (data: Data, offset: Data.Index)) throws -> Float { + return Float(bitPattern: try readInt(&reader)) +} + +// Reads a float at the current offset. +fileprivate func readDouble(_ reader: inout (data: Data, offset: Data.Index)) throws -> Double { + return Double(bitPattern: try readInt(&reader)) +} + +// Indicates if the offset has reached the end of the buffer. +fileprivate func hasRemaining(_ reader: (data: Data, offset: Data.Index)) -> Bool { + return reader.offset < reader.data.count +} + +// Define writer functionality. Normally this would be defined in a class or +// struct, but we use standalone functions instead in order to make external +// types work. See the above discussion on Readers for details. + +fileprivate func createWriter() -> [UInt8] { + return [] +} + +fileprivate func writeBytes<S>(_ writer: inout [UInt8], _ byteArr: S) where S: Sequence, S.Element == UInt8 { + writer.append(contentsOf: byteArr) +} + +// Writes an integer in big-endian order. +// +// Warning: make sure what you are trying to write +// is in the correct type! +fileprivate func writeInt<T: FixedWidthInteger>(_ writer: inout [UInt8], _ value: T) { + var value = value.bigEndian + withUnsafeBytes(of: &value) { writer.append(contentsOf: $0) } +} + +fileprivate func writeFloat(_ writer: inout [UInt8], _ value: Float) { + writeInt(&writer, value.bitPattern) +} + +fileprivate func writeDouble(_ writer: inout [UInt8], _ value: Double) { + writeInt(&writer, value.bitPattern) +} + +// Protocol for types that transfer other types across the FFI. This is +// analogous go the Rust trait of the same name. +fileprivate protocol FfiConverter { + associatedtype FfiType + associatedtype SwiftType + + static func lift(_ value: FfiType) throws -> SwiftType + static func lower(_ value: SwiftType) -> FfiType + static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType + static func write(_ value: SwiftType, into buf: inout [UInt8]) +} + +// Types conforming to `Primitive` pass themselves directly over the FFI. +fileprivate protocol FfiConverterPrimitive: FfiConverter where FfiType == SwiftType { } + +extension FfiConverterPrimitive { + public static func lift(_ value: FfiType) throws -> SwiftType { + return value + } + + public static func lower(_ value: SwiftType) -> FfiType { + return value + } +} + +// Types conforming to `FfiConverterRustBuffer` lift and lower into a `RustBuffer`. +// Used for complex types where it's hard to write a custom lift/lower. +fileprivate protocol FfiConverterRustBuffer: FfiConverter where FfiType == RustBuffer {} + +extension FfiConverterRustBuffer { + public static func lift(_ buf: RustBuffer) throws -> SwiftType { + var reader = createReader(data: Data(rustBuffer: buf)) + let value = try read(from: &reader) + if hasRemaining(reader) { + throw UniffiInternalError.incompleteData + } + buf.deallocate() + return value + } + + public static func lower(_ value: SwiftType) -> RustBuffer { + var writer = createWriter() + write(value, into: &writer) + return RustBuffer(bytes: writer) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/SequenceTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/SequenceTemplate.swift new file mode 100644 index 0000000000..bf664f6411 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/SequenceTemplate.swift @@ -0,0 +1,21 @@ +fileprivate struct {{ ffi_converter_name }}: FfiConverterRustBuffer { + typealias SwiftType = {{ type_name }} + + public static func write(_ value: {{ type_name }}, into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + {{ inner_type|write_fn }}(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ type_name }} { + let len: Int32 = try readInt(&buf) + var seq = {{ type_name }}() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try {{ inner_type|read_fn }}(from: &buf)) + } + return seq + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/StringHelper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/StringHelper.swift new file mode 100644 index 0000000000..b7d3466bdd --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/StringHelper.swift @@ -0,0 +1,37 @@ +fileprivate struct FfiConverterString: FfiConverter { + typealias SwiftType = String + typealias FfiType = RustBuffer + + public static func lift(_ value: RustBuffer) throws -> String { + defer { + value.deallocate() + } + if value.data == nil { + return String() + } + let bytes = UnsafeBufferPointer<UInt8>(start: value.data!, count: Int(value.len)) + return String(bytes: bytes, encoding: String.Encoding.utf8)! + } + + public static func lower(_ value: String) -> RustBuffer { + return value.utf8CString.withUnsafeBufferPointer { ptr in + // The swift string gives us int8_t, we want uint8_t. + ptr.withMemoryRebound(to: UInt8.self) { ptr in + // The swift string gives us a trailing null byte, we don't want it. + let buf = UnsafeBufferPointer(rebasing: ptr.prefix(upTo: ptr.count - 1)) + return RustBuffer.from(buf) + } + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> String { + let len: Int32 = try readInt(&buf) + return String(bytes: try readBytes(&buf, count: Int(len)), encoding: String.Encoding.utf8)! + } + + public static func write(_ value: String, into buf: inout [UInt8]) { + let len = Int32(value.utf8.count) + writeInt(&buf, len) + writeBytes(&buf, value.utf8) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/TimestampHelper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/TimestampHelper.swift new file mode 100644 index 0000000000..3cd472fa0e --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/TimestampHelper.swift @@ -0,0 +1,34 @@ +fileprivate struct FfiConverterTimestamp: FfiConverterRustBuffer { + typealias SwiftType = Date + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Date { + let seconds: Int64 = try readInt(&buf) + let nanoseconds: UInt32 = try readInt(&buf) + if seconds >= 0 { + let delta = Double(seconds) + (Double(nanoseconds) / 1.0e9) + return Date.init(timeIntervalSince1970: delta) + } else { + let delta = Double(seconds) - (Double(nanoseconds) / 1.0e9) + return Date.init(timeIntervalSince1970: delta) + } + } + + public static func write(_ value: Date, into buf: inout [UInt8]) { + var delta = value.timeIntervalSince1970 + var sign: Int64 = 1 + if delta < 0 { + // The nanoseconds portion of the epoch offset must always be + // positive, to simplify the calculation we will use the absolute + // value of the offset. + sign = -1 + delta = -delta + } + if delta.rounded(.down) > Double(Int64.max) { + fatalError("Timestamp overflow, exceeds max bounds supported by Uniffi") + } + let seconds = Int64(delta) + let nanoseconds = UInt32((delta - Double(seconds)) * 1.0e9) + writeInt(&buf, sign * seconds) + writeInt(&buf, nanoseconds) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift new file mode 100644 index 0000000000..5ce762d2a0 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift @@ -0,0 +1,15 @@ +{%- match func.return_type() -%} +{%- when Some with (return_type) %} + +public func {{ func.name()|fn_name }}({%- call swift::arg_list_decl(func) -%}) {% call swift::throws(func) %} -> {{ return_type|type_name }} { + return {% call swift::try(func) %} {{ return_type|lift_fn }}( + {% call swift::to_ffi_call(func) %} + ) +} + +{% when None %} + +public func {{ func.name()|fn_name }}({% call swift::arg_list_decl(func) %}) {% call swift::throws(func) %} { + {% call swift::to_ffi_call(func) %} +} +{% endmatch %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Types.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Types.swift new file mode 100644 index 0000000000..6e37e1b7f3 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Types.swift @@ -0,0 +1,90 @@ +{%- import "macros.swift" as swift %} +{%- 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 %} +{%- let contains_object_references = ci.item_contains_object_references(type_) %} + +{# + # Map `Type` instances to an include statement for that type. + # + # There is a companion match in `KotlinCodeOracle::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.swift" %} + +{%- when Type::String %} +{%- include "StringHelper.swift" %} + +{%- when Type::Int8 %} +{%- include "Int8Helper.swift" %} + +{%- when Type::Int16 %} +{%- include "Int16Helper.swift" %} + +{%- when Type::Int32 %} +{%- include "Int32Helper.swift" %} + +{%- when Type::Int64 %} +{%- include "Int64Helper.swift" %} + +{%- when Type::UInt8 %} +{%- include "UInt8Helper.swift" %} + +{%- when Type::UInt16 %} +{%- include "UInt16Helper.swift" %} + +{%- when Type::UInt32 %} +{%- include "UInt32Helper.swift" %} + +{%- when Type::UInt64 %} +{%- include "UInt64Helper.swift" %} + +{%- when Type::Float32 %} +{%- include "Float32Helper.swift" %} + +{%- when Type::Float64 %} +{%- include "Float64Helper.swift" %} + +{%- when Type::Timestamp %} +{%- include "TimestampHelper.swift" %} + +{%- when Type::Duration %} +{%- include "DurationHelper.swift" %} + +{%- when Type::CallbackInterface(name) %} +{%- include "CallbackInterfaceTemplate.swift" %} + +{%- when Type::Custom { name, builtin } %} +{%- include "CustomType.swift" %} + +{%- when Type::Enum(name) %} +{%- include "EnumTemplate.swift" %} + +{%- when Type::Error(name) %} +{%- include "ErrorTemplate.swift" %} + +{%- when Type::Object(name) %} +{%- include "ObjectTemplate.swift" %} + +{%- when Type::Record(name) %} +{%- include "RecordTemplate.swift" %} + +{%- when Type::Optional(inner_type) %} +{%- include "OptionalTemplate.swift" %} + +{%- when Type::Sequence(inner_type) %} +{%- include "SequenceTemplate.swift" %} + +{%- when Type::Map(key_type, value_type) %} +{%- include "MapTemplate.swift" %} + +{%- else %} +{%- endmatch %} +{%- endfor %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt16Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt16Helper.swift new file mode 100644 index 0000000000..b7fc0942a5 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt16Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterUInt16: FfiConverterPrimitive { + typealias FfiType = UInt16 + typealias SwiftType = UInt16 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt16 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt32Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt32Helper.swift new file mode 100644 index 0000000000..e7a64aab93 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt32Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterUInt32: FfiConverterPrimitive { + typealias FfiType = UInt32 + typealias SwiftType = UInt32 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt32 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt64Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt64Helper.swift new file mode 100644 index 0000000000..eb674a2c53 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt64Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterUInt64: FfiConverterPrimitive { + typealias FfiType = UInt64 + typealias SwiftType = UInt64 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt64 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt8Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt8Helper.swift new file mode 100644 index 0000000000..4baf613494 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt8Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterUInt8: FfiConverterPrimitive { + typealias FfiType = UInt8 + typealias SwiftType = UInt8 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt8 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: UInt8, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/macros.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/macros.swift new file mode 100644 index 0000000000..dcc0a2df74 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/macros.swift @@ -0,0 +1,97 @@ +{# +// 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) -%} +{% call try(func) %} + {% match func.throws_type() %} + {% when Some with (e) %} + rustCallWithError({{ e|ffi_converter_name }}.self) { + {% else %} + rustCall() { + {% endmatch %} + {{ func.ffi_func().name() }}({% call _arg_list_ffi_call(func) -%}{% if func.arguments().len() > 0 %}, {% endif %}$0) +} +{%- endmacro -%} + +{%- macro to_ffi_call_with_prefix(prefix, func) -%} +{% call try(func) %} + {%- match func.throws_type() %} + {%- when Some with (e) %} + rustCallWithError({{ e|ffi_converter_name }}.self) { + {%- else %} + rustCall() { + {% endmatch %} + {{ func.ffi_func().name() }}( + {{- prefix }}, {% call _arg_list_ffi_call(func) -%}{% if func.arguments().len() > 0 %}, {% endif %}$0 + ) +} +{%- 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 Swift declarations of methods, functions and constructors. +// Note the var_name and type_name filters. +-#} + +{% macro arg_list_decl(func) %} + {%- for arg in func.arguments() -%} + {% if config.omit_argument_labels() %}_ {% endif %}{{ arg.name()|var_name }}: {{ arg|type_name -}} + {%- match arg.default_value() %} + {%- when Some with(literal) %} = {{ literal|literal_swift(arg) }} + {%- else %} + {%- endmatch %} + {%- if !loop.last %}, {% endif -%} + {%- endfor %} +{%- endmacro %} + +{#- +// Field lists as used in Swift declarations of Records and Enums. +// Note the var_name and type_name filters. +-#} +{% macro field_list_decl(item) %} + {%- for field in item.fields() -%} + {{ field.name()|var_name }}: {{ field|type_name -}} + {%- match field.default_value() %} + {%- when Some with(literal) %} = {{ literal|literal_swift(field) }} + {%- else %} + {%- endmatch -%} + {% if !loop.last %}, {% endif %} + {%- endfor %} +{%- endmacro %} + + +{% macro arg_list_protocol(func) %} + {%- for arg in func.arguments() -%} + {% if config.omit_argument_labels() %}_ {% endif %}{{ arg.name()|var_name }}: {{ arg|type_name -}} + {%- 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 }} {{ arg.name() -}}, + {%- endfor %} + RustCallStatus *_Nonnull out_status +{%- endmacro -%} + +{%- macro throws(func) %} +{%- if func.throws() %}throws{% endif %} +{%- endmacro -%} + +{%- macro try(func) %} +{%- if func.throws() %}try{% else %}try!{% endif %} +{%- endmacro -%} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift new file mode 100644 index 0000000000..ac0557912e --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift @@ -0,0 +1,40 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! +{%- import "macros.swift" as swift %} +import Foundation +{%- for imported_class in self.imports() %} +import {{ imported_class }} +{%- endfor %} + +// Depending on the consumer's build setup, the low-level FFI code +// might be in a separate module, or it might be compiled inline into +// this module. This is a bit of light hackery to work with both. +#if canImport({{ config.ffi_module_name() }}) +import {{ config.ffi_module_name() }} +#endif + +{% include "RustBufferTemplate.swift" %} +{% include "Helpers.swift" %} + +// Public interface members begin here. +{{ type_helper_code }} + +{%- for func in ci.function_definitions() %} +{%- include "TopLevelFunctionTemplate.swift" %} +{%- endfor %} + +/** + * Top level initializers and tear down methods. + * + * This is generated by uniffi. + */ +public enum {{ config.module_name().borrow()|class_name }}Lifecycle { + /** + * Initialize the FFI and Rust library. This should be only called once per application. + */ + func initialize() { + {%- for initialization_fn in self.initialization_fns() %} + {{ initialization_fn }}() + {%- endfor %} + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/test.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/test.rs new file mode 100644 index 0000000000..e51c4a9c05 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/test.rs @@ -0,0 +1,225 @@ +/* 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::{bail, Context, Result}; +use camino::{Utf8Path, Utf8PathBuf}; +use heck::ToSnakeCase; +use std::env::consts::{DLL_PREFIX, DLL_SUFFIX}; +use std::ffi::OsStr; +use std::fs::{read_to_string, File}; +use std::io::Write; +use std::process::Command; +use uniffi_testing::{CompileSource, UniFFITestHelper}; + +/// Run Swift 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_fixture_library_to_out_dir()")?; + let generated_sources = + GeneratedSources::new(&test_helper.cdylib_path()?, &out_dir, &test_helper) + .context("generate_sources()")?; + + // Compile the generated sources together to create a single swift module + compile_swift_module( + &out_dir, + &calc_module_name(&generated_sources.main_source_filename), + &generated_sources.generated_swift_files, + &generated_sources.module_map, + )?; + + // Run the test script against compiled bindings + + let mut command = Command::new("swift"); + command + .current_dir(&out_dir) + .arg("-I") + .arg(&out_dir) + .arg("-L") + .arg(&out_dir) + .args(calc_library_args(&out_dir)?) + .arg("-Xcc") + .arg(format!( + "-fmodule-map-file={}", + generated_sources.module_map + )) + .arg(&script_path); + let status = command + .spawn() + .context("Failed to spawn `swiftc` when running test script")? + .wait() + .context("Failed to wait for `swiftc` when running test script")?; + if !status.success() { + bail!("running `swift` to run test script failed ({:?})", command) + } + Ok(()) +} + +fn calc_module_name(filename: &str) -> String { + filename.strip_suffix(".swift").unwrap().to_snake_case() +} + +fn compile_swift_module<T: AsRef<OsStr>>( + out_dir: &Utf8Path, + module_name: &str, + sources: impl IntoIterator<Item = T>, + module_map: &Utf8Path, +) -> Result<()> { + let output_filename = format!("{DLL_PREFIX}testmod_{module_name}{DLL_SUFFIX}"); + let mut command = Command::new("swiftc"); + command + .current_dir(out_dir) + .arg("-emit-module") + .arg("-module-name") + .arg(module_name) + .arg("-o") + .arg(output_filename) + .arg("-emit-library") + .arg("-Xcc") + .arg(format!("-fmodule-map-file={}", module_map)) + .arg("-I") + .arg(out_dir) + .arg("-L") + .arg(out_dir) + .args(calc_library_args(out_dir)?) + .args(sources); + let status = command + .spawn() + .context("Failed to spawn `swiftc` when compiling bindings")? + .wait() + .context("Failed to wait for `swiftc` when compiling bindings")?; + if !status.success() { + bail!( + "running `swiftc` to compile bindings failed ({:?})", + command + ) + }; + Ok(()) +} + +// Stores sources generated by `uniffi-bindgen-swift` +struct GeneratedSources { + generated_swift_files: Vec<Utf8PathBuf>, + module_map: Utf8PathBuf, + main_source_filename: String, +} + +impl GeneratedSources { + fn new( + library_path: &Utf8Path, + out_dir: &Utf8Path, + test_helper: &UniFFITestHelper, + ) -> Result<Self> { + // Generate the bindings for the main compile source, and use that for the swift module name + let main_compile_source = test_helper.get_main_compile_source()?; + Self::run_generate_bindings(&main_compile_source, library_path, out_dir)?; + let generated_files = glob(&out_dir.join("*.swift"))?; + let main_source_filename = match generated_files.len() { + 0 => bail!( + "No .swift file generated for {}", + main_compile_source.udl_path + ), + 1 => generated_files + .into_iter() + .next() + .unwrap() + .file_name() + .unwrap() + .to_string(), + n => bail!( + "{n} .swift files generated for {}", + main_compile_source.udl_path + ), + }; + + // Generate the bindings for other compile sources (crates used by external types) + for source in test_helper.get_external_compile_sources()? { + crate::generate_bindings( + &source.udl_path, + source.config_path.as_deref(), + vec!["swift"], + Some(out_dir), + Some(library_path), + false, + )?; + } + + let generated_module_maps = glob(&out_dir.join("*.modulemap"))?; + + Ok(GeneratedSources { + main_source_filename, + generated_swift_files: glob(&out_dir.join("*.swift"))?, + module_map: match generated_module_maps.len() { + 0 => bail!("No modulemap files found in {out_dir}"), + // Normally we only generate 1 module map and can return it directly + 1 => generated_module_maps.into_iter().next().unwrap(), + // When we use multiple UDL files in a test, for example the ext-types fixture, + // then we get multiple module maps and need to combine them + _ => { + let path = out_dir.join("combined.modulemap"); + let mut f = File::create(&path)?; + write!( + f, + "{}", + generated_module_maps + .into_iter() + .map(|path| Ok(read_to_string(path)?)) + .collect::<Result<Vec<String>>>()? + .join("\n") + )?; + path + } + }, + }) + } + + fn run_generate_bindings( + source: &CompileSource, + library_path: &Utf8Path, + out_dir: &Utf8Path, + ) -> Result<()> { + crate::generate_bindings( + &source.udl_path, + source.config_path.as_deref(), + vec!["swift"], + Some(out_dir), + Some(library_path), + false, + ) + } +} + +// Wraps glob to use Utf8Paths and flattens errors +fn glob(globspec: &Utf8Path) -> Result<Vec<Utf8PathBuf>> { + glob::glob(globspec.as_str())? + .map(|globresult| Ok(Utf8PathBuf::try_from(globresult?)?)) + .collect() +} + +fn calc_library_args(out_dir: &Utf8Path) -> Result<Vec<String>> { + let results = glob::glob( + out_dir + .join(format!("{}*{}", DLL_PREFIX, DLL_SUFFIX)) + .as_str(), + )?; + results + .map(|globresult| { + let path = Utf8PathBuf::try_from(globresult.unwrap())?; + Ok(format!( + "-l{}", + path.file_name() + .unwrap() + .strip_prefix(DLL_PREFIX) + .unwrap() + .strip_suffix(DLL_SUFFIX) + .unwrap() + )) + }) + .collect() +} |