diff --git a/.circleci/config.yml b/.circleci/config.yml index 393b09ca10..f9110bc59f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -39,7 +39,7 @@ orbs: jobs: Check Rust formatting: docker: - - image: cimg/rust:1.69 + - image: cimg/rust:1.72 resource_class: small steps: - checkout @@ -49,7 +49,7 @@ jobs: - run: cargo fmt -- --check Lint Rust with clippy: docker: - - image: cimg/rust:1.69 + - image: cimg/rust:1.72 resource_class: small steps: - checkout @@ -59,7 +59,7 @@ jobs: - run: cargo clippy --all --all-targets -- -D warnings Lint Rust Docs: docker: - - image: cimg/rust:1.69 + - image: cimg/rust:1.72 resource_class: small steps: - checkout @@ -117,7 +117,7 @@ jobs: - run: cargo test -- --skip trybuild_ui_tests Deploy website: docker: - - image: cimg/rust:1.69 + - image: cimg/rust:1.72 resource_class: small steps: - checkout diff --git a/CHANGELOG.md b/CHANGELOG.md index 713e2d4f54..08a98ddd90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ [All changes in [[UnreleasedUniFFIVersion]]](https://github.com/mozilla/uniffi-rs/compare/v0.24.3...HEAD). ### What's new +- Proc-macros can now expose standard Rust traits (eg, `Display`, `Eq`, etc) - Fixed issues when trying to combine UDL and procmacros in the same crate when the "namespace" is different from the crate name. This meant that the "ffi namespace" has changed to consistently be the crate name, rather than either the crate name or the namespace name depending on whether the @@ -33,12 +34,21 @@ - Proc-macros: Added support for ByRef arguments - Proc-macros: Implemented custom type conversion error handling (https://mozilla.github.io/uniffi-rs/udl/custom_types.html#error-handling-during-conversion) - Error types must now implement `Error + Send + Sync + 'static`. +- Proc-macros: The `handle_unknown_callback_error` attribute is no longer needed for callback + interface errors +- Foreign types can now implement trait interfaces ### What's Fixed - Updated the async functionality to correctly handle cancellation (#1669) - Kotlin: Fixed low-level issue with exported async APIs +### What's changed? + +- Implementing `From` with the error half being +a [compatible error type](./udl/errors.md) - see below for more on error handling. + +For example: + +```rust,no_run +pub trait Keychain: Send + Sync + Debug { + fn get(&self, key: String) -> Result, KeyChainError>; + fn put(&self, key: String, value: String) -> Result<(), KeyChainError>; +} +``` + +If you are using macros add `#[uniffi::export]` above the trait. +Otherwise define this trait in your UDL file: + +```webidl +[Trait] +interface Keychain { + [Throws=KeyChainError] + string? get(string key); + + [Throws=KeyChainError] + void put(string key, string data); +}; +``` + +## 2. Allow it to be passed into Rust + +Here, we define a new object with a constructor which takes a keychain. + +```webidl +interface Authenticator { + constructor(Keychain keychain); + void login(); +}; +``` + +In Rust we'd write: + +```rust,no_run +struct Authenticator { + keychain: Arc, +} + +impl Authenticator { + pub fn new(keychain: Arc) -> Self { + Self { keychain } + } + + pub fn login(&self) { + let username = self.keychain.get("username".into()); + let password = self.keychain.get("password".into()); + } +} +``` + +## 3. Create a foreign language implementation of the trait + +Here's a Kotlin implementation: + +```kotlin +class KotlinKeychain: Keychain { + override fun get(key: String): String? { + // … elide the implementation. + return value + } + override fun put(key: String) { + // … elide the implementation. + } +} +``` + +…and Swift: + +```swift +class SwiftKeychain: Keychain { + func get(key: String) -> String? { + // … elide the implementation. + return value + } + func put(key: String) { + // … elide the implementation. + } +} +``` + +## 4. Pass the implementation to Rust + +Again, in Kotlin + +```kt +val authenticator = Authenticator(KotlinKeychain()) +// later on: +authenticator.login() +``` + +and in Swift: + +```swift +let authenticator = Authenticator(SwiftKeychain()) +// later on: +authenticator.login() +``` + +Care is taken to ensure that things are cleaned up in the foreign language once all Rust references drop. + +## ⚠️ Avoid cycles + +Foreign trait implementations make it easy to create cycles between Rust and foreign objects causing memory leaks. +For example a foreign implementation holding a reference to a Rust object which also holds a reference to the same foreign implementation. + +UniFFI doesn't try to help here and there's no universal advice; take the usual precautions. + +# Error handling + +We must handle foreign code failing, so all methods of the Rust trait should return a `Result<>` with a [compatible error type](./udl/errors.md) otherwise these errors will panic. + +## Unexpected Error handling. + +So long as your function returns a `Result<>`, it's possible for you to define how "unexpected" errors +(ie, errors not directly covered by your `Result<>` type, panics, etc) are converted to your `Result<>`'s `Err`. + +If your code defines a `From` impl for your error type, then those errors will be converted into your error type which will be returned to the Rust caller. +If your code does not define this implementation the generated code will panic. +In other words, you really should implement this! + +See our [callbacks example](https://github.com/mozilla/uniffi-rs/tree/main/examples/callbacks) for more. + diff --git a/docs/manual/src/futures.md b/docs/manual/src/futures.md index c127ea205e..d6279236ab 100644 --- a/docs/manual/src/futures.md +++ b/docs/manual/src/futures.md @@ -4,7 +4,7 @@ UniFFI supports exposing async Rust functions over the FFI. It can convert a Rus Check out the [examples](https://github.com/mozilla/uniffi-rs/tree/main/examples/futures) or the more terse and thorough [fixtures](https://github.com/mozilla/uniffi-rs/tree/main/fixtures/futures). -Note that currently async functions are only supported by proc-macros, if you require UDL support please file a bug. +Note that currently async functions are only supported by proc-macros, UDL support is being planned in https://github.com/mozilla/uniffi-rs/issues/1716. ## Example @@ -41,97 +41,3 @@ In Rust `Future` terminology this means the foreign bindings supply the "executo There are [some great API docs](https://docs.rs/uniffi_core/latest/uniffi_core/ffi/rustfuture/index.html) on the implementation that are well worth a read. See the [foreign-executor fixture](https://github.com/mozilla/uniffi-rs/tree/main/fixtures/foreign-executor) for more implementation details. - -## How it works - -As [described in the documentation](https://docs.rs/uniffi_core/latest/uniffi_core/ffi/rustfuture/index.html), -UniFFI generates code which uses callbacks from Rust futures back into that foreign "executor" to drive them to completion. -Fortunately, each of the bindings and Rust have similar models, so the discussion below is Python, but it's almost exactly the same in Kotlin and Swift. - -In the above example, the generated `say_after` function looks something like: - -```python - -# A helper to work with asyncio. -def _rust_say_after_executor(eventloop_handle, rust_task_handle): - event_loop = UniFFIMagic_GetExecutor(eventloop_handle) - - def callback(task_handle): - # The event-loop has called us - call back into Rust. - _uniffi_say_after_executor_callback(task_handle) - - # Now have the asyncio eventloop - ask it to schedule a call to help drive the Rust future. - eventloop.call_soon_threadsafe(callback, rust_task_handle) - -# A helper for say_after which creates a future and passes it Rust -def _rust_call_say_after(callback_fn): - # Handle to our executor. - eventloop = asyncio.get_running_loop() - eventloop_handle = UniFFIMagic_SetExecutor(eventloop) - - # Use asyncio to create a new Python future. - future = eventloop.create_future() - future_handle = UniFFIMagic_SetFuture(future) - - # This is a "normal" UniFFI call across the FFI to Rust scaffoloding, but - # because it is an async function it has a special signature which - # requires the handles and the callback. - _uniffi_call_say_after(executor_handle, callback_fun, future_handle) - - # and return the future to the caller. - return future - -def say_after_callback(future_handle, result) - future = UniFFIMagic_GetFuture(future_handle) - if future.cancelled(): - return - future.set_result(result)) - -def say_after(...): - return await _rust_call_say_after(say_after_callback) - -``` - -And the code generated for Rust is something like: - -```rust -struct SayAfterHelper { - rust_future: Future<>, - uniffi_executor_handle: ::uniffi::ForeignExecutorHandle, - uniffi_callback: ::uniffi::FfiConverter::FutureCallback, - uniffi_future_handle: ..., -} - -impl SayAfterHelper { - fn wake(&self) { - match self.rust_future.poll() { - Some(Poll::Pending) => { - // ... snip executor stuff - self.rust_future.wake() - }, - Some(Poll::Ready(v)) => { - // ready - tell the foreign executor - UniFFI_Magic_Invoke_Foreign_Callback(self.uniffi_callback, self.uniffi_future_handle) - }, - None => todo!("error handling"), - } - } -} - -pub extern "C" fn _uniffi_call_say_after( - uniffi_executor_handle: ::uniffi::ForeignExecutorHandle, - uniffi_callback: ::uniffi::FfiConverter::FutureCallback, - uniffi_future_handle: ..., -) { - // Call the async function to get the Rust future. - let rust_future = say_after(...) - let helper = SayAfterHelper { - rust_future, - uniffi_executor_handle, - uniffi_callback, - uniffi_future_handle, - ); - helper.wake(); - Ok(()) -} -``` \ No newline at end of file diff --git a/docs/manual/src/kotlin/configuration.md b/docs/manual/src/kotlin/configuration.md new file mode 100644 index 0000000000..74adc09f1f --- /dev/null +++ b/docs/manual/src/kotlin/configuration.md @@ -0,0 +1,36 @@ +# Configuration + +The generated Kotlin modules can be configured using a `uniffi.toml` configuration file. + +## Available options + +| Configuration name | Default | Description | +| ------------------ | ------- |------------ | +| `package_name` | `uniffi` | The Kotlin package name - ie, the value used in the `package` statement at the top of generated files. | +| `cdylib_name` | `uniffi_{namespace}`[^1] | The name of the compiled Rust library containing the FFI implementation (not needed when using `generate --library`). | +| `custom_types` | | A map which controls how custom types are exposed to Kotlin. See the [custom types section of the manual](../udl/custom_types#custom-types-in-the-bindings-code)| +| `external_packages` | | A map of packages to be used for the specified external crates. The key is the Rust crate name, the value is the Kotlin package which will be used referring to types in that crate. See the [external types section of the manual](../udl/ext_types_external#kotlin) + + +## Example + +Custom types +```toml +# Assuming a Custom Type named URL using a String as the builtin. +[bindings.kotlin.custom_types.Url] +# Name of the type in the Kotlin code +type_name = "URL" +# Classes that need to be imported +imports = [ "java.net.URI", "java.net.URL" ] +# Functions to convert between strings and URLs +into_custom = "URI({}).toURL()" +from_custom = "{}.toString()" +``` + +External types +```toml +[bindings.kotlin.external_packages] +# This specifies that external types from the crate `rust-crate-name` will be referred by by the package `"kotlin.package.name`. +rust-crate-name = "kotlin.package.name" +``` + diff --git a/docs/manual/src/proc_macro/index.md b/docs/manual/src/proc_macro/index.md index cafc1b4c1e..e2a3b76ec6 100644 --- a/docs/manual/src/proc_macro/index.md +++ b/docs/manual/src/proc_macro/index.md @@ -231,8 +231,8 @@ pub struct Uuid { val: String, } -// Use `url::Url` as a custom type, with `String` as the Builtin -uniffi::custom_type!(Url, String); +// Use `Uuid` as a custom type, with `String` as the Builtin +uniffi::custom_type!(Uuid, String); impl UniffiCustomTypeConverter for Uuid { type Builtin = String; @@ -331,35 +331,6 @@ pub trait Person { // } ``` -### Exception handling in callback interfaces - -Most languages allow arbitrary exceptions to be thrown, which presents issues for callback -interfaces. If a callback interface function returns a non-Result type, then any exception will -result in a panic on the Rust side. - -To avoid panics, callback interfaces can use `Result` types for all return values. If the callback -interface implementation throws the exception that corresponds to the `E` parameter, `Err(E)` will -be returned to the Rust code. However, in most languages it's still possible for the implementation -to throw other exceptions. To avoid panics in those cases, the error type must be wrapped -with the `#[uniffi(handle_unknown_callback_error)]` attribute and -`From` must be implemented: - -```rust -#[derive(uniffi::Error)] -#[uniffi(handle_unknown_callback_error)] -pub enum MyApiError { - IOError, - ValueError, - UnexpectedError { reason: String }, -} - -impl From for MyApiError { - fn from(e: UnexpectedUniFFICallbackError) -> Self { - Self::UnexpectedError { reason: e.reason } - } -} -``` - ## Types from dependent crates When using proc-macros, you can use types from dependent crates in your exported library, as long as diff --git a/docs/manual/src/python/configuration.md b/docs/manual/src/python/configuration.md new file mode 100644 index 0000000000..067e830b69 --- /dev/null +++ b/docs/manual/src/python/configuration.md @@ -0,0 +1,60 @@ +# Configuration + +The generated Python modules can be configured using a `uniffi.toml` configuration file. + +## Available options + +| Configuration name | Default | Description | +| ------------------ | ------- |------------ | +| `cdylib_name` | `uniffi_{namespace}`[^1] | The name of the compiled Rust library containing the FFI implementation (not needed when using `generate --library`). | +| `custom_types` | | A map which controls how custom types are exposed to Python. See the [custom types section of the manual](../udl/custom_types#custom-types-in-the-bindings-code)| +| `external_packages` | | A map which controls the package name used by external packages. See below for more. + +## External Packages + +When you reference external modules, uniffi will generate statements like `from module import Type` +in the referencing module. The `external_packages` configuration value allows you to specify how `module` +is formed in such statements. + +The value is a map, keyed by the crate-name and the value is the package name which will be used by +Python for that crate. The default value is an empty map. + +When looking up crate-name, the following behavior is implemented. + +### Default value +If no value for the crate is found, it is assumed that you will be packaging up your library +as a simple Python package, so the statement will be of the form `from .module import Type`, +where `module` is the namespace specified in that crate. + +Note that this is invalid syntax unless the module lives in a package - attempting to +use the module as a stand-alone module will fail. UniFFI just generates flat .py files; the +packaging is up to you. Eg, a build process might create a directory, create an `__init__.py` +file in that directory (maybe including `from subpackage import *`) and have `uniffi-bindgen` +generate the bindings into this directory. + +### Specified value +If the crate-name is found in the map, the specified entry used as a package name, so the statement will be of the form +`from package.module import Type` (again, where `module` is the namespace specified in that crate) + +An exception is when the specified value is an empty string, in which case you will see +`from module import Type`, so each generated module functions outside a package. +This is used by some UniFFI tests to avoid the test code needing to create a Python package. + +## Examples + +Custom Types +```toml +# Assuming a Custom Type named URL using a String as the builtin. +[bindings.python.custom_types.Url] +imports = ["urllib.parse"] +# Functions to convert between strings and the ParsedUrl class +into_custom = "urllib.parse.urlparse({})" +from_custom = "urllib.parse.urlunparse({})" +``` + +External Packages +```toml +[bindings.python.external_packages] +# An external type `Foo` in `crate-name` (which specifies a namespace of `my_module`) will be referenced via `from MyPackageName.my_module import Foo` +crate-name = "MyPackageName" +``` diff --git a/docs/manual/src/swift/configuration.md b/docs/manual/src/swift/configuration.md index 794fc518e3..0c41b5cc48 100644 --- a/docs/manual/src/swift/configuration.md +++ b/docs/manual/src/swift/configuration.md @@ -12,6 +12,8 @@ The generated Swift module can be configured using a `uniffi.toml` configuration | `ffi_module_filename` | `{ffi_module_name}` | The filename stem for the lower-level C module containing the FFI declarations. | | `generate_module_map` | `true` | Whether to generate a `.modulemap` file for the lower-level C module with FFI declarations. | | `omit_argument_labels` | `false` | Whether to omit argument labels in Swift function definitions. | +| `custom_types` | | A map which controls how custom types are exposed to Swift. See the [custom types section of the manual](../udl/custom_types#custom-types-in-the-bindings-code)| + [^1]: `namespace` is the top-level namespace from your UDL file. diff --git a/docs/manual/src/udl/builtin_types.md b/docs/manual/src/udl/builtin_types.md index 9930e9fb2b..4c1e618c78 100644 --- a/docs/manual/src/udl/builtin_types.md +++ b/docs/manual/src/udl/builtin_types.md @@ -15,7 +15,7 @@ The following built-in types can be passed as arguments/returned by Rust methods | `&T` | `[ByRef] T` | This works for `&str` and `&[T]` | | `Option` | `T?` | | | `Vec` | `sequence` | | -| `HashMap` | `record` | Only string keys are supported | +| `HashMap` | `record` | | | `()` | `void` | Empty return | | `Result` | N/A | See [Errors](./errors.md) section | diff --git a/docs/manual/src/udl/callback_interfaces.md b/docs/manual/src/udl/callback_interfaces.md index 31cf7c634e..28691f85cf 100644 --- a/docs/manual/src/udl/callback_interfaces.md +++ b/docs/manual/src/udl/callback_interfaces.md @@ -1,175 +1,36 @@ # Callback interfaces -Callback interfaces are traits specified in UDL which can be implemented by foreign languages. +Callback interfaces are a special implementation of +[Rust traits implemented by foreign languages](../foreign_traits). -They can provide Rust code access available to the host language, but not easily replicated -in Rust. +These are described in both UDL and proc-macros as an explict "callback interface". +They are (soft) deprecated, remain now for backwards compatibility, but probably +should be avoided. - * accessing device APIs. - * provide glue to clip together Rust components at runtime. - * access shared resources and assets bundled with the app. - -# Using callback interfaces - -## 1. Define a Rust trait - -This toy example defines a way of Rust accessing a key-value store exposed -by the host operating system (e.g. the key chain). - -```rust,no_run -pub trait Keychain: Send + Sync + Debug { - fn get(&self, key: String) -> Result, KeyChainError>; - fn put(&self, key: String, value: String) -> Result<(), KeyChainError>; -} -``` - -### Send + Sync? - -The concrete types that UniFFI generates for callback interfaces implement `Send`, `Sync`, and `Debug`, so it's legal to -include these as supertraits of your callback interface trait. This isn't strictly necessary, but it's often useful. In -particular, `Send + Sync` is useful when: - - Storing `Box` types inside a type that needs to be `Send + Sync` (for example a UniFFI - interface type) - - Storing `Box` inside a global `Mutex` or `RwLock` - -**⚠ Warning ⚠**: this comes with a caveat: the methods of the foreign class must be safe to call -from multiple threads at once, for example because they are synchronized with a mutex, or use -thread-safe data structures. However, Rust can not enforce this in the foreign code. If you add -`Send + Sync` to your callback interface, you must make sure to inform your library consumers that -their implementations must logically be Send + Sync. - -## 2. Setup error handling - -All methods of the Rust trait should return a Result. The error half of that result must -be an [error type defined in the UDL](./errors.md). - -It's currently allowed for callback interface methods to return a regular value -rather than a `Result<>`. However, this is means that any exception from the -foreign bindings will lead to a panic. - -### Extra requirements for errors used in callback interfaces - -In order to support errors in callback interfaces, UniFFI must be able to -properly [lift the error](../internals/lifting_and_lowering.md). This means -that the if the error is described by an `enum` rather than an `interface` in -the UDL (see [Errors](./errors.md)) then all variants of the Rust enum must be unit variants. - -In addition to expected errors, a callback interface call can result in all kinds of -unexpected errors. Some examples are the foreign code throws an exception that's not part -of the exception type or there was a problem marshalling the data for the call. UniFFI -uses `uniffi::UnexpectedUniFFICallbackError` for these cases. Your code must include a -`From` impl for your error type to handle those or -the UniFFI scaffolding code will fail to compile. See `example/callbacks` for an -example of how to do this. - -## 3. Define a callback interface in the UDL +This document describes the differences from regular traits. +## Defining a callback +If you must define a callback in UDL it would look like: ```webidl callback interface Keychain { - [Throws=KeyChainError] - string? get(string key); - - [Throws=KeyChainError] - void put(string key, string data); -}; -``` - -## 4. And allow it to be passed into Rust - -Here, we define a constructor to pass the keychain to rust, and then another method -which may use it. - -In UDL: - -```webidl -interface Authenticator { - constructor(Keychain keychain); - void login(); + // as described in the foreign traits docs... }; ``` -In Rust: - -```rust,no_run -struct Authenticator { - keychain: Box, -} - -impl Authenticator { - pub fn new(keychain: Box) -> Self { - Self { keychain } - } - pub fn login(&self) { - let username = self.keychain.get("username".into()); - let password = self.keychain.get("password".into()); - } -} -``` - -## 5. Create an foreign language implementation of the callback interface - -In this example, here's a Kotlin implementation. - -```kotlin -class KotlinKeychain: Keychain { - override fun get(key: String): String? { - // … elide the implementation. - return value - } - override fun put(key: String) { - // … elide the implementation. - } -} -``` - -…and Swift: - -```swift -class SwiftKeychain: Keychain { - func get(key: String) -> String? { - // … elide the implementation. - return value - } - func put(key: String) { - // … elide the implementation. - } -} -``` - -Note: in Swift, this must be a `class`. - -## 6. Pass the implementation to Rust - -Again, in Kotlin - -```kt -val authenticator = Authenticator(KotlinKeychain()) -// later on: -authenticator.login() -``` - -and in Swift: +procmacros support it too, but just don't use it :) -```swift -let authenticator = Authenticator(SwiftKeychain()) -// later on: -authenticator.login() -``` +### Box and Send + Sync? -Care is taken to ensure that once `Box` is dropped in Rust, then it is cleaned up in the foreign language. +Traits defined purely for callbacks probably don't technically need to be `Sync` in Rust, but +they conceptually are, just outside of Rust's view. -Also note, that storing the `Box` in the `Authenticator` required that all implementations -*must* implement `Send`. +That is, the methods of the foreign class must be safe to call +from multiple threads at once, but Rust can not enforce this in the foreign code. -## ⚠️ Avoid callback interfaces cycles +## Rust signature differences -Callback interfaces can create cycles between Rust and foreign objects and lead to memory leaks. For example a callback -interface object holds a reference to a Rust object which also holds a reference to the same callback interface object. -Take care to avoid this by following guidelines: +Consider the examples in [Rust traits implemented by foreign languages](../foreign_traits). -1. Avoid references to UniFFI objects in callback objects, including direct references and transitive - references through intermediate objects. -2. Avoid references to callback objects from UniFFI objects, including direct references and transitive - references through intermediate objects. -3. If you need to break the first 2 guidelines, then take steps to manually break the cycles to avoid memory leaks. - Alternatively, ensure that the number of objects that can ever be created is bounded below some acceptable level. +If the traits in question are defined as a "callback" interface, the `Arc` types +would actually be `Box` - eg, the Rust implementation of the `Authenticator` +constructor would be ```fn new(keychain: Box) -> Self``` instead of the `Arc<>`. diff --git a/docs/manual/src/udl/ext_types_external.md b/docs/manual/src/udl/ext_types_external.md index 305e5f5eaa..5872a14553 100644 --- a/docs/manual/src/udl/ext_types_external.md +++ b/docs/manual/src/udl/ext_types_external.md @@ -29,6 +29,9 @@ dictionary ConsumingDict { ``` +If in the above example, `DemoDict` was actually "exported" via a procmacro +you should instead use `ExternExport` + (Note the special type `extern` used on the `typedef`. It is not currently enforced that the literal `extern` is used, but we hope to enforce this later, so please use it!) @@ -77,9 +80,6 @@ By default, UniFFI assumes that the Kotlin module name matches the Rust crate na rust-crate-name = "kotlin.package.name" ``` -See the [`ext-types` fixture](https://github.com/mozilla/uniffi-rs/blob/main/fixtures/ext-types/lib/uniffi.toml) -for an example - ### Swift For Swift, you must compile all generated `.swift` files together in a single diff --git a/docs/manual/src/udl/interfaces.md b/docs/manual/src/udl/interfaces.md index 474c82ef77..23db54a8d8 100644 --- a/docs/manual/src/udl/interfaces.md +++ b/docs/manual/src/udl/interfaces.md @@ -122,12 +122,28 @@ fn get_buttons() -> Vec> { ... } fn press(button: Arc) -> Arc { ... } ``` -See the ["traits" example](https://github.com/mozilla/uniffi-rs/tree/main/examples/traits) for more. +### Foreign implementations + +Traits can also be implemented on the foreign side passed into Rust, for example: + +```python +class PyButton(uniffi_module.Button): + def name(self): + return "PyButton" + +uniffi_module.press(PyButton()) +``` + +Note: This is currently supported on Python, Kotlin, and Swift. ### Traits construction Because any number of `struct`s may implement a trait, they don't have constructors. +### Traits example + +See the ["traits" example](https://github.com/mozilla/uniffi-rs/tree/main/examples/traits) for more. + ## Alternate Named Constructors In addition to the default constructor connected to the `::new()` method, you can specify @@ -169,6 +185,14 @@ struct TodoList { ... } ``` +(or using proc-macros) +```rust +#[derive(Debug, uniffi::Object)] +#[uniffi::export(Debug)] +struct TodoList { + ... +} +``` This will cause the Python bindings to generate a `__repr__` method that returns the value implemented by the `Debug` trait. Not all bindings support generating special methods, so they may be ignored. diff --git a/examples/callbacks/README.md b/examples/callbacks/README.md new file mode 100644 index 0000000000..6c73606540 --- /dev/null +++ b/examples/callbacks/README.md @@ -0,0 +1,5 @@ +This is an example of UniFFI "callbacks". + +Callbacks are the ability to implement Rust traits in foreign languges. These are defined by +[normal UniFFI traits](../../docs/manual/src/foreign_traits) or via ["callback" definitions](../../docs/manual/src/udl/callback_interfaces). + diff --git a/examples/callbacks/src/callbacks.udl b/examples/callbacks/src/callbacks.udl index e25be0872c..00cc1e62b3 100644 --- a/examples/callbacks/src/callbacks.udl +++ b/examples/callbacks/src/callbacks.udl @@ -1,4 +1,12 @@ -namespace callbacks {}; +namespace callbacks { + // `get_sim_cards` is defined via a procmacro. +}; + +// This trait is implemented in Rust and in foreign bindings. +[Trait] +interface SimCard { + string name(); // The name of the carrier/provider. +}; [Error] enum TelephoneError { @@ -9,9 +17,11 @@ enum TelephoneError { interface Telephone { constructor(); [Throws=TelephoneError] - string call(CallAnswerer answerer); + string call(SimCard sim, CallAnswerer answerer); }; +// callback interfaces are discouraged now that foreign code can +// implement traits, but here's one! callback interface CallAnswerer { [Throws=TelephoneError] string answer(); diff --git a/examples/callbacks/src/lib.rs b/examples/callbacks/src/lib.rs index 672e5bea9f..dac5653d1b 100644 --- a/examples/callbacks/src/lib.rs +++ b/examples/callbacks/src/lib.rs @@ -2,6 +2,8 @@ * 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::sync::Arc; + #[derive(Debug, thiserror::Error)] pub enum TelephoneError { #[error("Busy")] @@ -18,6 +20,25 @@ impl From for TelephoneError { } } +// SIM cards. +pub trait SimCard: Send + Sync { + fn name(&self) -> String; +} + +struct RustySim; +impl SimCard for RustySim { + fn name(&self) -> String { + "rusty!".to_string() + } +} + +// namespace functions. +#[uniffi::export] +fn get_sim_cards() -> Vec> { + vec![Arc::new(RustySim {})] +} + +// The call-answer, callback interface. pub trait CallAnswerer { fn answer(&self) -> Result; } @@ -26,11 +47,45 @@ pub trait CallAnswerer { pub struct Telephone; impl Telephone { pub fn new() -> Self { - Self::default() + Self {} + } + + pub fn call( + &self, + // Traits are Arc<>, callbacks Box<>. + sim: Arc, + answerer: Box, + ) -> Result { + if sim.name() != "rusty!" { + Ok(format!("{} est bon marché", sim.name())) + } else { + answerer.answer() + } + } +} + +// Some proc-macro definitions of the same thing. +#[derive(uniffi::Object, Debug, Default, Clone)] +pub struct FancyTelephone; + +#[uniffi::export] +impl FancyTelephone { + #[uniffi::constructor] + pub fn new() -> Arc { + Arc::new(Self) } - pub fn call(&self, answerer: Box) -> Result { - answerer.answer() + // Exact same signature as the UDL version. + pub fn call( + &self, + sim: Arc, + answerer: Box, + ) -> Result { + if sim.name() != "rusty!" { + Ok(format!("{} est bon marché", sim.name())) + } else { + answerer.answer() + } } } diff --git a/examples/callbacks/tests/bindings/test_callbacks.kts b/examples/callbacks/tests/bindings/test_callbacks.kts index 38ddd39f09..9239bc6d9f 100644 --- a/examples/callbacks/tests/bindings/test_callbacks.kts +++ b/examples/callbacks/tests/bindings/test_callbacks.kts @@ -22,18 +22,27 @@ class CallAnswererImpl(val mode: String) : CallAnswerer { } val telephone = Telephone() +val sim = getSimCards()[0] -assert(telephone.call(CallAnswererImpl("normal")) == "Bonjour") +assert(telephone.call(sim, CallAnswererImpl("normal")) == "Bonjour") + +// Our own sim. +class Sim() : SimCard { + override fun name(): String { + return "kotlin" + } +} +assert(telephone.call(Sim(), CallAnswererImpl("normal")) == "kotlin est bon marché") try { - telephone.call(CallAnswererImpl("busy")) + telephone.call(sim, CallAnswererImpl("busy")) throw RuntimeException("Should have thrown a Busy exception!") } catch(e: TelephoneException.Busy) { // It's okay } try { - telephone.call(CallAnswererImpl("something-else")) + telephone.call(sim, CallAnswererImpl("something-else")) throw RuntimeException("Should have thrown an internal exception!") } catch(e: TelephoneException.InternalTelephoneException) { // It's okay diff --git a/examples/callbacks/tests/bindings/test_callbacks.py b/examples/callbacks/tests/bindings/test_callbacks.py index 4e20b3f8f0..ab00615749 100644 --- a/examples/callbacks/tests/bindings/test_callbacks.py +++ b/examples/callbacks/tests/bindings/test_callbacks.py @@ -1,6 +1,8 @@ import unittest from callbacks import * +# This is defined in UDL as a "callback". It's not possible to have a Rust +# implementation of a callback, they only exist on the foreign side. class CallAnswererImpl(CallAnswerer): def __init__(self, mode): self.mode = mode @@ -13,22 +15,39 @@ def answer(self): else: raise ValueError("Testing an unexpected error") +# This is a normal Rust trait - very much like a callback but can be implemented +# in Rust or in foreign code and is generally more consistent with the uniffi +# Arc<>-based object model. +class DiscountSim(SimCard): + def name(self): + return "python" + class CallbacksTest(unittest.TestCase): + TelephoneImpl = Telephone def test_answer(self): cb_object = CallAnswererImpl("ready") - telephone = Telephone() - self.assertEqual("Bonjour", telephone.call(cb_object)) + telephone = self.TelephoneImpl() + self.assertEqual("Bonjour", telephone.call(get_sim_cards()[0], cb_object)) def test_busy(self): cb_object = CallAnswererImpl("busy") - telephone = Telephone() + telephone = self.TelephoneImpl() with self.assertRaises(TelephoneError.Busy): - telephone.call(cb_object) + telephone.call(get_sim_cards()[0], cb_object) def test_unexpected_error(self): cb_object = CallAnswererImpl("something-else") - telephone = Telephone() + telephone = self.TelephoneImpl() with self.assertRaises(TelephoneError.InternalTelephoneError): - telephone.call(cb_object) + telephone.call(get_sim_cards()[0], cb_object) + + def test_sims(self): + cb_object = CallAnswererImpl("ready") + telephone = self.TelephoneImpl() + sim = DiscountSim() + self.assertEqual("python est bon marché", telephone.call(sim, cb_object)) + +class FancyCallbacksTest(CallbacksTest): + TelephoneImpl = FancyTelephone unittest.main() diff --git a/examples/callbacks/tests/bindings/test_callbacks.swift b/examples/callbacks/tests/bindings/test_callbacks.swift index b65449aaf2..42572b5c99 100644 --- a/examples/callbacks/tests/bindings/test_callbacks.swift +++ b/examples/callbacks/tests/bindings/test_callbacks.swift @@ -33,17 +33,28 @@ class OnCallAnsweredImpl : CallAnswerer { } let telephone = Telephone() +let sim = getSimCards()[0]; -assert(try! telephone.call(answerer: OnCallAnsweredImpl(withMode: "ready")) == "Bonjour") +assert(try! telephone.call(sim: sim, answerer: OnCallAnsweredImpl(withMode: "ready")) == "Bonjour") +// We can implement our own sim cards. +class Sim : SimCard { + func name() -> String { + return "swift" + } +} + +assert(try! telephone.call(sim: Sim(), answerer: OnCallAnsweredImpl(withMode: "ready")) == "swift est bon marché") + +// Error cases. do { - _ = try telephone.call(answerer: OnCallAnsweredImpl(withMode: "busy")) + _ = try telephone.call(sim: sim, answerer: OnCallAnsweredImpl(withMode: "busy")) } catch TelephoneError.Busy { // Expected error } do { - _ = try telephone.call(answerer: OnCallAnsweredImpl(withMode: "unexpected-value")) + _ = try telephone.call(sim: sim, answerer: OnCallAnsweredImpl(withMode: "unexpected-value")) } catch TelephoneError.InternalTelephoneError { // Expected error } diff --git a/examples/custom-types/src/lib.rs b/examples/custom-types/src/lib.rs index 6a709af891..55c502cc5a 100644 --- a/examples/custom-types/src/lib.rs +++ b/examples/custom-types/src/lib.rs @@ -1,5 +1,10 @@ use url::Url; +// A custom guid defined via a proc-macro (ie, not referenced in the UDL) +// By far the easiest way to define custom types. +pub struct ExampleCustomType(String); +uniffi::custom_newtype!(ExampleCustomType, String); + // Custom Handle type which trivially wraps an i64. pub struct Handle(pub i64); @@ -108,4 +113,9 @@ pub fn get_custom_types_demo(v: Option) -> CustomTypesDemo { }) } +#[uniffi::export] +pub fn get_example_custom_type() -> ExampleCustomType { + ExampleCustomType("abadidea".to_string()) +} + uniffi::include_scaffolding!("custom-types"); diff --git a/examples/fxa-client/README.md b/examples/fxa-client/README.md new file mode 100644 index 0000000000..12f792bc31 --- /dev/null +++ b/examples/fxa-client/README.md @@ -0,0 +1,4 @@ +# Firefox Account Example. + +An old version of the UDL for a Firefox Account, as the very first UniFFI consumer. +See also [the current implementation](https://github.com/mozilla/application-services/tree/main/components/fxa-client) diff --git a/examples/traits/tests/bindings/test_traits.kts b/examples/traits/tests/bindings/test_traits.kts new file mode 100644 index 0000000000..592073b204 --- /dev/null +++ b/examples/traits/tests/bindings/test_traits.kts @@ -0,0 +1,16 @@ +import uniffi.traits.* + +for (button in getButtons()) { + val name = button.name() + // Check that the name is one of the expected values + assert(name in listOf("go", "stop")) + // Check that we can round-trip the button through Rust + assert(press(button).name() == name) +} + +// Test a button implemented in Kotlin +class KtButton : Button { + override fun name() = "KtButton" +} + +assert(press(KtButton()).name() == "KtButton") diff --git a/examples/traits/tests/bindings/test_traits.py b/examples/traits/tests/bindings/test_traits.py index fff64de7d5..70cf2c0b81 100644 --- a/examples/traits/tests/bindings/test_traits.py +++ b/examples/traits/tests/bindings/test_traits.py @@ -1,7 +1,15 @@ from traits import * for button in get_buttons(): - if button.name() in ["go", "stop"]: - press(button) - else: - print("unknown button", button) + name = button.name() + # Check that the name is one of the expected values + assert(name in ["go", "stop"]) + # Check that we can round-trip the button through Rust + assert(press(button).name() == name) + +# Test a button implemented in Python +class PyButton(Button): + def name(self): + return "PyButton" + +assert(press(PyButton()).name() == "PyButton") diff --git a/examples/traits/tests/bindings/test_traits.swift b/examples/traits/tests/bindings/test_traits.swift new file mode 100644 index 0000000000..868bb9449c --- /dev/null +++ b/examples/traits/tests/bindings/test_traits.swift @@ -0,0 +1,18 @@ +import traits + +for button in getButtons() { + let name = button.name() + // Check that the name is one of the expected values + assert(["go", "stop"].contains(name)) + // Check that we can round-trip the button through Rust + assert(press(button: button).name() == name) +} + +// Test a Button implemented in Swift +class SwiftButton: Button { + func name() -> String { + return "SwiftButton" + } +} + +assert(press(button: SwiftButton()).name() == "SwiftButton") diff --git a/examples/traits/tests/test_generated_bindings.rs b/examples/traits/tests/test_generated_bindings.rs index 33d4998351..fc6411434b 100644 --- a/examples/traits/tests/test_generated_bindings.rs +++ b/examples/traits/tests/test_generated_bindings.rs @@ -1 +1,5 @@ -uniffi::build_foreign_language_testcases!("tests/bindings/test_traits.py",); +uniffi::build_foreign_language_testcases!( + "tests/bindings/test_traits.py", + "tests/bindings/test_traits.kts", + "tests/bindings/test_traits.swift", +); diff --git a/fixtures/coverall/src/coverall.udl b/fixtures/coverall/src/coverall.udl index 2c0d9550d6..6b1c2d304b 100644 --- a/fixtures/coverall/src/coverall.udl +++ b/fixtures/coverall/src/coverall.udl @@ -4,7 +4,7 @@ namespace coverall { u64 get_num_alive(); - sequence get_traits(); + sequence get_traits(); MaybeSimpleDict get_maybe_simple_dict(i8 index); @@ -17,6 +17,11 @@ namespace coverall { [Throws=CoverallRichErrorNoVariantData] void throw_rich_error_no_variant_data(); + + Getters make_rust_getters(); + void test_getters(Getters g); + + sequence ancestor_names(NodeTrait node); }; dictionary SimpleDict { @@ -41,7 +46,7 @@ dictionary SimpleDict { double float64; double? maybe_float64; Coveralls? coveralls; - TestTrait? test_trait; + NodeTrait? test_trait; }; dictionary DictWithDefaults { @@ -190,24 +195,37 @@ interface ThreadsafeCounter { i32 increment_if_busy(); }; -// This is a trait implemented on the Rust side. +// Test trait #1 +// +// The goal here is to test all possible arg, return, and error types. [Trait] -interface TestTrait { - string name(); // The name of the implementation +interface Getters { + boolean get_bool(boolean v, boolean arg2); + [Throws=CoverallError] + string get_string(string v, boolean arg2); + [Throws=ComplexError] + string? get_option(string v, boolean arg2); + sequence get_list(sequence v, boolean arg2); + void get_nothing(string v); +}; - [Self=ByArc] - u64 number(); +// Test trait #2 +// +// The goal here is test passing objects back and forth between Rust and the foreign side +[Trait] +interface NodeTrait { + string name(); // The name of the this node + + /// Takes an `Arc` and stores it our parent node, dropping any existing / reference. Note + //you can create circular references with this. + void set_parent(NodeTrait? parent); + + /// Returns what was previously set via `set_parent()`, or null. + NodeTrait? get_parent(); /// Calls `Arc::strong_count()` on the `Arc` containing `self`. [Self=ByArc] u64 strong_count(); - - /// Takes an `Arc` and stores it in `self`, dropping the existing - /// reference. Note you can create circular references by passing `self`. - void take_other(TestTrait? other); - - /// Returns what was previously set via `take_other()`, or null. - TestTrait? get_other(); }; // Forward/backward declarations are fine in UDL. diff --git a/fixtures/coverall/src/lib.rs b/fixtures/coverall/src/lib.rs index e591c0db79..82b7236cc9 100644 --- a/fixtures/coverall/src/lib.rs +++ b/fixtures/coverall/src/lib.rs @@ -10,11 +10,11 @@ use std::time::SystemTime; use once_cell::sync::Lazy; mod traits; -pub use traits::{get_traits, TestTrait}; +pub use traits::{ancestor_names, get_traits, make_rust_getters, test_getters, Getters, NodeTrait}; static NUM_ALIVE: Lazy> = Lazy::new(|| RwLock::new(0)); -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, PartialEq, Eq)] pub enum CoverallError { #[error("The coverall has too many holes")] TooManyHoles, @@ -80,7 +80,7 @@ impl From for CoverallError { } } -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, PartialEq, Eq)] pub enum ComplexError { #[error("OsError: {code} ({extended_code})")] OsError { code: i16, extended_code: i16 }, @@ -131,7 +131,7 @@ pub struct SimpleDict { float64: f64, maybe_float64: Option, coveralls: Option>, - test_trait: Option>, + test_trait: Option>, } #[derive(Debug, Clone)] @@ -204,7 +204,7 @@ fn create_some_dict() -> SimpleDict { float64: 0.0, maybe_float64: Some(1.0), coveralls: Some(Arc::new(Coveralls::new("some_dict".to_string()))), - test_trait: Some(Arc::new(traits::Trait2 {})), + test_trait: Some(Arc::new(traits::Trait2::default())), } } diff --git a/fixtures/coverall/src/traits.rs b/fixtures/coverall/src/traits.rs index fe9694ef0e..15785ef0c6 100644 --- a/fixtures/coverall/src/traits.rs +++ b/fixtures/coverall/src/traits.rs @@ -2,73 +2,187 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +use super::{ComplexError, CoverallError}; use std::sync::{Arc, Mutex}; // namespace functions. -pub fn get_traits() -> Vec> { - vec![ - Arc::new(Trait1 { - ..Default::default() - }), - Arc::new(Trait2 {}), - ] +pub fn get_traits() -> Vec> { + vec![Arc::new(Trait1::default()), Arc::new(Trait2::default())] } -pub trait TestTrait: Send + Sync + std::fmt::Debug { +pub trait NodeTrait: Send + Sync + std::fmt::Debug { fn name(&self) -> String; - fn number(self: Arc) -> u64; + fn set_parent(&self, parent: Option>); + + fn get_parent(&self) -> Option>; fn strong_count(self: Arc) -> u64 { Arc::strong_count(&self) as u64 } +} - fn take_other(&self, other: Option>); +pub fn ancestor_names(node: Arc) -> Vec { + let mut names = vec![]; + let mut parent = node.get_parent(); + while let Some(node) = parent { + names.push(node.name()); + parent = node.get_parent(); + } + names +} - fn get_other(&self) -> Option>; +/// Test trait +/// +/// The goal here is to test all possible arg, return, and error types. +pub trait Getters: Send + Sync { + fn get_bool(&self, v: bool, arg2: bool) -> bool; + fn get_string(&self, v: String, arg2: bool) -> Result; + fn get_option(&self, v: String, arg2: bool) -> Result, ComplexError>; + fn get_list(&self, v: Vec, arg2: bool) -> Vec; + fn get_nothing(&self, v: String); +} + +struct RustGetters; + +impl Getters for RustGetters { + fn get_bool(&self, v: bool, arg2: bool) -> bool { + v ^ arg2 + } + + fn get_string(&self, v: String, arg2: bool) -> Result { + if v == "too-many-holes" { + Err(CoverallError::TooManyHoles) + } else if v == "unexpected-error" { + panic!("unexpected error") + } else if arg2 { + Ok(v.to_uppercase()) + } else { + Ok(v) + } + } + + fn get_option(&self, v: String, arg2: bool) -> Result, ComplexError> { + if v == "os-error" { + Err(ComplexError::OsError { + code: 100, + extended_code: 200, + }) + } else if v == "unknown-error" { + Err(ComplexError::UnknownError) + } else if arg2 { + if !v.is_empty() { + Ok(Some(v.to_uppercase())) + } else { + Ok(None) + } + } else { + Ok(Some(v)) + } + } + + fn get_list(&self, v: Vec, arg2: bool) -> Vec { + if arg2 { + v + } else { + vec![] + } + } + + fn get_nothing(&self, _v: String) {} +} + +pub fn make_rust_getters() -> Arc { + Arc::new(RustGetters) +} + +pub fn test_getters(getters: Arc) { + assert!(!getters.get_bool(true, true)); + assert!(getters.get_bool(true, false)); + assert!(getters.get_bool(false, true)); + assert!(!getters.get_bool(false, false)); + + assert_eq!( + getters.get_string("hello".to_owned(), false).unwrap(), + "hello" + ); + assert_eq!( + getters.get_string("hello".to_owned(), true).unwrap(), + "HELLO" + ); + + assert_eq!( + getters.get_option("hello".to_owned(), true).unwrap(), + Some("HELLO".to_owned()) + ); + assert_eq!( + getters.get_option("hello".to_owned(), false).unwrap(), + Some("hello".to_owned()) + ); + assert_eq!(getters.get_option("".to_owned(), true).unwrap(), None); + + assert_eq!(getters.get_list(vec![1, 2, 3], true), vec![1, 2, 3]); + assert_eq!(getters.get_list(vec![1, 2, 3], false), Vec::::new()); + + // Call get_nothing to make sure it doesn't panic. There's no point in checking the output + // though + getters.get_nothing("hello".to_owned()); + + assert_eq!( + getters.get_string("too-many-holes".to_owned(), true), + Err(CoverallError::TooManyHoles) + ); + assert_eq!( + getters.get_option("os-error".to_owned(), true), + Err(ComplexError::OsError { + code: 100, + extended_code: 200 + }) + ); + assert_eq!( + getters.get_option("unknown-error".to_owned(), true), + Err(ComplexError::UnknownError) + ); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + getters.get_string("unexpected-error".to_owned(), true) + })); + assert!(result.is_err()); } #[derive(Debug, Default)] pub(crate) struct Trait1 { // A reference to another trait. - other: Mutex>>, + parent: Mutex>>, } -impl TestTrait for Trait1 { +impl NodeTrait for Trait1 { fn name(&self) -> String { - "trait 1".to_string() + "node-1".to_string() } - fn number(self: Arc) -> u64 { - 1_u64 + fn set_parent(&self, parent: Option>) { + *self.parent.lock().unwrap() = parent.map(|arc| Arc::clone(&arc)) } - fn take_other(&self, other: Option>) { - *self.other.lock().unwrap() = other.map(|arc| Arc::clone(&arc)) - } - - fn get_other(&self) -> Option> { - (*self.other.lock().unwrap()).as_ref().map(Arc::clone) + fn get_parent(&self) -> Option> { + (*self.parent.lock().unwrap()).as_ref().map(Arc::clone) } } -#[derive(Debug)] -pub(crate) struct Trait2 {} -impl TestTrait for Trait2 { +#[derive(Debug, Default)] +pub(crate) struct Trait2 { + parent: Mutex>>, +} +impl NodeTrait for Trait2 { fn name(&self) -> String { - "trait 2".to_string() - } - - fn number(self: Arc) -> u64 { - 2_u64 + "node-2".to_string() } - // Don't bother implementing these here - the test on the struct above is ok. - fn take_other(&self, _other: Option>) { - unimplemented!(); + fn set_parent(&self, parent: Option>) { + *self.parent.lock().unwrap() = parent.map(|arc| Arc::clone(&arc)) } - fn get_other(&self) -> Option> { - unimplemented!() + fn get_parent(&self) -> Option> { + (*self.parent.lock().unwrap()).as_ref().map(Arc::clone) } } diff --git a/fixtures/coverall/tests/bindings/test_coverall.kts b/fixtures/coverall/tests/bindings/test_coverall.kts index 62fbbb5a70..8bf3b0077b 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.kts +++ b/fixtures/coverall/tests/bindings/test_coverall.kts @@ -214,6 +214,164 @@ Coveralls("test_regressions").use { coveralls -> assert(coveralls.getStatus("success") == "status: success") } +class KotlinGetters : Getters { + override fun getBool(v: Boolean, arg2: Boolean) : Boolean { + return v != arg2 + } + + override fun getString(v: String, arg2: Boolean) : String { + if (v == "too-many-holes") { + throw CoverallException.TooManyHoles("too many holes") + } else if (v == "unexpected-error") { + throw RuntimeException("unexpected error") + } else if (arg2) { + return v.uppercase() + } else { + return v + } + } + + override fun getOption(v: String, arg2: Boolean) : String? { + if (v == "os-error") { + throw ComplexException.OsException(100, 200) + } else if (v == "unknown-error") { + throw ComplexException.UnknownException() + } else if (arg2) { + if (!v.isEmpty()) { + return v.uppercase() + } else { + return null + } + } else { + return v + } + } + + override fun getList(v: List, arg2: Boolean) : List { + if (arg2) { + return v + } else { + return listOf() + } + } + + @Suppress("UNUSED_PARAMETER") + override fun getNothing(v: String) = Unit +} + +// Test traits implemented in Rust +makeRustGetters().let { rustGetters -> + testGetters(rustGetters) + testGettersFromKotlin(rustGetters) +} + +// Test traits implemented in Kotlin +KotlinGetters().let { kotlinGetters -> + testGetters(kotlinGetters) + testGettersFromKotlin(kotlinGetters) +} + +fun testGettersFromKotlin(getters: Getters) { + assert(getters.getBool(true, true) == false); + assert(getters.getBool(true, false) == true); + assert(getters.getBool(false, true) == true); + assert(getters.getBool(false, false) == false); + + assert(getters.getString("hello", false) == "hello"); + assert(getters.getString("hello", true) == "HELLO"); + + assert(getters.getOption("hello", true) == "HELLO"); + assert(getters.getOption("hello", false) == "hello"); + assert(getters.getOption("", true) == null); + + assert(getters.getList(listOf(1, 2, 3), true) == listOf(1, 2, 3)) + assert(getters.getList(listOf(1, 2, 3), false) == listOf()) + + assert(getters.getNothing("hello") == Unit); + + try { + getters.getString("too-many-holes", true) + throw RuntimeException("Expected method to throw exception") + } catch(e: CoverallException.TooManyHoles) { + // Expected + } + + try { + getters.getOption("os-error", true) + throw RuntimeException("Expected method to throw exception") + } catch(e: ComplexException.OsException) { + assert(e.code.toInt() == 100) + assert(e.extendedCode.toInt() == 200) + } + + try { + getters.getOption("unknown-error", true) + throw RuntimeException("Expected method to throw exception") + } catch(e: ComplexException.UnknownException) { + // Expected + } + + try { + getters.getString("unexpected-error", true) + } catch(e: Exception) { + // Expected + } +} + +class KotlinNode() : NodeTrait { + var currentParent: NodeTrait? = null + + override fun name() = "node-kt" + + override fun setParent(parent: NodeTrait?) { + currentParent = parent + } + + override fun getParent() = currentParent + + override fun strongCount() : ULong { + return 0.toULong() // TODO + } +} + +// Test NodeTrait +getTraits().let { traits -> + assert(traits[0].name() == "node-1") + // Note: strong counts are 1 more than you might expect, because the strongCount() method + // holds a strong ref. + assert(traits[0].strongCount() == 2UL) + + assert(traits[1].name() == "node-2") + assert(traits[1].strongCount() == 2UL) + + // Note: this doesn't increase the Rust strong count, since we wrap the Rust impl with a + // Swift impl before passing it to `setParent()` + traits[0].setParent(traits[1]) + assert(ancestorNames(traits[0]) == listOf("node-2")) + assert(ancestorNames(traits[1]).isEmpty()) + assert(traits[1].strongCount() == 2UL) + assert(traits[0].getParent()!!.name() == "node-2") + + val ktNode = KotlinNode() + traits[1].setParent(ktNode) + assert(ancestorNames(traits[0]) == listOf("node-2", "node-kt")) + assert(ancestorNames(traits[1]) == listOf("node-kt")) + assert(ancestorNames(ktNode) == listOf()) + + traits[1].setParent(null) + ktNode.setParent(traits[0]) + assert(ancestorNames(ktNode) == listOf("node-1", "node-2")) + assert(ancestorNames(traits[0]) == listOf("node-2")) + assert(ancestorNames(traits[1]) == listOf()) + + // Unset everything and check that we don't get a memory error + ktNode.setParent(null) + traits[0].setParent(null) + + // FIXME: We should be calling `NodeTraitImpl.close()` to release the Rust pointer, however that's + // not possible through the `NodeTrait` interface (see #1787). +} + // This tests that the UniFFI-generated scaffolding doesn't introduce any unexpected locking. // We have one thread busy-wait for a some period of time, while a second thread repeatedly // increments the counter and then checks if the object is still busy. The second thread should diff --git a/fixtures/coverall/tests/bindings/test_coverall.py b/fixtures/coverall/tests/bindings/test_coverall.py index a63a2e80b9..17593bc833 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.py +++ b/fixtures/coverall/tests/bindings/test_coverall.py @@ -31,7 +31,7 @@ def test_some_dict(self): self.assertEqual(d.signed64, 9223372036854775807) self.assertEqual(d.maybe_signed64, 0) self.assertEqual(d.coveralls.get_name(), "some_dict") - self.assertEqual(d.test_trait.name(), "trait 2") + self.assertEqual(d.test_trait.name(), "node-2") # floats should be "close enough" - although it's mildly surprising that # we need to specify `places=6` whereas the default is 7. @@ -278,21 +278,140 @@ def test_bytes(self): coveralls = Coveralls("test_bytes") self.assertEqual(coveralls.reverse(b"123"), b"321") +class PyGetters: + def get_bool(self, v, arg2): + return v ^ arg2 + + def get_string(self, v, arg2): + if v == "too-many-holes": + raise CoverallError.TooManyHoles + elif v == "unexpected-error": + raise RuntimeError("unexpected error") + elif arg2: + return v.upper() + else: + return v + + def get_option(self, v, arg2): + if v == "os-error": + raise ComplexError.OsError(100, 200) + elif v == "unknown-error": + raise ComplexError.UnknownError + elif arg2: + if v: + return v.upper() + else: + return None + else: + return v + + def get_list(self, v, arg2): + if arg2: + return v + else: + return [] + + def get_nothing(self, _v): + return None + +class PyNode: + def __init__(self): + self.parent = None + + def name(self): + return "node-py" + + def set_parent(self, parent): + self.parent = parent + + def get_parent(self): + return self.parent + + def strong_count(self): + return 0 # TODO + class TraitsTest(unittest.TestCase): - def test_simple(self): + # Test traits implemented in Rust + # def test_rust_getters(self): + # test_getters(None) + # self.check_getters_from_python(make_rust_getters()) + + # Test traits implemented in Rust + def test_python_getters(self): + test_getters(PyGetters()) + #self.check_getters_from_python(PyGetters()) + + def check_getters_from_python(self, getters): + self.assertEqual(getters.get_bool(True, True), False); + self.assertEqual(getters.get_bool(True, False), True); + self.assertEqual(getters.get_bool(False, True), True); + self.assertEqual(getters.get_bool(False, False), False); + + self.assertEqual(getters.get_string("hello", False), "hello"); + self.assertEqual(getters.get_string("hello", True), "HELLO"); + + self.assertEqual(getters.get_option("hello", True), "HELLO"); + self.assertEqual(getters.get_option("hello", False), "hello"); + self.assertEqual(getters.get_option("", True), None); + + self.assertEqual(getters.get_list([1, 2, 3], True), [1, 2, 3]); + self.assertEqual(getters.get_list([1, 2, 3], False), []) + + self.assertEqual(getters.get_nothing("hello"), None); + + with self.assertRaises(CoverallError.TooManyHoles): + getters.get_string("too-many-holes", True) + + with self.assertRaises(ComplexError.OsError) as cm: + getters.get_option("os-error", True) + self.assertEqual(cm.exception.code, 100) + self.assertEqual(cm.exception.extended_code, 200) + + with self.assertRaises(ComplexError.UnknownError): + getters.get_option("unknown-error", True) + + with self.assertRaises(InternalError): + getters.get_string("unexpected-error", True) + + def test_path(self): + # Get traits creates 2 objects that implement the trait traits = get_traits() - self.assertEqual(traits[0].name(), "trait 1") - self.assertEqual(traits[0].number(), 1) + self.assertEqual(traits[0].name(), "node-1") + # Note: strong counts are 1 more than you might expect, because the strong_count() method + # holds a strong ref. self.assertEqual(traits[0].strong_count(), 2) - self.assertEqual(traits[1].name(), "trait 2") - self.assertEqual(traits[1].number(), 2) + self.assertEqual(traits[1].name(), "node-2") self.assertEqual(traits[1].strong_count(), 2) - traits[0].take_other(traits[1]) - self.assertEqual(traits[1].strong_count(), 3) - self.assertEqual(traits[0].get_other().name(), "trait 2") - traits[0].take_other(None) + # Let's try connecting them together + traits[0].set_parent(traits[1]) + # Note: this doesn't increase the Rust strong count, since we wrap the Rust impl with a + # python impl before passing it to `set_parent()` + self.assertEqual(traits[1].strong_count(), 2) + self.assertEqual(ancestor_names(traits[0]), ["node-2"]) + self.assertEqual(ancestor_names(traits[1]), []) + self.assertEqual(traits[0].get_parent().name(), "node-2") + + # Throw in a Python implementation of the trait + # The ancestry chain now goes traits[0] -> traits[1] -> py_node + py_node = PyNode() + traits[1].set_parent(py_node) + self.assertEqual(ancestor_names(traits[0]), ["node-2", "node-py"]) + self.assertEqual(ancestor_names(traits[1]), ["node-py"]) + self.assertEqual(ancestor_names(py_node), []) + + # Rotating things. + # The ancestry chain now goes py_node -> traits[0] -> traits[1] + traits[1].set_parent(None) + py_node.set_parent(traits[0]) + self.assertEqual(ancestor_names(py_node), ["node-1", "node-2"]) + self.assertEqual(ancestor_names(traits[0]), ["node-2"]) + self.assertEqual(ancestor_names(traits[1]), []) + + # Make sure we don't crash when undoing it all + py_node.set_parent(None) + traits[0].set_parent(None) if __name__=='__main__': unittest.main() diff --git a/fixtures/coverall/tests/bindings/test_coverall.swift b/fixtures/coverall/tests/bindings/test_coverall.swift index d380881184..c6fcba4290 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.swift +++ b/fixtures/coverall/tests/bindings/test_coverall.swift @@ -247,3 +247,168 @@ do { let coveralls = Coveralls(name: "test_bytes") assert(coveralls.reverse(value: Data("123".utf8)) == Data("321".utf8)) } + + +struct SomeOtherError: Error { } + + +class SwiftGetters: Getters { + func getBool(v: Bool, arg2: Bool) -> Bool { v != arg2 } + func getString(v: String, arg2: Bool) throws -> String { + if v == "too-many-holes" { + throw CoverallError.TooManyHoles(message: "Too many") + } + if v == "unexpected-error" { + throw SomeOtherError() + } + return arg2 ? "HELLO" : v + } + func getOption(v: String, arg2: Bool) throws -> String? { + if v == "os-error" { + throw ComplexError.OsError(code: 100, extendedCode: 200) + } + if v == "unknown-error" { + throw ComplexError.UnknownError + } + if arg2 { + if !v.isEmpty { + return v.uppercased() + } else { + return nil + } + } else { + return v + } + } + func getList(v: [Int32], arg2: Bool) -> [Int32] { arg2 ? v : [] } + func getNothing(v: String) -> () { + } +} + + +// Test traits implemented in Rust +do { + let getters = makeRustGetters() + testGetters(g: getters) + testGettersFromSwift(getters: getters) +} + +// Test traits implemented in Swift +do { + let getters = SwiftGetters() + testGetters(g: getters) + testGettersFromSwift(getters: getters) +} + +func testGettersFromSwift(getters: Getters) { + assert(getters.getBool(v: true, arg2: true) == false); + assert(getters.getBool(v: true, arg2: false) == true); + assert(getters.getBool(v: false, arg2: true) == true); + assert(getters.getBool(v: false, arg2: false) == false); + + assert(try! getters.getString(v: "hello", arg2: false) == "hello"); + assert(try! getters.getString(v: "hello", arg2: true) == "HELLO"); + + assert(try! getters.getOption(v: "hello", arg2: true) == "HELLO"); + assert(try! getters.getOption(v: "hello", arg2: false) == "hello"); + assert(try! getters.getOption(v: "", arg2: true) == nil); + + assert(getters.getList(v: [1, 2, 3], arg2: true) == [1, 2, 3]) + assert(getters.getList(v: [1, 2, 3], arg2: false) == []) + + assert(getters.getNothing(v: "hello") == ()); + + do { + let _ = try getters.getString(v: "too-many-holes", arg2: true) + fatalError("should have thrown") + } catch CoverallError.TooManyHoles { + // Expected + } catch { + fatalError("Unexpected error: \(error)") + } + + do { + let _ = try getters.getOption(v: "os-error", arg2: true) + fatalError("should have thrown") + } catch ComplexError.OsError(let code, let extendedCode) { + assert(code == 100) + assert(extendedCode == 200) + } catch { + fatalError("Unexpected error: \(error)") + } + + do { + let _ = try getters.getOption(v: "unknown-error", arg2: true) + fatalError("should have thrown") + } catch ComplexError.UnknownError { + // Expected + } catch { + fatalError("Unexpected error: \(error)") + } + + do { + let _ = try getters.getString(v: "unexpected-error", arg2: true) + } catch { + // Expected + } +} + +class SwiftNode: NodeTrait { + var p: NodeTrait? = nil + + func name() -> String { + return "node-swift" + } + + func setParent(parent: NodeTrait?) { + self.p = parent + } + + func getParent() -> NodeTrait? { + return self.p + } + + func strongCount() -> UInt64 { + return 0 // TODO + } +} + +// Test Node trait +do { + let traits = getTraits() + assert(traits[0].name() == "node-1") + // Note: strong counts are 1 more than you might expect, because the strongCount() method + // holds a strong ref. + assert(traits[0].strongCount() == 2) + + assert(traits[1].name() == "node-2") + assert(traits[1].strongCount() == 2) + + // Note: this doesn't increase the Rust strong count, since we wrap the Rust impl with a + // Swift impl before passing it to `set_parent()` + traits[0].setParent(parent: traits[1]) + assert(ancestorNames(node: traits[0]) == ["node-2"]) + assert(ancestorNames(node: traits[1]) == []) + assert(traits[1].strongCount() == 2) + assert(traits[0].getParent()!.name() == "node-2") + + // Throw in a Swift implementation of the trait + // The ancestry chain now goes traits[0] -> traits[1] -> swiftNode + let swiftNode = SwiftNode() + traits[1].setParent(parent: swiftNode) + assert(ancestorNames(node: traits[0]) == ["node-2", "node-swift"]) + assert(ancestorNames(node: traits[1]) == ["node-swift"]) + assert(ancestorNames(node: swiftNode) == []) + + // Rotating things. + // The ancestry chain now goes swiftNode -> traits[0] -> traits[1] + traits[1].setParent(parent: nil) + swiftNode.setParent(parent: traits[0]) + assert(ancestorNames(node: swiftNode) == ["node-1", "node-2"]) + assert(ancestorNames(node: traits[0]) == ["node-2"]) + assert(ancestorNames(node: traits[1]) == []) + + // Make sure we don't crash when undoing it all + swiftNode.setParent(parent: nil) + traits[0].setParent(parent: nil) +} diff --git a/fixtures/ext-types/lib/src/ext-types-lib.udl b/fixtures/ext-types/lib/src/ext-types-lib.udl index 8f62b23be5..38f5bf2d06 100644 --- a/fixtures/ext-types/lib/src/ext-types-lib.udl +++ b/fixtures/ext-types/lib/src/ext-types-lib.udl @@ -18,6 +18,7 @@ namespace imported_types_lib { UniffiOneInterface get_uniffi_one_interface(); ExternalCrateInterface get_external_crate_interface(string val); + UniffiOneProcMacroType get_uniffi_one_proc_macro_type(UniffiOneProcMacroType t); }; // A type defined in a .udl file in the `uniffi-one` crate (ie, in @@ -33,6 +34,10 @@ typedef extern UniffiOneEnum; [ExternalInterface="uniffi_one"] typedef extern UniffiOneInterface; +// A type defined via procmacros in an external crate +[ExternalExport="uniffi_one"] +typedef extern UniffiOneProcMacroType; + // A "wrapped" type defined in the guid crate (ie, defined in `../../guid/src/lib.rs` and // "declared" in `../../guid/src/guid.udl`). But it's still "external" from our POV, // So same as the `.udl` type above! diff --git a/fixtures/ext-types/lib/src/lib.rs b/fixtures/ext-types/lib/src/lib.rs index 52628e86f9..c9dc4fe02a 100644 --- a/fixtures/ext-types/lib/src/lib.rs +++ b/fixtures/ext-types/lib/src/lib.rs @@ -2,7 +2,7 @@ use custom_types::Handle; use ext_types_external_crate::{ExternalCrateDictionary, ExternalCrateInterface}; use ext_types_guid::Guid; use std::sync::Arc; -use uniffi_one::{UniffiOneEnum, UniffiOneInterface, UniffiOneType}; +use uniffi_one::{UniffiOneEnum, UniffiOneInterface, UniffiOneProcMacroType, UniffiOneType}; use url::Url; pub struct CombinedType { @@ -113,6 +113,10 @@ fn get_uniffi_one_interface() -> Arc { Arc::new(UniffiOneInterface::new()) } +fn get_uniffi_one_proc_macro_type(t: UniffiOneProcMacroType) -> UniffiOneProcMacroType { + t +} + fn get_external_crate_interface(val: String) -> Arc { Arc::new(ExternalCrateInterface::new(val)) } diff --git a/fixtures/ext-types/lib/tests/bindings/test_imported_types.kts b/fixtures/ext-types/lib/tests/bindings/test_imported_types.kts index 78cc8d033f..49937fe1ce 100644 --- a/fixtures/ext-types/lib/tests/bindings/test_imported_types.kts +++ b/fixtures/ext-types/lib/tests/bindings/test_imported_types.kts @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import uniffi.imported_types_lib.* -import uniffi.uniffi_one.* +import uniffi.uniffi_one_ns.* val ct = getCombinedType(null) assert(ct.uot.sval == "hello") @@ -27,6 +27,10 @@ assert(getMaybeUniffiOneType(null) == null) assert(getUniffiOneTypes(listOf(uot)) == listOf(uot)) assert(getMaybeUniffiOneTypes(listOf(uot, null)) == listOf(uot, null)) +val uopmt = UniffiOneProcMacroType("hello from proc-macro world") +assert(getUniffiOneProcMacroType(uopmt) == uopmt) +assert(getMyProcMacroType(uopmt) == uopmt) + val uoe = UniffiOneEnum.ONE assert(getUniffiOneEnum(uoe) == uoe) assert(getMaybeUniffiOneEnum(uoe)!! == uoe) diff --git a/fixtures/ext-types/lib/tests/bindings/test_imported_types.py b/fixtures/ext-types/lib/tests/bindings/test_imported_types.py index 1cf60d8059..bf3b5cfe45 100644 --- a/fixtures/ext-types/lib/tests/bindings/test_imported_types.py +++ b/fixtures/ext-types/lib/tests/bindings/test_imported_types.py @@ -5,7 +5,7 @@ import unittest import urllib from imported_types_lib import * -from uniffi_one import * +from uniffi_one_ns import * class TestIt(unittest.TestCase): def test_it(self): @@ -48,5 +48,10 @@ def test_external_crate_types(self): self.assertEqual(ct.ecd.sval, "ecd"); self.assertEqual(get_external_crate_interface("foo").value(), "foo") + def test_procmacro_types(self): + t1 = UniffiOneProcMacroType("hello") + self.assertEqual(t1, get_uniffi_one_proc_macro_type(t1)) + self.assertEqual(t1, get_my_proc_macro_type(t1)) + if __name__=='__main__': unittest.main() diff --git a/fixtures/ext-types/lib/tests/bindings/test_imported_types.swift b/fixtures/ext-types/lib/tests/bindings/test_imported_types.swift index cec96613e7..833d0dbd4b 100644 --- a/fixtures/ext-types/lib/tests/bindings/test_imported_types.swift +++ b/fixtures/ext-types/lib/tests/bindings/test_imported_types.swift @@ -3,7 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import imported_types_lib -//import uniffi_one import Foundation let ct = getCombinedType(value: nil) @@ -26,6 +25,7 @@ assert(getMaybeUniffiOneType(t: UniffiOneType(sval: "hello"))!.sval == "hello") assert(getMaybeUniffiOneType(t: nil) == nil) assert(getUniffiOneTypes(ts: [UniffiOneType(sval: "hello")]) == [UniffiOneType(sval: "hello")]) assert(getMaybeUniffiOneTypes(ts: [UniffiOneType(sval: "hello"), nil]) == [UniffiOneType(sval: "hello"), nil]) +assert(getMyProcMacroType(t: UniffiOneProcMacroType(sval: "proc-macros")).sval == "proc-macros") assert(getUniffiOneEnum(e: UniffiOneEnum.one) == UniffiOneEnum.one) assert(getMaybeUniffiOneEnum(e: UniffiOneEnum.one)! == UniffiOneEnum.one) diff --git a/fixtures/ext-types/lib/uniffi.toml b/fixtures/ext-types/lib/uniffi.toml new file mode 100644 index 0000000000..c090246e23 --- /dev/null +++ b/fixtures/ext-types/lib/uniffi.toml @@ -0,0 +1,5 @@ +[bindings.python.external_packages] +# This fixture does not create a Python package, so we want all modules to be top-level modules. +custom_types = "" +ext_types_guid = "" +uniffi_one_ns = "" diff --git a/fixtures/ext-types/proc-macro-lib/build.rs b/fixtures/ext-types/proc-macro-lib/build.rs deleted file mode 100644 index 376a44c55a..0000000000 --- a/fixtures/ext-types/proc-macro-lib/build.rs +++ /dev/null @@ -1,7 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -fn main() { - uniffi::generate_scaffolding("src/ext-types-lib.udl").unwrap(); -} diff --git a/fixtures/ext-types/proc-macro-lib/src/ext-types-lib.udl b/fixtures/ext-types/proc-macro-lib/src/ext-types-lib.udl deleted file mode 100644 index dbaf61ae64..0000000000 --- a/fixtures/ext-types/proc-macro-lib/src/ext-types-lib.udl +++ /dev/null @@ -1 +0,0 @@ -namespace imported_types_lib { }; diff --git a/fixtures/ext-types/proc-macro-lib/src/lib.rs b/fixtures/ext-types/proc-macro-lib/src/lib.rs index 0a535b32e4..e6ad524d02 100644 --- a/fixtures/ext-types/proc-macro-lib/src/lib.rs +++ b/fixtures/ext-types/proc-macro-lib/src/lib.rs @@ -196,4 +196,4 @@ fn get_guid_procmacro(g: Option) -> Guid { ext_types_guid::get_guid(g) } -uniffi::include_scaffolding!("ext-types-lib"); +uniffi::setup_scaffolding!("imported_types_lib"); diff --git a/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.kts b/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.kts index 4d405f33ab..9dc98372e7 100644 --- a/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.kts +++ b/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.kts @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import uniffi.imported_types_lib.* -import uniffi.uniffi_one.* +import uniffi.uniffi_one_ns.* val ct = getCombinedType(null) assert(ct.uot.sval == "hello") @@ -29,6 +29,7 @@ assert(getMaybeUniffiOneTypes(listOf(uot, null)) == listOf(uot, null)) val uopmt = UniffiOneProcMacroType("hello from proc-macro world") assert(getUniffiOneProcMacroType(uopmt) == uopmt) +assert(getMyProcMacroType(uopmt) == uopmt) val uoe = UniffiOneEnum.ONE assert(getUniffiOneEnum(uoe) == uoe) diff --git a/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.py b/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.py index 2f6aa2d376..37d046086f 100644 --- a/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.py +++ b/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.py @@ -6,7 +6,7 @@ import urllib from ext_types_guid import * from imported_types_lib import * -from uniffi_one import * +from uniffi_one_ns import * class TestIt(unittest.TestCase): def test_it(self): diff --git a/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.swift b/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.swift index 891777d6ae..2bf9302e9a 100644 --- a/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.swift +++ b/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.swift @@ -3,7 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import imported_types_lib -//import uniffi_one import Foundation let ct = getCombinedType(value: nil) @@ -28,6 +27,7 @@ assert(getUniffiOneTypes(ts: [UniffiOneType(sval: "hello")]) == [UniffiOneType(s assert(getMaybeUniffiOneTypes(ts: [UniffiOneType(sval: "hello"), nil]) == [UniffiOneType(sval: "hello"), nil]) assert(getUniffiOneProcMacroType(t: UniffiOneProcMacroType(sval: "hello from proc-macro world")).sval == "hello from proc-macro world") +assert(getMyProcMacroType(t: UniffiOneProcMacroType(sval: "proc-macros all the way down")).sval == "proc-macros all the way down") assert(getUniffiOneEnum(e: UniffiOneEnum.one) == UniffiOneEnum.one) assert(getMaybeUniffiOneEnum(e: UniffiOneEnum.one)! == UniffiOneEnum.one) diff --git a/fixtures/ext-types/proc-macro-lib/uniffi.toml b/fixtures/ext-types/proc-macro-lib/uniffi.toml new file mode 100644 index 0000000000..c090246e23 --- /dev/null +++ b/fixtures/ext-types/proc-macro-lib/uniffi.toml @@ -0,0 +1,5 @@ +[bindings.python.external_packages] +# This fixture does not create a Python package, so we want all modules to be top-level modules. +custom_types = "" +ext_types_guid = "" +uniffi_one_ns = "" diff --git a/fixtures/ext-types/uniffi-one/Cargo.toml b/fixtures/ext-types/uniffi-one/Cargo.toml index de1dbf9807..493db47288 100644 --- a/fixtures/ext-types/uniffi-one/Cargo.toml +++ b/fixtures/ext-types/uniffi-one/Cargo.toml @@ -8,6 +8,7 @@ publish = false [lib] crate-type = ["lib", "cdylib"] +# Note the crate name is different from the namespace used by this crate. name = "uniffi_one" [dependencies] diff --git a/fixtures/ext-types/uniffi-one/src/lib.rs b/fixtures/ext-types/uniffi-one/src/lib.rs index 41e0388572..cf905f48b4 100644 --- a/fixtures/ext-types/uniffi-one/src/lib.rs +++ b/fixtures/ext-types/uniffi-one/src/lib.rs @@ -29,4 +29,9 @@ impl UniffiOneInterface { } } +#[uniffi::export] +fn get_my_proc_macro_type(t: UniffiOneProcMacroType) -> UniffiOneProcMacroType { + t +} + uniffi::include_scaffolding!("uniffi-one"); diff --git a/fixtures/ext-types/uniffi-one/src/uniffi-one.udl b/fixtures/ext-types/uniffi-one/src/uniffi-one.udl index 900833dad7..fed862c614 100644 --- a/fixtures/ext-types/uniffi-one/src/uniffi-one.udl +++ b/fixtures/ext-types/uniffi-one/src/uniffi-one.udl @@ -1,4 +1,4 @@ -namespace uniffi_one {}; +namespace uniffi_one_ns {}; dictionary UniffiOneType { string sval; diff --git a/fixtures/futures/src/lib.rs b/fixtures/futures/src/lib.rs index f077ed4299..63f6c46b5a 100644 --- a/fixtures/futures/src/lib.rs +++ b/fixtures/futures/src/lib.rs @@ -151,6 +151,12 @@ pub async fn async_maybe_new_megaphone(y: bool) -> Option> { } } +/// Async function that inputs `Megaphone`. +#[uniffi::export] +pub async fn say_after_with_megaphone(megaphone: Arc, ms: u16, who: String) -> String { + megaphone.say_after(ms, who).await +} + /// A megaphone. Be careful with the neighbours. #[derive(uniffi::Object)] pub struct Megaphone; @@ -162,7 +168,12 @@ impl Megaphone { say_after(ms, who).await.to_uppercase() } - // An async method that can throw. + /// An async method without any extra arguments. + pub async fn silence(&self) -> String { + String::new() + } + + /// An async method that can throw. pub async fn fallible_me(self: Arc, do_fail: bool) -> Result { if do_fail { Err(MyError::Foo) diff --git a/fixtures/futures/tests/bindings/test_futures.kts b/fixtures/futures/tests/bindings/test_futures.kts index 0ac594bc0f..810bb40f41 100644 --- a/fixtures/futures/tests/bindings/test_futures.kts +++ b/fixtures/futures/tests/bindings/test_futures.kts @@ -92,6 +92,17 @@ runBlocking { assertApproximateTime(time, 200, "async methods") } +runBlocking { + val megaphone = newMegaphone() + val time = measureTimeMillis { + val resultAlice = sayAfterWithMegaphone(megaphone, 200U, "Alice") + + assert(resultAlice == "HELLO, ALICE!") + } + + assertApproximateTime(time, 200, "async methods") +} + // Test async method returning optional object runBlocking { val megaphone = asyncMaybeNewMegaphone(true) @@ -209,7 +220,7 @@ runBlocking { runBlocking { val time = measureTimeMillis { val job = launch { - useSharedResource(SharedResourceOptions(releaseAfterMs=100U, timeoutMs=1000U)) + useSharedResource(SharedResourceOptions(releaseAfterMs=5000U, timeoutMs=100U)) } // Wait some time to ensure the task has locked the shared resource @@ -233,6 +244,3 @@ runBlocking { } println("useSharedResource (not canceled): ${time}ms") } - -// Test that we properly cleaned up future callback references -assert(uniffiActiveFutureCallbacks.size == 0) diff --git a/fixtures/futures/tests/bindings/test_futures.py b/fixtures/futures/tests/bindings/test_futures.py index eab2fc0ac9..bfbeba86f8 100644 --- a/fixtures/futures/tests/bindings/test_futures.py +++ b/fixtures/futures/tests/bindings/test_futures.py @@ -74,6 +74,19 @@ async def test(): asyncio.run(test()) + def test_async_object_param(self): + async def test(): + megaphone = new_megaphone() + t0 = now() + result_alice = await say_after_with_megaphone(megaphone, 200, 'Alice') + t1 = now() + + t_delta = (t1 - t0).total_seconds() + self.assertGreater(t_delta, 0.2) + self.assertEqual(result_alice, 'HELLO, ALICE!') + + asyncio.run(test()) + def test_with_tokio_runtime(self): async def test(): t0 = now() @@ -150,21 +163,14 @@ async def test(): # Test a future that uses a lock and that is cancelled. def test_shared_resource_cancellation(self): - # Note: Python uses the event loop to schedule calls via the `call_soon_threadsafe()` - # method. This means that creating a task and cancelling it won't trigger the issue, we - # need to create an EventLoop and close it instead. - loop = asyncio.new_event_loop() - loop.create_task(use_shared_resource( - SharedResourceOptions(release_after_ms=100, timeout_ms=1000))) - # Wait some time to ensure the task has locked the shared resource - loop.call_later(0.05, loop.stop) - loop.run_forever() - # Close the EventLoop before the shared resource has been released. - loop.close() - - # Try accessing the shared resource again using the main event loop. The initial task - # should release the shared resource before the timeout expires. - asyncio.run(use_shared_resource(SharedResourceOptions(release_after_ms=0, timeout_ms=1000))) + async def test(): + task = asyncio.create_task(use_shared_resource( + SharedResourceOptions(release_after_ms=5000, timeout_ms=100))) + # Wait some time to ensure the task has locked the shared resource + await asyncio.sleep(0.05) + task.cancel() + await use_shared_resource(SharedResourceOptions(release_after_ms=0, timeout_ms=1000)) + asyncio.run(test()) def test_shared_resource_no_cancellation(self): async def test(): diff --git a/fixtures/keywords/kotlin/src/keywords.udl b/fixtures/keywords/kotlin/src/keywords.udl index 72e1c1e8cc..6cdd7a6f07 100644 --- a/fixtures/keywords/kotlin/src/keywords.udl +++ b/fixtures/keywords/kotlin/src/keywords.udl @@ -9,7 +9,7 @@ interface break { callback interface continue { return return(return v); - continue? continue(sequence continue); + continue? continue(); record break(break? v); while while(sequence while); record>? class(record> v); @@ -32,7 +32,6 @@ interface false { true(u8 object); }; -[Error] enum class { "object", }; diff --git a/fixtures/keywords/kotlin/src/lib.rs b/fixtures/keywords/kotlin/src/lib.rs index c156a8037c..d5c63ab780 100644 --- a/fixtures/keywords/kotlin/src/lib.rs +++ b/fixtures/keywords/kotlin/src/lib.rs @@ -17,7 +17,7 @@ impl r#break { #[allow(non_camel_case_types)] trait r#continue { fn r#return(&self, v: r#return) -> r#return; - fn r#continue(&self, v: Vec>) -> Option>; + fn r#continue(&self) -> Option>; fn r#break(&self, _v: Option>) -> HashMap>; fn r#while(&self, _v: Vec) -> r#while; fn class(&self, _v: HashMap>) -> Option>>; @@ -44,9 +44,8 @@ pub enum r#false { } #[allow(non_camel_case_types)] -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] pub enum class { - #[error("object error")] object, } diff --git a/fixtures/keywords/rust/src/keywords.udl b/fixtures/keywords/rust/src/keywords.udl index fe51b1e315..1edc1cdb6b 100644 --- a/fixtures/keywords/rust/src/keywords.udl +++ b/fixtures/keywords/rust/src/keywords.udl @@ -7,7 +7,7 @@ namespace keywords_rust { interface break { return return(return v); - sequence? continue(sequence continue); + void continue(sequence continue); record? break(record v); void yield(u8 async); void async(u8? yield); @@ -15,7 +15,7 @@ interface break { callback interface continue { return return(return v); - continue? continue(sequence continue); + continue? continue(); record break(break? v); while while(sequence while); record>? yield(record> v); @@ -28,7 +28,7 @@ dictionary return { dictionary while { return return; - sequence continue; + sequence yield; record break; break? for; sequence async; @@ -39,7 +39,6 @@ interface async { as(u8 async); }; -[Error] enum yield { "async", }; diff --git a/fixtures/keywords/rust/src/lib.rs b/fixtures/keywords/rust/src/lib.rs index 09f9e6de24..0b93557307 100644 --- a/fixtures/keywords/rust/src/lib.rs +++ b/fixtures/keywords/rust/src/lib.rs @@ -16,9 +16,7 @@ impl r#break { pub fn r#break(&self, v: HashMap>) -> Option>> { Some(v) } - fn r#continue(&self, v: Vec>) -> Option>> { - Some(v) - } + fn r#continue(&self, _v: Vec>) {} pub fn r#yield(&self, _async: u8) {} pub fn r#async(&self, _yield: Option) {} } @@ -26,7 +24,7 @@ impl r#break { #[allow(non_camel_case_types)] pub trait r#continue { fn r#return(&self, v: r#return) -> r#return; - fn r#continue(&self, v: Vec>) -> Option>; + fn r#continue(&self) -> Option>; fn r#break(&self, _v: Option>) -> HashMap>; fn r#while(&self, _v: Vec) -> r#while; fn r#yield(&self, _v: HashMap>) -> Option>>; @@ -42,7 +40,7 @@ pub struct r#return { #[allow(non_camel_case_types)] pub struct r#while { r#return: r#return, - r#continue: Vec>, + r#yield: Vec, r#break: HashMap>, r#for: Option>, r#async: Vec, @@ -55,9 +53,8 @@ pub enum r#async { } #[allow(non_camel_case_types)] -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] pub enum r#yield { - #[error("async error")] r#async, } diff --git a/fixtures/metadata/Cargo.toml b/fixtures/metadata/Cargo.toml index b4980799df..a95c447885 100644 --- a/fixtures/metadata/Cargo.toml +++ b/fixtures/metadata/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "uniffi_fixture_metadata" +name = "uniffi-fixture-metadata" version = "0.1.0" edition = "2021" license = "MPL-2.0" diff --git a/fixtures/metadata/src/tests.rs b/fixtures/metadata/src/tests.rs index 8833aa5360..eac852cfea 100644 --- a/fixtures/metadata/src/tests.rs +++ b/fixtures/metadata/src/tests.rs @@ -66,6 +66,12 @@ mod calc { pub struct Calculator {} } +mod uniffi_traits { + #[derive(Debug, PartialEq, Eq, uniffi::Object)] + #[uniffi::export(Debug, Eq)] + pub struct Special {} +} + #[uniffi::export(callback_interface)] pub trait Logger { fn log(&self, message: String); @@ -75,15 +81,16 @@ pub use calc::Calculator; pub use error::{ComplexError, FlatError}; pub use person::Person; pub use state::State; +pub use uniffi_traits::Special; pub use weapon::Weapon; mod test_type_ids { use super::*; use std::collections::HashMap; use std::sync::Arc; - use uniffi_core::FfiConverter; + use uniffi_core::Lower; - fn check_type_id>(correct_type: Type) { + fn check_type_id>(correct_type: Type) { let buf = &mut T::TYPE_ID_META.as_ref(); assert_eq!( uniffi_meta::read_metadata_type(buf).unwrap(), @@ -308,10 +315,27 @@ mod test_metadata { module_path: "uniffi_fixture_metadata".into(), name: "Calculator".into(), imp: ObjectImpl::Struct, - uniffi_traits: vec![], }, ); } + + #[test] + fn test_uniffi_traits() { + assert!(matches!( + uniffi_meta::read_metadata(&uniffi_traits::UNIFFI_META_UNIFFI_FIXTURE_METADATA_UNIFFI_TRAIT_SPECIAL_DEBUG).unwrap(), + Metadata::UniffiTrait(UniffiTraitMetadata::Debug { fmt }) + if fmt.module_path == "uniffi_fixture_metadata" + && fmt.self_name == "Special" + )); + assert!(matches!( + uniffi_meta::read_metadata(&uniffi_traits::UNIFFI_META_UNIFFI_FIXTURE_METADATA_UNIFFI_TRAIT_SPECIAL_EQ).unwrap(), + Metadata::UniffiTrait(UniffiTraitMetadata::Eq { eq, ne }) + if eq.module_path == "uniffi_fixture_metadata" + && ne.module_path == "uniffi_fixture_metadata" + && eq.self_name == "Special" + && ne.self_name == "Special" + )); + } } mod test_function_metadata { diff --git a/fixtures/proc-macro/src/lib.rs b/fixtures/proc-macro/src/lib.rs index 56666d32bc..351dbd8245 100644 --- a/fixtures/proc-macro/src/lib.rs +++ b/fixtures/proc-macro/src/lib.rs @@ -2,7 +2,7 @@ * 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::sync::Arc; +use std::{collections::HashMap, sync::Arc}; mod callback_interface; @@ -116,13 +116,24 @@ fn take_two(two: Two) -> String { two.a } +#[uniffi::export] +fn make_hashmap(k: i8, v: u64) -> HashMap { + HashMap::from([(k, v)]) +} + +// XXX - fails to call this from python - https://github.com/mozilla/uniffi-rs/issues/1774 +#[uniffi::export] +fn return_hashmap(h: HashMap) -> HashMap { + h +} + #[uniffi::export] fn take_record_with_bytes(rwb: RecordWithBytes) -> Vec { rwb.some_bytes } #[uniffi::export] -fn test_callback_interface(cb: Box) { +fn call_callback_interface(cb: Box) { cb.do_nothing(); assert_eq!(cb.add(1, 1), 2); assert_eq!(cb.optional(Some(1)), 1); @@ -177,7 +188,6 @@ fn enum_identity(value: MaybeBool) -> MaybeBool { } #[derive(thiserror::Error, uniffi::Error, Debug, PartialEq, Eq)] -#[uniffi(handle_unknown_callback_error)] pub enum BasicError { #[error("InvalidInput")] InvalidInput, diff --git a/fixtures/proc-macro/tests/bindings/test_proc_macro.kts b/fixtures/proc-macro/tests/bindings/test_proc_macro.kts index 1a5493731a..8d5199a001 100644 --- a/fixtures/proc-macro/tests/bindings/test_proc_macro.kts +++ b/fixtures/proc-macro/tests/bindings/test_proc_macro.kts @@ -75,4 +75,4 @@ class KtTestCallbackInterface : TestCallbackInterface { } } -testCallbackInterface(KtTestCallbackInterface()) +callCallbackInterface(KtTestCallbackInterface()) diff --git a/fixtures/proc-macro/tests/bindings/test_proc_macro.py b/fixtures/proc-macro/tests/bindings/test_proc_macro.py index 087c1caa93..c80489fc1a 100644 --- a/fixtures/proc-macro/tests/bindings/test_proc_macro.py +++ b/fixtures/proc-macro/tests/bindings/test_proc_macro.py @@ -33,6 +33,11 @@ assert(make_zero().inner == "ZERO") assert(make_record_with_bytes().some_bytes == bytes([0, 1, 2, 3, 4])) +assert(make_hashmap(1, 2) == {1: 2}) +# fails with AttributeError!? - https://github.com/mozilla/uniffi-rs/issues/1774 +# d = {1, 2} +# assert(return_hashmap(d) == d) + try: always_fails() except BasicError.OsError: @@ -77,4 +82,4 @@ def callback_handler(self, h): v = h.take_error(BasicError.InvalidInput()) return v -test_callback_interface(PyTestCallbackInterface()) +call_callback_interface(PyTestCallbackInterface()) diff --git a/fixtures/proc-macro/tests/bindings/test_proc_macro.swift b/fixtures/proc-macro/tests/bindings/test_proc_macro.swift index 388324cde3..1232b406bd 100644 --- a/fixtures/proc-macro/tests/bindings/test_proc_macro.swift +++ b/fixtures/proc-macro/tests/bindings/test_proc_macro.swift @@ -84,4 +84,4 @@ class SwiftTestCallbackInterface : TestCallbackInterface { } } -testCallbackInterface(cb: SwiftTestCallbackInterface()) +callCallbackInterface(cb: SwiftTestCallbackInterface()) diff --git a/fixtures/reexport-scaffolding-macro/src/lib.rs b/fixtures/reexport-scaffolding-macro/src/lib.rs index 6262b431a7..6bd04f2ccd 100644 --- a/fixtures/reexport-scaffolding-macro/src/lib.rs +++ b/fixtures/reexport-scaffolding-macro/src/lib.rs @@ -8,7 +8,7 @@ mod tests { use std::ffi::CString; use std::os::raw::c_void; use std::process::{Command, Stdio}; - use uniffi::{FfiConverter, ForeignCallback, RustBuffer, RustCallStatus}; + use uniffi::{FfiConverter, ForeignCallback, RustBuffer, RustCallStatus, RustCallStatusCode}; use uniffi_bindgen::ComponentInterface; struct UniFfiTag; @@ -165,7 +165,7 @@ mod tests { get_symbol(&library, object_def.ffi_object_free().name()); let num_alive = unsafe { get_num_alive(&mut call_status) }; - assert_eq!(call_status.code, 0); + assert_eq!(call_status.code, RustCallStatusCode::Success); assert_eq!(num_alive, 0); let obj_id = unsafe { @@ -174,24 +174,24 @@ mod tests { &mut call_status, ) }; - assert_eq!(call_status.code, 0); + assert_eq!(call_status.code, RustCallStatusCode::Success); let name_buf = unsafe { coveralls_get_name(obj_id, &mut call_status) }; - assert_eq!(call_status.code, 0); + assert_eq!(call_status.code, RustCallStatusCode::Success); assert_eq!( >::try_lift(name_buf).unwrap(), "TestName" ); let num_alive = unsafe { get_num_alive(&mut call_status) }; - assert_eq!(call_status.code, 0); + assert_eq!(call_status.code, RustCallStatusCode::Success); assert_eq!(num_alive, 1); unsafe { coveralls_free(obj_id, &mut call_status) }; - assert_eq!(call_status.code, 0); + assert_eq!(call_status.code, RustCallStatusCode::Success); let num_alive = unsafe { get_num_alive(&mut call_status) }; - assert_eq!(call_status.code, 0); + assert_eq!(call_status.code, RustCallStatusCode::Success); assert_eq!(num_alive, 0); } } diff --git a/fixtures/trait-methods/src/lib.rs b/fixtures/trait-methods/src/lib.rs index 81da1c640a..acc3683f35 100644 --- a/fixtures/trait-methods/src/lib.rs +++ b/fixtures/trait-methods/src/lib.rs @@ -1,8 +1,11 @@ /* 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::sync::Arc; + #[derive(Debug, PartialEq, Eq, Hash)] -struct TraitMethods { +pub struct TraitMethods { val: String, } @@ -18,4 +21,24 @@ impl std::fmt::Display for TraitMethods { } } +#[derive(Debug, PartialEq, Eq, Hash, uniffi::Object)] +#[uniffi::export(Debug, Display, Eq, Hash)] +pub struct ProcTraitMethods { + val: String, +} + +#[uniffi::export] +impl ProcTraitMethods { + #[uniffi::constructor] + fn new(val: String) -> Arc { + Arc::new(Self { val }) + } +} + +impl std::fmt::Display for ProcTraitMethods { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ProcTraitMethods({})", self.val) + } +} + uniffi::include_scaffolding!("trait_methods"); diff --git a/fixtures/trait-methods/tests/bindings/test.py b/fixtures/trait-methods/tests/bindings/test.py index 1f5b9cf8e2..9ec51a4cb0 100644 --- a/fixtures/trait-methods/tests/bindings/test.py +++ b/fixtures/trait-methods/tests/bindings/test.py @@ -29,5 +29,29 @@ def test_hash(self): d[m] = "m" self.assertTrue(m in d) +class TestProcmacroTraitMethods(unittest.TestCase): + def test_str(self): + m = ProcTraitMethods("yo") + self.assertEqual(str(m), "ProcTraitMethods(yo)") + + def test_repr(self): + m = ProcTraitMethods("yo") + self.assertEqual(repr(m), 'ProcTraitMethods { val: "yo" }') + + def test_eq(self): + m = ProcTraitMethods("yo") + self.assertEqual(m, ProcTraitMethods("yo")) + self.assertNotEqual(m, ProcTraitMethods("yoyo")) + + def test_eq(self): + m = ProcTraitMethods("yo") + self.assertNotEqual(m, 17) + + def test_hash(self): + d = {} + m = ProcTraitMethods("m") + d[m] = "m" + self.assertTrue(m in d) + if __name__=='__main__': unittest.main() diff --git a/fixtures/uitests/tests/ui/fieldless_errors_used_in_callbacks_cant_have_fields.stderr b/fixtures/uitests/tests/ui/fieldless_errors_used_in_callbacks_cant_have_fields.stderr index 6b8f8c4271..fdb3b15fb3 100644 --- a/fixtures/uitests/tests/ui/fieldless_errors_used_in_callbacks_cant_have_fields.stderr +++ b/fixtures/uitests/tests/ui/fieldless_errors_used_in_callbacks_cant_have_fields.stderr @@ -4,7 +4,6 @@ error[E0533]: expected value, found struct variant `Self::DivisionByZero` | / #[::uniffi::derive_error_for_udl( | | flat_error, | | with_try_read, - | | handle_unknown_callback_error, | | )] | |__^ not a value | diff --git a/fixtures/uitests/tests/ui/invalid_types_in_signatures.rs b/fixtures/uitests/tests/ui/invalid_types_in_signatures.rs new file mode 100644 index 0000000000..a0f9e20cf3 --- /dev/null +++ b/fixtures/uitests/tests/ui/invalid_types_in_signatures.rs @@ -0,0 +1,25 @@ +fn main() { /* empty main required by `trybuild` */} + +#[derive(Debug, uniffi::Error, thiserror::Error)] +pub enum ErrorType { + #[error("Foo")] + Foo, +} + +#[uniffi::export] +pub fn input_error(_e: ErrorType) { } + +#[uniffi::export] +pub fn output_error() -> ErrorType { + ErrorType::Foo +} + +#[uniffi::export] +pub fn input_result(_r: Result<(), ErrorType>) { } + +#[uniffi::export] +pub fn return_option_result() -> Option> { + Some(Ok(())) +} + +uniffi_macros::setup_scaffolding!(); diff --git a/fixtures/uitests/tests/ui/invalid_types_in_signatures.stderr b/fixtures/uitests/tests/ui/invalid_types_in_signatures.stderr new file mode 100644 index 0000000000..697bb4437c --- /dev/null +++ b/fixtures/uitests/tests/ui/invalid_types_in_signatures.stderr @@ -0,0 +1,37 @@ +error[E0277]: the trait bound `Result<(), ErrorType>: Lift` is not satisfied + --> tests/ui/invalid_types_in_signatures.rs:17:1 + | +17 | #[uniffi::export] + | ^^^^^^^^^^^^^^^^^ the trait `Lift` is not implemented for `Result<(), ErrorType>` + | + = help: the following other types implement trait `Lift`: + bool + i8 + i16 + i32 + i64 + u8 + u16 + u32 + and $N others + = note: this error originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `Result<(), ErrorType>: Lower` is not satisfied + --> tests/ui/invalid_types_in_signatures.rs:20:1 + | +20 | #[uniffi::export] + | ^^^^^^^^^^^^^^^^^ the trait `Lower` is not implemented for `Result<(), ErrorType>` + | + = help: the following other types implement trait `Lower`: + bool + i8 + i16 + i32 + i64 + u8 + u16 + u32 + and $N others + = note: required for `Option>` to implement `Lower` + = note: required for `Option>` to implement `LowerReturn` + = note: this error originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/fixtures/uitests/tests/ui/non_hashable_record_key.stderr b/fixtures/uitests/tests/ui/non_hashable_record_key.stderr index 98a1f99d3e..2841a127d9 100644 --- a/fixtures/uitests/tests/ui/non_hashable_record_key.stderr +++ b/fixtures/uitests/tests/ui/non_hashable_record_key.stderr @@ -5,16 +5,17 @@ error[E0277]: the trait bound `f32: Hash` is not satisfied | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Hash` is not implemented for `f32` | = help: the following other types implement trait `Hash`: - i128 + isize + i8 i16 i32 i64 - i8 - isize - u128 - u16 + i128 + usize + u8 and $N others - = note: required for `HashMap` to implement `FfiConverter` + = note: required for `HashMap` to implement `Lower` + = note: required for `HashMap` to implement `LowerReturn` = note: this error originates in the attribute macro `::uniffi::export_for_udl` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `f32: std::cmp::Eq` is not satisfied @@ -24,16 +25,17 @@ error[E0277]: the trait bound `f32: std::cmp::Eq` is not satisfied | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::cmp::Eq` is not implemented for `f32` | = help: the following other types implement trait `std::cmp::Eq`: - i128 + isize + i8 i16 i32 i64 - i8 - isize - u128 - u16 + i128 + usize + u8 and $N others - = note: required for `HashMap` to implement `FfiConverter` + = note: required for `HashMap` to implement `Lower` + = note: required for `HashMap` to implement `LowerReturn` = note: this error originates in the attribute macro `::uniffi::export_for_udl` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0425]: cannot find function `get_dict` in this scope diff --git a/fixtures/uitests/tests/ui/trait_methods_no_trait.stderr b/fixtures/uitests/tests/ui/trait_methods_no_trait.stderr index 8dde34ea49..c689aeca95 100644 --- a/fixtures/uitests/tests/ui/trait_methods_no_trait.stderr +++ b/fixtures/uitests/tests/ui/trait_methods_no_trait.stderr @@ -1,28 +1,24 @@ error[E0277]: `TraitMethods` doesn't implement `std::fmt::Display` --> $OUT_DIR[uniffi_uitests]/trait_methods.uniffi.rs | - | ... uniffi::deps::static_assertions::assert_impl_all!(r#TraitMethods: std::fmt::Display); // This object has a trait method which requi... - | ^^^^^^^^^^^^^^ `TraitMethods` cannot be formatted with the default formatter + | struct r#TraitMethods { } + | ^^^^^^^^^^^^^^ `TraitMethods` cannot be formatted with the default formatter | = help: the trait `std::fmt::Display` is not implemented for `TraitMethods` = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead -note: required by a bound in `uniffi_uniffi_trait_methods_fn_method_traitmethods_uniffi_trait_display::{closure#0}::_::{closure#0}::assert_impl_all` +note: required by a bound in `TraitMethods::uniffi_trait_display::_::{closure#0}::assert_impl_all` --> $OUT_DIR[uniffi_uitests]/trait_methods.uniffi.rs | - | ... uniffi::deps::static_assertions::assert_impl_all!(r#TraitMethods: std::fmt::Display); // This object has a trait method which requi... - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `assert_impl_all` - = note: this error originates in the macro `uniffi::deps::static_assertions::assert_impl_all` (in Nightly builds, run with -Z macro-backtrace for more info) + | #[uniffi::export(Display)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `assert_impl_all` + = note: this error originates in the macro `::uniffi::deps::static_assertions::assert_impl_all` which comes from the expansion of the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: `TraitMethods` doesn't implement `std::fmt::Display` --> $OUT_DIR[uniffi_uitests]/trait_methods.uniffi.rs | - | / match as ::uniffi::FfiConverter>::try_lift(r#ptr) { - | | Ok(ref val) => val, - | | Err(err) => panic!("Failed to convert arg '{}': {}", "ptr", err), - | | } - | |_________________^ `TraitMethods` cannot be formatted with the default formatter + | #[uniffi::export(Display)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ `TraitMethods` cannot be formatted with the default formatter | = help: the trait `std::fmt::Display` is not implemented for `TraitMethods` = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead - = help: the trait `std::fmt::Display` is implemented for `Arc` - = note: this error originates in the macro `$crate::__export::format_args` which comes from the expansion of the macro `format` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the macro `$crate::__export::format_args` which comes from the expansion of the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index e0039971b1..3c573cf5ab 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -5,7 +5,7 @@ # * ./.circleci/config.yml which also specifies the rust versions used in CI. [toolchain] -channel = "1.69.0" +channel = "1.72.0" targets = [ "aarch64-linux-android", "armv7-linux-androideabi", diff --git a/uniffi/src/lib.rs b/uniffi/src/lib.rs index 758f1d4c2d..0625bd9c66 100644 --- a/uniffi/src/lib.rs +++ b/uniffi/src/lib.rs @@ -25,6 +25,11 @@ pub use uniffi_build::{generate_scaffolding, generate_scaffolding_for_crate}; #[cfg(feature = "bindgen-tests")] pub use uniffi_macros::build_foreign_language_testcases; +#[cfg(feature = "cli")] +pub fn uniffi_bindgen_main() { + cli::run_main().unwrap(); +} + #[cfg(test)] mod test { #[test] @@ -33,8 +38,3 @@ mod test { t.compile_fail("tests/ui/*.rs"); } } - -#[cfg(feature = "cli")] -pub fn uniffi_bindgen_main() { - cli::run_main().unwrap(); -} diff --git a/uniffi/tests/ui/proc_macro_arc.stderr b/uniffi/tests/ui/proc_macro_arc.stderr index e12ebbe671..0654a29e25 100644 --- a/uniffi/tests/ui/proc_macro_arc.stderr +++ b/uniffi/tests/ui/proc_macro_arc.stderr @@ -4,32 +4,19 @@ error[E0277]: the trait bound `Foo: FfiConverterArc` is not satisfied 10 | #[uniffi::export] | ^^^^^^^^^^^^^^^^^ the trait `FfiConverterArc` is not implemented for `Foo` | - = help: the trait `FfiConverter` is implemented for `Arc` + = help: the trait `LowerReturn` is implemented for `Arc` = note: required for `Arc` to implement `FfiConverter` + = note: required for `Arc` to implement `Lower` + = note: required for `Arc` to implement `LowerReturn` = note: this error originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) -note: erroneous constant used - --> tests/ui/proc_macro_arc.rs:10:1 - | -10 | #[uniffi::export] - | ^^^^^^^^^^^^^^^^^ - | - = note: this note originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) - error[E0277]: the trait bound `child::Foo: FfiConverterArc` is not satisfied --> tests/ui/proc_macro_arc.rs:20:5 | 20 | #[uniffi::export] | ^^^^^^^^^^^^^^^^^ the trait `FfiConverterArc` is not implemented for `child::Foo` | - = help: the trait `FfiConverter` is implemented for `Arc` + = help: the trait `Lift` is implemented for `Arc` = note: required for `Arc` to implement `FfiConverter` + = note: required for `Arc` to implement `Lift` = note: this error originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) - -note: erroneous constant used - --> tests/ui/proc_macro_arc.rs:20:5 - | -20 | #[uniffi::export] - | ^^^^^^^^^^^^^^^^^ - | - = note: this note originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/callback_interface.rs b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/callback_interface.rs index 65afbef276..17a08745b4 100644 --- a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/callback_interface.rs +++ b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/callback_interface.rs @@ -29,6 +29,6 @@ impl CodeType for CallbackInterfaceCodeType { } fn initialization_fn(&self) -> Option { - Some(format!("{}.register", self.ffi_converter_name())) + Some(format!("uniffiCallbackInterface{}.register", self.id)) } } diff --git a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs index 2c1a3e468d..ef2cccfebd 100644 --- a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs +++ b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs @@ -66,8 +66,6 @@ impl Config { } impl BindingsConfig for Config { - const TOML_KEY: &'static str = "kotlin"; - fn update_from_ci(&mut self, ci: &ComponentInterface) { self.package_name .get_or_insert_with(|| format!("uniffi.{}", ci.namespace())); @@ -144,11 +142,13 @@ impl<'a> TypeRenderer<'a> { } // Get the package name for an external type - fn external_type_package_name(&self, module_path: &str) -> String { + fn external_type_package_name(&self, module_path: &str, namespace: &str) -> String { + // config overrides are keyed by the crate name, default fallback is the namespace. let crate_name = module_path.split("::").next().unwrap(); match self.kotlin_config.external_packages.get(crate_name) { Some(name) => name.clone(), - None => crate_name.to_string(), + // unreachable in library mode - all deps are in our config with correct namespace. + None => format!("uniffi.{namespace}"), } } @@ -196,6 +196,7 @@ pub struct KotlinWrapper<'a> { ci: &'a ComponentInterface, type_helper_code: String, type_imports: BTreeSet, + has_async_fns: bool, } impl<'a> KotlinWrapper<'a> { @@ -208,6 +209,7 @@ impl<'a> KotlinWrapper<'a> { ci, type_helper_code, type_imports, + has_async_fns: ci.has_async_fns(), } } @@ -216,6 +218,10 @@ impl<'a> KotlinWrapper<'a> { .iter_types() .map(|t| KotlinCodeOracle.find(t)) .filter_map(|ct| ct.initialization_fn()) + .chain( + self.has_async_fns + .then(|| "uniffiRustFutureContinuationCallback.register".into()), + ) .collect() } @@ -300,10 +306,32 @@ impl KotlinCodeOracle { FfiType::ForeignCallback => "ForeignCallback".to_string(), FfiType::ForeignExecutorHandle => "USize".to_string(), FfiType::ForeignExecutorCallback => "UniFfiForeignExecutorCallback".to_string(), - FfiType::FutureCallback { return_type } => { - format!("UniFfiFutureCallback{}", Self::ffi_type_label(return_type)) + FfiType::RustFutureHandle => "Pointer".to_string(), + FfiType::RustFutureContinuationCallback => { + "UniFffiRustFutureContinuationCallbackType".to_string() + } + FfiType::RustFutureContinuationData => "USize".to_string(), + } + } + + /// Get the name of the interface and class name for an object. + /// + /// This depends on the `ObjectImpl`: + /// + /// For struct impls, the class name is the object name and the interface name is derived from that. + /// For trait impls, the interface name is the object name, and the class name is derived from that. + /// + /// This split is needed because of the `FfiConverter` interface. For struct impls, `lower` + /// can only lower the concrete class. For trait impls, `lower` can lower anything that + /// implement the interface. + fn object_names(&self, obj: &Object) -> (String, String) { + let class_name = self.class_name(obj.name()); + match obj.imp() { + ObjectImpl::Struct => (format!("{class_name}Interface"), class_name), + ObjectImpl::Trait => { + let interface_name = format!("{class_name}Impl"); + (class_name, interface_name) } - FfiType::FutureCallbackData => "USize".to_string(), } } } @@ -340,7 +368,7 @@ impl AsCodeType for T { Type::Duration => Box::new(miscellany::DurationCodeType), Type::Enum { name, .. } => Box::new(enum_::EnumCodeType::new(name)), - Type::Object { name, .. } => Box::new(object::ObjectCodeType::new(name)), + Type::Object { name, imp, .. } => Box::new(object::ObjectCodeType::new(name, imp)), Type::Record { name, .. } => Box::new(record::RecordCodeType::new(name)), Type::CallbackInterface { name, .. } => { Box::new(callback_interface::CallbackInterfaceCodeType::new(name)) @@ -513,6 +541,10 @@ pub mod filters { .ffi_converter_name()) } + pub fn object_names(obj: &Object) -> Result<(String, String), askama::Error> { + Ok(KotlinCodeOracle.object_names(obj)) + } + /// Remove the "`" chars we put around function/variable names /// /// These are used to avoid name clashes with kotlin identifiers, but sometimes you want to diff --git a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/object.rs b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/object.rs index 16fa0f2403..c6c42194ac 100644 --- a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/object.rs +++ b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/object.rs @@ -2,29 +2,40 @@ * 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::{CodeType, Literal}; +use crate::{ + backend::{CodeType, Literal}, + interface::ObjectImpl, +}; #[derive(Debug)] pub struct ObjectCodeType { - id: String, + name: String, + imp: ObjectImpl, } impl ObjectCodeType { - pub fn new(id: String) -> Self { - Self { id } + pub fn new(name: String, imp: ObjectImpl) -> Self { + Self { name, imp } } } impl CodeType for ObjectCodeType { fn type_label(&self) -> String { - super::KotlinCodeOracle.class_name(&self.id) + super::KotlinCodeOracle.class_name(&self.name) } fn canonical_name(&self) -> String { - format!("Type{}", self.id) + format!("Type{}", self.name) } fn literal(&self, _literal: &Literal) -> String { unreachable!(); } + + fn initialization_fn(&self) -> Option { + match &self.imp { + ObjectImpl::Struct => None, + ObjectImpl::Trait => Some(format!("uniffiCallbackInterface{}.register", self.name)), + } + } } diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/Async.kt b/uniffi_bindgen/src/bindings/kotlin/templates/Async.kt new file mode 100644 index 0000000000..c6a32655f2 --- /dev/null +++ b/uniffi_bindgen/src/bindings/kotlin/templates/Async.kt @@ -0,0 +1,44 @@ +// Async return type handlers + +internal const val UNIFFI_RUST_FUTURE_POLL_READY = 0.toShort() +internal const val UNIFFI_RUST_FUTURE_POLL_MAYBE_READY = 1.toShort() + +internal val uniffiContinuationHandleMap = UniFfiHandleMap>() + +// FFI type for Rust future continuations +internal object uniffiRustFutureContinuationCallback: UniFffiRustFutureContinuationCallbackType { + override fun callback(continuationHandle: USize, pollResult: Short) { + uniffiContinuationHandleMap.remove(continuationHandle)?.resume(pollResult) + } + + internal fun register(lib: _UniFFILib) { + lib.{{ ci.ffi_rust_future_continuation_callback_set().name() }}(this) + } +} + +internal suspend fun uniffiRustCallAsync( + rustFuture: Pointer, + pollFunc: (Pointer, USize) -> Unit, + completeFunc: (Pointer, RustCallStatus) -> F, + freeFunc: (Pointer) -> Unit, + liftFunc: (F) -> T, + errorHandler: CallStatusErrorHandler +): T { + try { + do { + val pollResult = suspendCancellableCoroutine { continuation -> + pollFunc( + rustFuture, + uniffiContinuationHandleMap.insert(continuation) + ) + } + } while (pollResult != UNIFFI_RUST_FUTURE_POLL_READY); + + return liftFunc( + rustCallWithError(errorHandler, { status -> completeFunc(rustFuture, status) }) + ) + } finally { + freeFunc(rustFuture) + } +} + diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/AsyncTypes.kt b/uniffi_bindgen/src/bindings/kotlin/templates/AsyncTypes.kt deleted file mode 100644 index 51da2bf314..0000000000 --- a/uniffi_bindgen/src/bindings/kotlin/templates/AsyncTypes.kt +++ /dev/null @@ -1,47 +0,0 @@ -// Async return type handlers - -{# add imports that we use #} -{{ self.add_import("kotlin.coroutines.Continuation") }} -{{ self.add_import("kotlin.coroutines.resume") }} -{{ self.add_import("kotlin.coroutines.resumeWithException") }} - -{# We use these in the generated functions, which don't have access to add_import() -- might as well add it here #} -{{ self.add_import("kotlinx.coroutines.suspendCancellableCoroutine") }} -{{ self.add_import("kotlinx.coroutines.coroutineScope") }} - -// Stores all active future callbacks to ensure they're not GC'ed while waiting for the Rust code to -// complete the callback -val uniffiActiveFutureCallbacks = mutableSetOf() - -// FFI type for callback handlers -{%- for callback_param in ci.iter_future_callback_params()|unique_ffi_types %} -internal interface UniFfiFutureCallback{{ callback_param|ffi_type_name }} : com.sun.jna.Callback { - // Note: callbackData is always 0. We could pass Rust a pointer/usize to represent the - // continuation, but with JNA it's easier to just store it in the callback handler. - fun callback(_callbackData: USize, returnValue: {{ callback_param|ffi_type_name_by_value }}?, callStatus: RustCallStatus.ByValue); -} -{%- endfor %} - -// Callback handlers for an async call. These are invoked by Rust when the future is ready. They -// lift the return value or error and resume the suspended function. -{%- for result_type in ci.iter_async_result_types() %} -{%- let callback_param = result_type.future_callback_param() %} - -internal class {{ result_type|future_callback_handler }}(val continuation: {{ result_type|future_continuation_type }}) - : UniFfiFutureCallback{{ callback_param|ffi_type_name }} { - override fun callback(_callbackData: USize, returnValue: {{ callback_param|ffi_type_name_by_value }}?, callStatus: RustCallStatus.ByValue) { - uniffiActiveFutureCallbacks.remove(this) - try { - checkCallStatus({{ result_type|error_handler }}, callStatus) - {%- match result_type.return_type %} - {%- when Some(return_type) %} - continuation.resume({{ return_type|lift_fn }}(returnValue!!)) - {%- when None %} - continuation.resume(Unit) - {%- endmatch %} - } catch (e: Throwable) { - continuation.resumeWithException(e) - } - } -} -{%- endfor %} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceImpl.kt b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceImpl.kt new file mode 100644 index 0000000000..f1c58ee971 --- /dev/null +++ b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceImpl.kt @@ -0,0 +1,107 @@ +{% if self.include_once_check("CallbackInterfaceRuntime.kt") %}{% include "CallbackInterfaceRuntime.kt" %}{% endif %} + +// Implement the foreign callback handler for {{ interface_name }} +internal class {{ callback_handler_class }} : ForeignCallback { + @Suppress("TooGenericExceptionCaught") + override fun invoke(handle: Handle, method: Int, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int { + val cb = {{ ffi_converter_name }}.handleMap.get(handle) + return when (method) { + IDX_CALLBACK_FREE -> { + {{ ffi_converter_name }}.handleMap.remove(handle) + + // Successful return + // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` + UNIFFI_CALLBACK_SUCCESS + } + {% for meth in methods.iter() -%} + {% 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_core/src/ffi/foreigncallbacks.rs` for info + try { + this.{{ method_name }}(cb, argsData, argsLen, outBuf) + } 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 + } + UNIFFI_CALLBACK_UNEXPECTED_ERROR + } + } + {% endfor %} + else -> { + // An unexpected error happened. + // See docs of ForeignCallback in `uniffi_core/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 + } + UNIFFI_CALLBACK_UNEXPECTED_ERROR + } + } + } + + {% for meth in methods.iter() -%} + {% let method_name = format!("invoke_{}", meth.name())|fn_name %} + @Suppress("UNUSED_PARAMETER") + private fun {{ method_name }}(kotlinCallbackInterface: {{ interface_name }}, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int { + {%- if meth.arguments().len() > 0 %} + val argsBuf = argsData.getByteBuffer(0, argsLen.toLong()).also { + it.order(ByteOrder.BIG_ENDIAN) + } + {%- endif %} + + {%- match meth.return_type() %} + {%- when Some with (return_type) %} + fun makeCall() : Int { + val returnValue = kotlinCallbackInterface.{{ meth.name()|fn_name }}( + {%- for arg in meth.arguments() %} + {{ arg|read_fn }}(argsBuf) + {% if !loop.last %}, {% endif %} + {%- endfor %} + ) + outBuf.setValue({{ return_type|ffi_converter_name }}.lowerIntoRustBuffer(returnValue)) + return UNIFFI_CALLBACK_SUCCESS + } + {%- when None %} + fun makeCall() : Int { + kotlinCallbackInterface.{{ meth.name()|fn_name }}( + {%- for arg in meth.arguments() %} + {{ arg|read_fn }}(argsBuf) + {%- if !loop.last %}, {% endif %} + {%- endfor %} + ) + return UNIFFI_CALLBACK_SUCCESS + } + {%- endmatch %} + + {%- match meth.throws_type() %} + {%- when None %} + fun makeCallAndHandleError() : Int = makeCall() + {%- when Some(error_type) %} + fun makeCallAndHandleError() : Int = try { + makeCall() + } catch (e: {{ error_type|error_type_name }}) { + // Expected error, serialize it into outBuf + outBuf.setValue({{ error_type|ffi_converter_name }}.lowerIntoRustBuffer(e)) + UNIFFI_CALLBACK_ERROR + } + {%- endmatch %} + + return makeCallAndHandleError() + } + {% endfor %} + + // Registers the foreign callback with the Rust side. + // This method is generated for each callback interface. + internal fun register(lib: _UniFFILib) { + lib.{{ ffi_init_callback.name() }}(this) + } +} + +internal val {{ callback_handler_obj }} = {{ callback_handler_class }}() diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt index 62a71e02f1..d0e0686322 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt @@ -1,7 +1,10 @@ +{{- self.add_import("java.util.concurrent.atomic.AtomicLong") }} +{{- self.add_import("java.util.concurrent.locks.ReentrantLock") }} +{{- self.add_import("kotlin.concurrent.withLock") }} + internal typealias Handle = Long internal class ConcurrentHandleMap( private val leftMap: MutableMap = mutableMapOf(), - private val rightMap: MutableMap = mutableMapOf() ) { private val lock = java.util.concurrent.locks.ReentrantLock() private val currentHandle = AtomicLong(0L) @@ -9,16 +12,14 @@ internal class ConcurrentHandleMap( fun insert(obj: T): Handle = lock.withLock { - rightMap[obj] ?: - currentHandle.getAndAdd(stride) - .also { handle -> - leftMap[handle] = obj - rightMap[obj] = handle - } + currentHandle.getAndAdd(stride) + .also { handle -> + leftMap[handle] = obj + } } fun get(handle: Handle) = lock.withLock { - leftMap[handle] + leftMap[handle] ?: throw InternalException("No callback in handlemap; this is a Uniffi bug") } fun delete(handle: Handle) { @@ -27,15 +28,12 @@ internal class ConcurrentHandleMap( fun remove(handle: Handle): T? = lock.withLock { - leftMap.remove(handle)?.let { obj -> - rightMap.remove(obj) - obj - } + leftMap.remove(handle) } } interface ForeignCallback : com.sun.jna.Callback { - public fun callback(handle: Handle, method: Int, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int + public fun invoke(handle: Handle, method: Int, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int } // Magic number for the Rust proxy to call using the same mechanism as every other method, @@ -46,29 +44,20 @@ internal const val UNIFFI_CALLBACK_SUCCESS = 0 internal const val UNIFFI_CALLBACK_ERROR = 1 internal const val UNIFFI_CALLBACK_UNEXPECTED_ERROR = 2 -public abstract class FfiConverterCallbackInterface( - protected val foreignCallback: ForeignCallback -): FfiConverter { - private val handleMap = ConcurrentHandleMap() +public abstract class FfiConverterCallbackInterface: FfiConverter { + internal val handleMap = ConcurrentHandleMap() - // 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() } + internal fun drop(handle: Handle) { + handleMap.remove(handle) } override fun lift(value: Handle): CallbackInterface { - return handleMap.get(value) ?: throw InternalException("No callback in handlemap; this is a Uniffi bug") + return handleMap.get(value) } 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 lower(value: CallbackInterface) = handleMap.insert(value) override fun allocationSize(value: CallbackInterface) = 8 diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt index 445252a9ca..59a127b1a2 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt @@ -1,128 +1,12 @@ {%- let cbi = ci|get_callback_interface_definition(name) %} -{%- let type_name = cbi|type_name %} -{%- let foreign_callback = format!("ForeignCallback{}", canonical_type_name) %} +{%- let callback_handler_class = format!("UniffiCallbackInterface{}", name) %} +{%- let callback_handler_obj = format!("uniffiCallbackInterface{}", name) %} +{%- let ffi_init_callback = cbi.ffi_init_callback() %} +{%- let interface_name = cbi|type_name %} +{%- let methods = cbi.methods() %} -{% 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 callback(handle: Handle, method: Int, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int { - val cb = {{ ffi_converter_name }}.lift(handle) - return when (method) { - IDX_CALLBACK_FREE -> { - {{ ffi_converter_name }}.drop(handle) - // Successful return - // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` - UNIFFI_CALLBACK_SUCCESS - } - {% 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_core/src/ffi/foreigncallbacks.rs` for info - try { - this.{{ method_name }}(cb, argsData, argsLen, outBuf) - } 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 - } - UNIFFI_CALLBACK_UNEXPECTED_ERROR - } - } - {% endfor %} - else -> { - // An unexpected error happened. - // See docs of ForeignCallback in `uniffi_core/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 - } - UNIFFI_CALLBACK_UNEXPECTED_ERROR - } - } - } - - {% for meth in cbi.methods() -%} - {% let method_name = format!("invoke_{}", meth.name())|fn_name %} - @Suppress("UNUSED_PARAMETER") - private fun {{ method_name }}(kotlinCallbackInterface: {{ type_name }}, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int { - {%- if meth.arguments().len() > 0 %} - val argsBuf = argsData.getByteBuffer(0, argsLen.toLong()).also { - it.order(ByteOrder.BIG_ENDIAN) - } - {%- endif %} - - {%- match meth.return_type() %} - {%- when Some with (return_type) %} - fun makeCall() : Int { - val returnValue = kotlinCallbackInterface.{{ meth.name()|fn_name }}( - {%- for arg in meth.arguments() %} - {{ arg|read_fn }}(argsBuf) - {% if !loop.last %}, {% endif %} - {%- endfor %} - ) - outBuf.setValue({{ return_type|ffi_converter_name }}.lowerIntoRustBuffer(returnValue)) - return UNIFFI_CALLBACK_SUCCESS - } - {%- when None %} - fun makeCall() : Int { - kotlinCallbackInterface.{{ meth.name()|fn_name }}( - {%- for arg in meth.arguments() %} - {{ arg|read_fn }}(argsBuf) - {%- if !loop.last %}, {% endif %} - {%- endfor %} - ) - return UNIFFI_CALLBACK_SUCCESS - } - {%- endmatch %} - - {%- match meth.throws_type() %} - {%- when None %} - fun makeCallAndHandleError() : Int = makeCall() - {%- when Some(error_type) %} - fun makeCallAndHandleError() : Int = try { - makeCall() - } catch (e: {{ error_type|error_type_name }}) { - // Expected error, serialize it into outBuf - outBuf.setValue({{ error_type|ffi_converter_name }}.lowerIntoRustBuffer(e)) - UNIFFI_CALLBACK_ERROR - } - {%- endmatch %} - - return makeCallAndHandleError() - } - {% endfor %} -} +{% include "Interface.kt" %} +{% include "CallbackInterfaceImpl.kt" %} // 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) - } - } -} +public object {{ ffi_converter_name }}: FfiConverterCallbackInterface<{{ interface_name }}>() diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt index a4da1a117a..6971a14a03 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt @@ -13,6 +13,7 @@ enum class {{ type_name }} { {% include "EnumVariantDocsTemplate.kt" %} {{ variant|variant_name }}{% if loop.last %};{% else %},{% endif %} {%- endfor %} + companion object } public object {{ e|ffi_converter_name }}: FfiConverterRustBuffer<{{ type_name }}> { @@ -41,7 +42,9 @@ sealed class {{ type_name }}{% if contains_object_references %}: Disposable {% e {% for field in variant.fields() -%} val {{ field.name()|var_name }}: {{ field|type_name}}{% if loop.last %}{% else %}, {% endif %} {% endfor -%} - ) : {{ type_name }}() + ) : {{ type_name }}() { + companion object + } {%- endif %} {% endfor %} @@ -61,6 +64,7 @@ sealed class {{ type_name }}{% if contains_object_references %}: Disposable {% e }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } } {% endif %} + companion object } public object {{ e|ffi_converter_name }} : FfiConverterRustBuffer<{{ type_name }}>{ diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ExternalTypeTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ExternalTypeTemplate.kt index 1e08de3b9e..0fade7a0bc 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ExternalTypeTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ExternalTypeTemplate.kt @@ -1,4 +1,4 @@ -{%- let package_name=self.external_type_package_name(module_path) %} +{%- let package_name=self.external_type_package_name(module_path, namespace) %} {%- 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) %} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt b/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt index 26926a17a5..382a5f7413 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt @@ -150,7 +150,12 @@ internal class UniFfiHandleMap { return map.get(handle) } - fun remove(handle: USize) { - map.remove(handle) + fun remove(handle: USize): T? { + return map.remove(handle) } } + +// FFI type for Rust future continuations +internal interface UniFffiRustFutureContinuationCallbackType : com.sun.jna.Callback { + fun callback(continuationHandle: USize, pollResult: Short); +} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/Interface.kt b/uniffi_bindgen/src/bindings/kotlin/templates/Interface.kt new file mode 100644 index 0000000000..2de84c102a --- /dev/null +++ b/uniffi_bindgen/src/bindings/kotlin/templates/Interface.kt @@ -0,0 +1,13 @@ +public interface {{ interface_name }} { + {% for meth in methods.iter() -%} + {%- include "FunctionDocsTemplate.kt" -%} + {% if meth.is_async() -%}suspend {% endif -%} + 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 %} + companion object +} + diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt index c8cbecb68b..5fb635342d 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt @@ -2,15 +2,14 @@ {%- let value_type_name = value_type|type_name %} public object {{ ffi_converter_name }}: FfiConverterRustBuffer> { 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 buildMap<{{ key_type_name }}, {{ value_type_name }}>(len) { + repeat(len) { + val k = {{ key_type|read_fn }}(buf) + val v = {{ value_type|read_fn }}(buf) + this[k] = v + } } - return items } override fun allocationSize(value: Map<{{ key_type_name }}, {{ value_type_name }}>): Int { diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectRuntime.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectRuntime.kt index b9352c690f..1e3ead5a7e 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectRuntime.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectRuntime.kt @@ -1,3 +1,5 @@ +{{- self.add_import("java.util.concurrent.atomic.AtomicLong") }} +{{- self.add_import("java.util.concurrent.atomic.AtomicBoolean") }} // Interface implemented by anything that can contain an object reference. // // Such types expose a `destroy()` method that must be called to cleanly diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt index b7fd7ace2b..ea1ca62ab7 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt @@ -1,34 +1,13 @@ {%- let obj = ci|get_object_definition(name) %} {%- 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") }} +{%- let (interface_name, impl_class_name) = obj|object_names %} +{%- let methods = obj.methods() %} -public interface {{ type_name }}Interface { - {% for meth in obj.methods() -%} - {%- let func = meth -%} - {%- include "FunctionDocsTemplate.kt" -%} - {%- match meth.throws_type() -%} - {%- when Some with (throwable) -%} - @Throws({{ throwable|error_type_name }}::class) - {%- when None -%} - {%- endmatch %} - {% if meth.is_async() -%} - suspend fun {{ meth.name()|fn_name }}({% call kt::arg_list_decl(meth) %}) - {%- else -%} - fun {{ meth.name()|fn_name }}({% call kt::arg_list_decl(meth) %}) - {%- endif %} - {%- match meth.return_type() -%} - {%- when Some with (return_type) %}: {{ return_type|type_name -}} - {%- when None -%} - {%- endmatch -%} +{% include "Interface.kt" %} - {% endfor %} -} - -{% let struct = obj %}{% include "StructureDocsTemplate.kt" %} -class {{ type_name }}( +class {{ impl_class_name }}( pointer: Pointer -) : FFIObject(pointer), {{ type_name }}Interface { +) : FFIObject(pointer), {{ interface_name }}{ {%- match obj.primary_constructor() %} {%- when Some with (cons) %} @@ -62,32 +41,31 @@ class {{ type_name }}( {%- if meth.is_async() %} @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") override suspend fun {{ meth.name()|fn_name }}({%- call kt::arg_list_decl(meth) -%}){% match meth.return_type() %}{% when Some with (return_type) %} : {{ return_type|type_name }}{% when None %}{%- endmatch %} { - // Create a new `CoroutineScope` for this operation, suspend the coroutine, and call the - // scaffolding function, passing it one of the callback handlers from `AsyncTypes.kt`. - return coroutineScope { - val scope = this - return@coroutineScope suspendCancellableCoroutine { continuation -> - try { - val callback = {{ meth.result_type().borrow()|future_callback_handler }}(continuation) - uniffiActiveFutureCallbacks.add(callback) - continuation.invokeOnCancellation { uniffiActiveFutureCallbacks.remove(callback) } - callWithPointer { thisPtr -> - rustCall { status -> - _UniFFILib.INSTANCE.{{ meth.ffi_func().name() }}( - thisPtr, - {% call kt::arg_list_lowered(meth) %} - FfiConverterForeignExecutor.lower(scope), - callback, - USize(0), - status, - ) - } - } - } catch (e: Exception) { - continuation.resumeWithException(e) - } - } - } + return uniffiRustCallAsync( + callWithPointer { thisPtr -> + _UniFFILib.INSTANCE.{{ meth.ffi_func().name() }}( + thisPtr, + {% call kt::arg_list_lowered(meth) %} + ) + }, + { future, continuation -> _UniFFILib.INSTANCE.{{ meth.ffi_rust_future_poll(ci) }}(future, continuation) }, + { future, status -> _UniFFILib.INSTANCE.{{ meth.ffi_rust_future_complete(ci) }}(future, status) }, + { future -> _UniFFILib.INSTANCE.{{ meth.ffi_rust_future_free(ci) }}(future) }, + // lift function + {%- match meth.return_type() %} + {%- when Some(return_type) %} + { {{ return_type|lift_fn }}(it) }, + {%- when None %} + { Unit }, + {% endmatch %} + // Error FFI converter + {%- match meth.throws_type() %} + {%- when Some(e) %} + {{ e|error_type_name }}.ErrorHandler, + {%- when None %} + NullCallStatusErrorHandler, + {%- endmatch %} + ) } {%- else -%} {%- match meth.return_type() -%} @@ -111,20 +89,39 @@ class {{ type_name }}( {% if !obj.alternate_constructors().is_empty() -%} companion object { {% for cons in obj.alternate_constructors() -%} - {%- let func = cons -%} {%- include "FunctionDocsTemplate.kt" %} - fun {{ cons.name()|fn_name }}({% call kt::arg_list_decl(cons) %}): {{ type_name }} = - {{ type_name }}({% call kt::to_ffi_call(cons) %}) + fun {{ cons.name()|fn_name }}({% call kt::arg_list_decl(cons) %}): {{ impl_class_name }} = + {{ impl_class_name }}({% call kt::to_ffi_call(cons) %}) {% endfor %} } + {% else %} + companion object {% endif %} } +{%- if obj.is_trait_interface() %} +{%- let callback_handler_class = format!("UniffiCallbackInterface{}", name) %} +{%- let callback_handler_obj = format!("uniffiCallbackInterface{}", name) %} +{%- let ffi_init_callback = obj.ffi_init_callback() %} +{% include "CallbackInterfaceImpl.kt" %} +{%- endif %} + public object {{ obj|ffi_converter_name }}: FfiConverter<{{ type_name }}, Pointer> { - override fun lower(value: {{ type_name }}): Pointer = value.callWithPointer { it } + {%- if obj.is_trait_interface() %} + internal val handleMap = ConcurrentHandleMap<{{ interface_name }}>() + {%- endif %} + + override fun lower(value: {{ type_name }}): Pointer { + {%- match obj.imp() %} + {%- when ObjectImpl::Struct %} + return value.callWithPointer { it } + {%- when ObjectImpl::Trait %} + return Pointer(handleMap.insert(value)) + {%- endmatch %} + } override fun lift(value: Pointer): {{ type_name }} { - return {{ type_name }}(value) + return {{ impl_class_name }}(value) } override fun read(buf: ByteBuffer): {{ type_name }} { diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt index 01f1f86c00..1dd0874177 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt @@ -17,6 +17,7 @@ data class {{ type_name }} ( {% call kt::destroy_fields(rec) %} } {% endif %} + companion object } public object {{ rec|ffi_converter_name }}: FfiConverterRustBuffer<{{ type_name }}> { diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt index a5c5a76a49..c111836ba8 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt @@ -7,29 +7,26 @@ @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") suspend fun {{ func.name()|fn_name }}({%- call kt::arg_list_decl(func) -%}){% match func.return_type() %}{% when Some with (return_type) %} : {{ return_type|type_name }}{% when None %}{%- endmatch %} { - // Create a new `CoroutineScope` for this operation, suspend the coroutine, and call the - // scaffolding function, passing it one of the callback handlers from `AsyncTypes.kt`. - return coroutineScope { - val scope = this - return@coroutineScope suspendCancellableCoroutine { continuation -> - try { - val callback = {{ func.result_type().borrow()|future_callback_handler }}(continuation) - uniffiActiveFutureCallbacks.add(callback) - continuation.invokeOnCancellation { uniffiActiveFutureCallbacks.remove(callback) } - rustCall { status -> - _UniFFILib.INSTANCE.{{ func.ffi_func().name() }}( - {% call kt::arg_list_lowered(func) %} - FfiConverterForeignExecutor.lower(scope), - callback, - USize(0), - status, - ) - } - } catch (e: Exception) { - continuation.resumeWithException(e) - } - } - } + return uniffiRustCallAsync( + _UniFFILib.INSTANCE.{{ func.ffi_func().name() }}({% call kt::arg_list_lowered(func) %}), + { future, continuation -> _UniFFILib.INSTANCE.{{ func.ffi_rust_future_poll(ci) }}(future, continuation) }, + { future, status -> _UniFFILib.INSTANCE.{{ func.ffi_rust_future_complete(ci) }}(future, status) }, + { future -> _UniFFILib.INSTANCE.{{ func.ffi_rust_future_free(ci) }}(future) }, + // lift function + {%- match func.return_type() %} + {%- when Some(return_type) %} + { {{ return_type|lift_fn }}(it) }, + {%- when None %} + { Unit }, + {% endmatch %} + // Error FFI converter + {%- match func.throws_type() %} + {%- when Some(e) %} + {{ e|error_type_name }}.ErrorHandler, + {%- when None %} + NullCallStatusErrorHandler, + {%- endmatch %} + ) } {%- else %} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt b/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt index a9f75a0c3b..89546eac92 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt @@ -94,7 +94,7 @@ {%- when Type::Custom { module_path, name, builtin } %} {% include "CustomTypeTemplate.kt" %} -{%- when Type::External { module_path, name, kind } %} +{%- when Type::External { module_path, name, namespace, kind, tagged } %} {% include "ExternalTypeTemplate.kt" %} {%- else %} @@ -102,5 +102,8 @@ {%- endfor %} {%- if ci.has_async_fns() %} -{% include "AsyncTypes.kt" %} +{# Import types needed for async support #} +{{ self.add_import("kotlin.coroutines.resume") }} +{{ self.add_import("kotlinx.coroutines.suspendCancellableCoroutine") }} +{{ self.add_import("kotlinx.coroutines.CancellableContinuation") }} {%- endif %} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/wrapper.kt b/uniffi_bindgen/src/bindings/kotlin/templates/wrapper.kt index e09c4e7001..a5585c7211 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/wrapper.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/wrapper.kt @@ -42,6 +42,11 @@ import java.util.concurrent.ConcurrentHashMap // and the FFI Function declarations in a com.sun.jna.Library. {% include "NamespaceLibraryTemplate.kt" %} +// Async support +{%- if ci.has_async_fns() %} +{% include "Async.kt" %} +{%- endif %} + // Public interface members begin here. {{ type_helper_code }} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs index 35b17e3009..9199bbefd0 100644 --- a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs +++ b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs @@ -75,6 +75,8 @@ pub struct Config { cdylib_name: Option, #[serde(default)] custom_types: HashMap, + #[serde(default)] + external_packages: HashMap, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -94,11 +96,19 @@ impl Config { "uniffi".into() } } + + /// Get the package name for a given external namespace. + pub fn module_for_namespace(&self, ns: &str) -> String { + let ns = ns.to_string().to_snake_case(); + match self.external_packages.get(&ns) { + None => format!(".{ns}"), + Some(value) if value.is_empty() => ns, + Some(value) => format!("{value}.{ns}"), + } + } } impl BindingsConfig for Config { - const TOML_KEY: &'static str = "python"; - fn update_from_ci(&mut self, ci: &ComponentInterface) { self.cdylib_name .get_or_insert_with(|| format!("uniffi_{}", ci.namespace())); @@ -311,13 +321,24 @@ impl PythonCodeOracle { // Pointer to an `asyncio.EventLoop` instance FfiType::ForeignExecutorHandle => "ctypes.c_size_t".to_string(), FfiType::ForeignExecutorCallback => "_UNIFFI_FOREIGN_EXECUTOR_CALLBACK_T".to_string(), - FfiType::FutureCallback { return_type } => { - format!( - "_uniffi_future_callback_t({})", - Self::ffi_type_label(return_type), - ) + FfiType::RustFutureHandle => "ctypes.c_void_p".to_string(), + FfiType::RustFutureContinuationCallback => "_UNIFFI_FUTURE_CONTINUATION_T".to_string(), + FfiType::RustFutureContinuationData => "ctypes.c_size_t".to_string(), + } + } + + /// Get the name of the protocol and class name for an object. + /// + /// For struct impls, the class name is the object name and the protocol name is derived from that. + /// For trait impls, the protocol name is the object name, and the class name is derived from that. + fn object_names(&self, obj: &Object) -> (String, String) { + let class_name = self.class_name(obj.name()); + match obj.imp() { + ObjectImpl::Struct => (format!("{class_name}Protocol"), class_name), + ObjectImpl::Trait => { + let protocol_name = format!("{class_name}Impl"); + (class_name, protocol_name) } - FfiType::FutureCallbackData => "ctypes.c_size_t".to_string(), } } } @@ -408,25 +429,14 @@ pub mod filters { Ok(format!("{}.write", ffi_converter_name(as_ct)?)) } - // Name of the callback function we pass to Rust to complete an async call - pub fn async_callback_fn(result_type: &ResultType) -> Result { - let return_string = match &result_type.return_type { - Some(t) => PythonCodeOracle.find(t).canonical_name().to_snake_case(), - None => "void".into(), - }; - let throws_string = match &result_type.throws_type { - Some(t) => PythonCodeOracle.find(t).canonical_name().to_snake_case(), - None => "void".into(), - }; - Ok(format!( - "_uniffi_async_callback_{return_string}__{throws_string}" - )) - } - pub fn literal_py(literal: &Literal, as_ct: &impl AsCodeType) -> Result { Ok(as_ct.as_codetype().literal(literal)) } + pub fn ffi_type(type_: &Type) -> Result { + Ok(type_.into()) + } + /// Get the Python syntax for representing a given low-level `FfiType`. pub fn ffi_type_name(type_: &FfiType) -> Result { Ok(PythonCodeOracle::ffi_type_label(type_)) @@ -451,4 +461,9 @@ pub mod filters { pub fn enum_variant_py(nm: &str) -> Result { Ok(PythonCodeOracle.enum_variant_name(nm)) } + + /// Get the idiomatic Python rendering of an individual enum variant. + pub fn object_names(obj: &Object) -> Result<(String, String), askama::Error> { + Ok(PythonCodeOracle.object_names(obj)) + } } diff --git a/uniffi_bindgen/src/bindings/python/templates/Async.py b/uniffi_bindgen/src/bindings/python/templates/Async.py new file mode 100644 index 0000000000..82aa534b46 --- /dev/null +++ b/uniffi_bindgen/src/bindings/python/templates/Async.py @@ -0,0 +1,40 @@ +# RustFuturePoll values +_UNIFFI_RUST_FUTURE_POLL_READY = 0 +_UNIFFI_RUST_FUTURE_POLL_MAYBE_READY = 1 + +# Stores futures for _uniffi_continuation_callback +_UniffiContinuationPointerManager = _UniffiPointerManager() + +# Continuation callback for async functions +# lift the return value or error and resolve the future, causing the async function to resume. +@_UNIFFI_FUTURE_CONTINUATION_T +def _uniffi_continuation_callback(future_ptr, poll_code): + (eventloop, future) = _UniffiContinuationPointerManager.release_pointer(future_ptr) + eventloop.call_soon_threadsafe(_uniffi_set_future_result, future, poll_code) + +def _uniffi_set_future_result(future, poll_code): + if not future.cancelled(): + future.set_result(poll_code) + +async def _uniffi_rust_call_async(rust_future, ffi_poll, ffi_complete, ffi_free, lift_func, error_ffi_converter): + try: + eventloop = asyncio.get_running_loop() + + # Loop and poll until we see a _UNIFFI_RUST_FUTURE_POLL_READY value + while True: + future = eventloop.create_future() + ffi_poll( + rust_future, + _UniffiContinuationPointerManager.new_pointer((eventloop, future)), + ) + poll_code = await future + if poll_code == _UNIFFI_RUST_FUTURE_POLL_READY: + break + + return lift_func( + _rust_call_with_error(error_ffi_converter, ffi_complete, rust_future) + ) + finally: + ffi_free(rust_future) + +_UniffiLib.{{ ci.ffi_rust_future_continuation_callback_set().name() }}(_uniffi_continuation_callback) diff --git a/uniffi_bindgen/src/bindings/python/templates/AsyncTypes.py b/uniffi_bindgen/src/bindings/python/templates/AsyncTypes.py deleted file mode 100644 index c85891defe..0000000000 --- a/uniffi_bindgen/src/bindings/python/templates/AsyncTypes.py +++ /dev/null @@ -1,36 +0,0 @@ -# Callback handlers for async returns - -_UniffiPyFuturePointerManager = _UniffiPointerManager() - -# Callback handlers for an async calls. These are invoked by Rust when the future is ready. They -# lift the return value or error and resolve the future, causing the async function to resume. -{%- for result_type in ci.iter_async_result_types() %} -@_uniffi_future_callback_t( - {%- match result_type.return_type -%} - {%- when Some(return_type) -%} - {{ return_type|ffi_type|ffi_type_name }} - {%- when None -%} - ctypes.c_uint8 - {%- endmatch -%} -) -def {{ result_type|async_callback_fn }}(future_ptr, result, call_status): - future = _UniffiPyFuturePointerManager.release_pointer(future_ptr) - if future.cancelled(): - return - try: - {%- match result_type.throws_type %} - {%- when Some(throws_type) %} - _uniffi_check_call_status({{ throws_type|ffi_converter_name }}, call_status) - {%- when None %} - _uniffi_check_call_status(None, call_status) - {%- endmatch %} - - {%- match result_type.return_type %} - {%- when Some(return_type) %} - future.set_result({{ return_type|lift_fn }}(result)) - {%- when None %} - future.set_result(None) - {%- endmatch %} - except BaseException as e: - future.set_exception(e) -{%- endfor %} diff --git a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py new file mode 100644 index 0000000000..f9a4768a5c --- /dev/null +++ b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py @@ -0,0 +1,91 @@ +{% if self.include_once_check("CallbackInterfaceRuntime.py") %}{% include "CallbackInterfaceRuntime.py" %}{% endif %} + +# Declaration and _UniffiConverters for {{ type_name }} Callback Interface + +def {{ callback_handler_class }}(handle, method, args_data, args_len, buf_ptr): + {% for meth in methods.iter() -%} + {% let method_name = format!("invoke_{}", meth.name())|fn_name %} + def {{ method_name }}(python_callback, args_stream, buf_ptr): + {#- Unpacking args from the _UniffiRustBuffer #} + def makeCall(): + {#- Calling the concrete callback object #} + {%- if meth.arguments().len() != 0 -%} + return python_callback.{{ meth.name()|fn_name }}( + {% for arg in meth.arguments() -%} + {{ arg|read_fn }}(args_stream) + {%- if !loop.last %}, {% endif %} + {% endfor -%} + ) + {%- else %} + return python_callback.{{ meth.name()|fn_name }}() + {%- endif %} + + def makeCallAndHandleReturn(): + {%- match meth.return_type() %} + {%- when Some(return_type) %} + rval = makeCall() + with _UniffiRustBuffer.alloc_with_builder() as builder: + {{ return_type|write_fn }}(rval, builder) + buf_ptr[0] = builder.finalize() + {%- when None %} + makeCall() + {%- endmatch %} + return _UNIFFI_CALLBACK_SUCCESS + + {%- match meth.throws_type() %} + {%- when None %} + return makeCallAndHandleReturn() + {%- when Some(err) %} + try: + return makeCallAndHandleReturn() + except {{ err|type_name }} as e: + # Catch errors declared in the UDL file + with _UniffiRustBuffer.alloc_with_builder() as builder: + {{ err|write_fn }}(e, builder) + buf_ptr[0] = builder.finalize() + return _UNIFFI_CALLBACK_ERROR + {%- endmatch %} + + {% endfor %} + + cb = {{ ffi_converter_name }}._handle_map.get(handle) + + if method == IDX_CALLBACK_FREE: + {{ ffi_converter_name }}._handle_map.remove(handle) + + # Successfull return + # See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` + return _UNIFFI_CALLBACK_SUCCESS + + {% for meth in methods.iter() -%} + {% 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_core/src/ffi/foreigncallbacks.rs` for details + try: + return {{ method_name }}(cb, _UniffiRustBufferStream(args_data, args_len), buf_ptr) + 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 _UNIFFI_CALLBACK_UNEXPECTED_ERROR + {% 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_core/src/ffi/foreigncallbacks.rs` + return _UNIFFI_CALLBACK_UNEXPECTED_ERROR + +# 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. +{{ callback_handler_obj }} = _UNIFFI_FOREIGN_CALLBACK_T({{ callback_handler_class }}) +_UniffiLib.{{ ffi_init_callback.name() }}({{ callback_handler_obj }}) diff --git a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceRuntime.py b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceRuntime.py index 0fe2ab8dc0..1b7346ba4c 100644 --- a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceRuntime.py +++ b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceRuntime.py @@ -8,33 +8,29 @@ class ConcurrentHandleMap: 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 + handle = self._current_handle + self._current_handle += self._stride + self._left_map[handle] = obj + return handle def get(self, handle): with self._lock: - return self._left_map.get(handle) + obj = self._left_map.get(handle) + if not obj: + raise InternalError("No callback in handlemap; this is a uniffi bug") + return obj 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, @@ -45,22 +41,12 @@ def remove(self, handle): _UNIFFI_CALLBACK_ERROR = 1 _UNIFFI_CALLBACK_UNEXPECTED_ERROR = 2 -class _UniffiConverterCallbackInterface: +class UniffiCallbackInterfaceFfiConverter: _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 + return cls._handle_map.get(handle) @classmethod def read(cls, buf): diff --git a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py index e0e926757a..dbfa094562 100644 --- a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py @@ -1,105 +1,12 @@ -{%- let cbi = ci|get_callback_interface_definition(id) %} -{%- let foreign_callback = format!("foreignCallback{}", canonical_type_name) %} +{%- let cbi = ci|get_callback_interface_definition(name) %} +{%- let callback_handler_class = format!("UniffiCallbackInterface{}", name) %} +{%- let callback_handler_obj = format!("uniffiCallbackInterface{}", name) %} +{%- let ffi_init_callback = cbi.ffi_init_callback() %} +{%- let protocol_name = type_name.clone() %} +{%- let methods = cbi.methods() %} -{% if self.include_once_check("CallbackInterfaceRuntime.py") %}{% include "CallbackInterfaceRuntime.py" %}{% endif %} - -# Declaration and _UniffiConverters for {{ type_name }} Callback Interface - -class {{ type_name }}: - {% for meth in cbi.methods() -%} - def {{ meth.name()|fn_name }}(self, {% call py::arg_list_decl(meth) %}): - raise NotImplementedError - - {% endfor %} - -def py_{{ foreign_callback }}(handle, method, args_data, args_len, buf_ptr): - {% for meth in cbi.methods() -%} - {% let method_name = format!("invoke_{}", meth.name())|fn_name %} - def {{ method_name }}(python_callback, args_stream, buf_ptr): - {#- Unpacking args from the _UniffiRustBuffer #} - def makeCall(): - {#- Calling the concrete callback object #} - {%- if meth.arguments().len() != 0 -%} - return python_callback.{{ meth.name()|fn_name }}( - {% for arg in meth.arguments() -%} - {{ arg|read_fn }}(args_stream) - {%- if !loop.last %}, {% endif %} - {% endfor -%} - ) - {%- else %} - return python_callback.{{ meth.name()|fn_name }}() - {%- endif %} - - def makeCallAndHandleReturn(): - {%- match meth.return_type() %} - {%- when Some(return_type) %} - rval = makeCall() - with _UniffiRustBuffer.alloc_with_builder() as builder: - {{ return_type|write_fn }}(rval, builder) - buf_ptr[0] = builder.finalize() - {%- when None %} - makeCall() - {%- endmatch %} - return _UNIFFI_CALLBACK_SUCCESS - - {%- match meth.throws_type() %} - {%- when None %} - return makeCallAndHandleReturn() - {%- when Some(err) %} - try: - return makeCallAndHandleReturn() - except {{ err|type_name }} as e: - # Catch errors declared in the UDL file - with _UniffiRustBuffer.alloc_with_builder() as builder: - {{ err|write_fn }}(e, builder) - buf_ptr[0] = builder.finalize() - return _UNIFFI_CALLBACK_ERROR - {%- 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) - # Successfull return - # See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` - return _UNIFFI_CALLBACK_SUCCESS - - {% 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_core/src/ffi/foreigncallbacks.rs` for details - try: - return {{ method_name }}(cb, _UniffiRustBufferStream(args_data, args_len), buf_ptr) - 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 _UNIFFI_CALLBACK_UNEXPECTED_ERROR - {% 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_core/src/ffi/foreigncallbacks.rs` - return _UNIFFI_CALLBACK_UNEXPECTED_ERROR - -# 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 }} = _UNIFFI_FOREIGN_CALLBACK_T(py_{{ foreign_callback }}) -_rust_call(lambda err: _UniffiLib.{{ cbi.ffi_init_callback().name() }}({{ foreign_callback }}, err)) +{% include "Protocol.py" %} +{% include "CallbackInterfaceImpl.py" %} # The _UniffiConverter which transforms the Callbacks in to Handles to pass to Rust. -{{ ffi_converter_name }} = _UniffiConverterCallbackInterface({{ foreign_callback }}) +{{ ffi_converter_name }} = UniffiCallbackInterfaceFfiConverter() diff --git a/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py index 651431db58..26e4639e19 100644 --- a/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py @@ -1,8 +1,9 @@ -{%- let mod_name = module_path|fn_name %} +{%- let module = python_config.module_for_namespace(namespace) -%} +# External type {{name}} is in namespace "{{namespace}}", crate {{module_path}} {%- let ffi_converter_name = "_UniffiConverterType{}"|format(name) %} -{{ self.add_import_of(mod_name, ffi_converter_name) }} -{{ self.add_import_of(mod_name, name) }} {#- import the type alias itself -#} +{{ self.add_import_of(module, ffi_converter_name) }} +{{ self.add_import_of(module, name) }} {#- import the type alias itself -#} {%- let rustbuffer_local_name = "_UniffiRustBuffer{}"|format(name) %} -{{ self.add_import_of_as(mod_name, "_UniffiRustBuffer", rustbuffer_local_name) }} +{{ self.add_import_of_as(module, "_UniffiRustBuffer", rustbuffer_local_name) }} diff --git a/uniffi_bindgen/src/bindings/python/templates/Helpers.py b/uniffi_bindgen/src/bindings/python/templates/Helpers.py index 356fe9786b..dca962f176 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Helpers.py +++ b/uniffi_bindgen/src/bindings/python/templates/Helpers.py @@ -44,25 +44,6 @@ def _rust_call_with_error(error_ffi_converter, fn, *args): _uniffi_check_call_status(error_ffi_converter, call_status) return result -def _rust_call_async(scaffolding_fn, callback_fn, *args): - # Call the scaffolding function, passing it a callback handler for `AsyncTypes.py` and a pointer - # to a python Future object. The async function then awaits the Future. - uniffi_eventloop = asyncio.get_running_loop() - uniffi_py_future = uniffi_eventloop.create_future() - uniffi_call_status = _UniffiRustCallStatus(code=_UniffiRustCallStatus.CALL_SUCCESS, error_buf=_UniffiRustBuffer(0, 0, None)) - scaffolding_fn(*args, - _UniffiConverterForeignExecutor._pointer_manager.new_pointer(uniffi_eventloop), - callback_fn, - # Note: It's tempting to skip the pointer manager and just use a `py_object` pointing to a - # local variable like we do in Swift. However, Python doesn't use cooperative cancellation - # -- asyncio can cancel a task at anytime. This means if we use a local variable, the Rust - # callback could fire with a dangling pointer. - _UniffiPyFuturePointerManager.new_pointer(uniffi_py_future), - ctypes.byref(uniffi_call_status), - ) - _uniffi_check_call_status(None, uniffi_call_status) - return uniffi_py_future - def _uniffi_check_call_status(error_ffi_converter, call_status): if call_status.code == _UniffiRustCallStatus.CALL_SUCCESS: pass @@ -88,3 +69,7 @@ def _uniffi_check_call_status(error_ffi_converter, call_status): # A function pointer for a callback as defined by UniFFI. # Rust definition `fn(handle: u64, method: u32, args: _UniffiRustBuffer, buf_ptr: *mut _UniffiRustBuffer) -> int` _UNIFFI_FOREIGN_CALLBACK_T = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_ulonglong, ctypes.c_ulong, ctypes.POINTER(ctypes.c_char), ctypes.c_int, ctypes.POINTER(_UniffiRustBuffer)) + +# UniFFI future continuation +_UNIFFI_FUTURE_CONTINUATION_T = ctypes.CFUNCTYPE(None, ctypes.c_size_t, ctypes.c_int8) + diff --git a/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py index 037b7b401b..09d439651d 100644 --- a/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py @@ -1,6 +1,10 @@ {%- let obj = ci|get_object_definition(name) %} +{%- let (protocol_name, impl_name) = obj|object_names %} +{%- let methods = obj.methods() %} -class {{ type_name }}: {% let struct = obj %}{% include "StructureDocsTemplate.py" %} +{% include "Protocol.py" %} + +class {{ impl_name }}: _pointer: ctypes.c_void_p {%- match obj.primary_constructor() %} @@ -68,25 +72,40 @@ def __ne__(self, other: object) -> {{ ne.return_type().unwrap()|type_name }}: {% endmatch %} {% endfor %} +{%- if obj.is_trait_interface() %} +{%- let callback_handler_class = format!("UniffiCallbackInterface{}", name) %} +{%- let callback_handler_obj = format!("uniffiCallbackInterface{}", name) %} +{%- let ffi_init_callback = obj.ffi_init_callback() %} +{% include "CallbackInterfaceImpl.py" %} +{%- endif %} class {{ ffi_converter_name }}: + {%- if obj.is_trait_interface() %} + _handle_map = ConcurrentHandleMap() + {%- endif %} + + @staticmethod + def lift(value: int): + return {{ impl_name }}._make_instance_(value) + + @staticmethod + def lower(value: {{ protocol_name }}): + {%- match obj.imp() %} + {%- when ObjectImpl::Struct %} + if not isinstance(value, {{ impl_name }}): + raise TypeError("Expected {{ impl_name }} instance, {} found".format(type(value).__name__)) + return value._pointer + {%- when ObjectImpl::Trait %} + return {{ ffi_converter_name }}._handle_map.insert(value) + {%- endmatch %} + @classmethod - def read(cls, buf): + def read(cls, buf: _UniffiRustBuffer): ptr = buf.read_u64() 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(type(value).__name__)) + def write(cls, value: {{ protocol_name }}, buf: _UniffiRustBuffer): buf.write_u64(cls.lower(value)) - - @staticmethod - def lift(value): - return {{ type_name }}._make_instance_(value) - - @staticmethod - def lower(value): - return value._pointer \ No newline at end of file diff --git a/uniffi_bindgen/src/bindings/python/templates/Protocol.py b/uniffi_bindgen/src/bindings/python/templates/Protocol.py new file mode 100644 index 0000000000..63e22769fb --- /dev/null +++ b/uniffi_bindgen/src/bindings/python/templates/Protocol.py @@ -0,0 +1,7 @@ +class {{ protocol_name }}(typing.Protocol): + {%- for meth in methods.iter() %} + def {{ meth.name()|fn_name }}(self, {% call py::arg_list_decl(meth) %}): + raise NotImplementedError + {%- else %} + pass + {%- endfor %} diff --git a/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py b/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py index 175d67f82f..8eea929403 100644 --- a/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py @@ -1,11 +1,25 @@ {%- if func.is_async() %} -async def {{ func.name()|fn_name }}({%- call py::arg_list_decl(func) -%}): - {%- call py::setup_args(func) %} - return await _rust_call_async( - _UniffiLib.{{ func.ffi_func().name() }}, - {{ func.result_type().borrow()|async_callback_fn }}, - {% call py::arg_list_lowered(func) %} +def {{ func.name()|fn_name }}({%- call py::arg_list_decl(func) -%}): + return _uniffi_rust_call_async( + _UniffiLib.{{ func.ffi_func().name() }}({% call py::arg_list_lowered(func) %}), + _UniffiLib.{{func.ffi_rust_future_poll(ci) }}, + _UniffiLib.{{func.ffi_rust_future_complete(ci) }}, + _UniffiLib.{{func.ffi_rust_future_free(ci) }}, + # lift function + {%- match func.return_type() %} + {%- when Some(return_type) %} + {{ return_type|lift_fn }}, + {%- when None %} + lambda val: None, + {% endmatch %} + # Error FFI converter + {%- match func.throws_type() %} + {%- when Some(e) %} + {{ e|ffi_converter_name }}, + {%- when None %} + None, + {%- endmatch %} ) {%- else %} diff --git a/uniffi_bindgen/src/bindings/python/templates/Types.py b/uniffi_bindgen/src/bindings/python/templates/Types.py index a2b29f799e..84afa6bbff 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Types.py +++ b/uniffi_bindgen/src/bindings/python/templates/Types.py @@ -85,13 +85,13 @@ {%- when Type::Map { key_type, value_type } %} {%- include "MapTemplate.py" %} -{%- when Type::CallbackInterface { name: id, module_path } %} +{%- when Type::CallbackInterface { name, module_path } %} {%- include "CallbackInterfaceTemplate.py" %} {%- when Type::Custom { name, module_path, builtin } %} {%- include "CustomType.py" %} -{%- when Type::External { name, module_path, kind } %} +{%- when Type::External { name, module_path, namespace, kind, tagged } %} {%- include "ExternalTemplate.py" %} {%- when Type::ForeignExecutor %} @@ -100,7 +100,3 @@ {%- else %} {%- endmatch %} {%- endfor %} - -{%- if ci.has_async_fns() %} -{%- include "AsyncTypes.py" %} -{%- endif %} diff --git a/uniffi_bindgen/src/bindings/python/templates/macros.py b/uniffi_bindgen/src/bindings/python/templates/macros.py index bc4f121653..ef3b1bb94d 100644 --- a/uniffi_bindgen/src/bindings/python/templates/macros.py +++ b/uniffi_bindgen/src/bindings/python/templates/macros.py @@ -100,13 +100,29 @@ {%- macro method_decl(py_method_name, meth) %} {% if meth.is_async() %} - async def {{ py_method_name }}(self, {% call arg_list_decl(meth) %}): + def {{ py_method_name }}(self, {% call arg_list_decl(meth) %}): {%- call setup_args_extra_indent(meth) %} - return await _rust_call_async( - _UniffiLib.{{ func.ffi_func().name() }}, - {{ func.result_type().borrow()|async_callback_fn }}, - self._pointer, - {% call arg_list_lowered(func) %} + return _uniffi_rust_call_async( + _UniffiLib.{{ meth.ffi_func().name() }}( + self._pointer, {% call arg_list_lowered(meth) %} + ), + _UniffiLib.{{ meth.ffi_rust_future_poll(ci) }}, + _UniffiLib.{{ meth.ffi_rust_future_complete(ci) }}, + _UniffiLib.{{ meth.ffi_rust_future_free(ci) }}, + # lift function + {%- match meth.return_type() %} + {%- when Some(return_type) %} + {{ return_type|lift_fn }}, + {%- when None %} + lambda val: None, + {% endmatch %} + # Error FFI converter + {%- match meth.throws_type() %} + {%- when Some(e) %} + {{ e|ffi_converter_name }}, + {%- when None %} + None, + {%- endmatch %} ) {%- else -%} diff --git a/uniffi_bindgen/src/bindings/python/templates/wrapper.py b/uniffi_bindgen/src/bindings/python/templates/wrapper.py index 6fb88dcaee..24c3290ff7 100644 --- a/uniffi_bindgen/src/bindings/python/templates/wrapper.py +++ b/uniffi_bindgen/src/bindings/python/templates/wrapper.py @@ -40,6 +40,11 @@ # Contains loading, initialization code, and the FFI Function declarations. {% include "NamespaceLibraryTemplate.py" %} +# Async support +{%- if ci.has_async_fns() %} +{%- include "Async.py" %} +{%- endif %} + # Public interface members begin here. {{ type_helper_code }} diff --git a/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs b/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs index 4380cc378c..e6defe65cc 100644 --- a/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs +++ b/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs @@ -104,8 +104,6 @@ impl Config { } impl BindingsConfig for Config { - const TOML_KEY: &'static str = "ruby"; - fn update_from_ci(&mut self, ci: &ComponentInterface) { self.cdylib_name .get_or_insert_with(|| format!("uniffi_{}", ci.namespace())); @@ -155,14 +153,18 @@ mod filters { 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"), + // Callback interfaces are not yet implemented, but this needs to return something in + // order for the coverall tests to pass. + FfiType::ForeignCallback => ":pointer".to_string(), FfiType::ForeignExecutorCallback => { unimplemented!("Foreign executors are not implemented") } FfiType::ForeignExecutorHandle => { unimplemented!("Foreign executors are not implemented") } - FfiType::FutureCallback { .. } | FfiType::FutureCallbackData => { + FfiType::RustFutureHandle + | FfiType::RustFutureContinuationCallback + | FfiType::RustFutureContinuationData => { unimplemented!("Async functions are not implemented") } }) diff --git a/uniffi_bindgen/src/bindings/ruby/templates/NamespaceLibraryTemplate.rb b/uniffi_bindgen/src/bindings/ruby/templates/NamespaceLibraryTemplate.rb index 858b42bf91..8536fc322e 100644 --- a/uniffi_bindgen/src/bindings/ruby/templates/NamespaceLibraryTemplate.rb +++ b/uniffi_bindgen/src/bindings/ruby/templates/NamespaceLibraryTemplate.rb @@ -9,7 +9,7 @@ module UniFFILib ffi_lib '{{ config.cdylib_name() }}' {% endif %} - {% for func in ci.iter_ffi_function_definitions() -%} + {% for func in ci.iter_ffi_function_definitions_non_async() -%} 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 %} diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift/callback_interface.rs b/uniffi_bindgen/src/bindings/swift/gen_swift/callback_interface.rs index 99d503f881..b25e3fb6dc 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift/callback_interface.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift/callback_interface.rs @@ -6,21 +6,25 @@ use crate::backend::CodeType; #[derive(Debug)] pub struct CallbackInterfaceCodeType { - id: String, + name: String, } impl CallbackInterfaceCodeType { - pub fn new(id: String) -> Self { - Self { id } + pub fn new(name: String) -> Self { + Self { name } } } impl CodeType for CallbackInterfaceCodeType { fn type_label(&self) -> String { - super::SwiftCodeOracle.class_name(&self.id) + super::SwiftCodeOracle.class_name(&self.name) } fn canonical_name(&self) -> String { format!("CallbackInterface{}", self.type_label()) } + + fn initialization_fn(&self) -> Option { + Some(format!("uniffiCallbackInit{}", self.name)) + } } diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs index f85453c9ff..0bab0cf52e 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs @@ -204,8 +204,6 @@ impl Config { } impl BindingsConfig for Config { - const TOML_KEY: &'static str = "swift"; - fn update_from_ci(&mut self, ci: &ComponentInterface) { self.module_name .get_or_insert_with(|| ci.namespace().into()); @@ -342,6 +340,7 @@ pub struct SwiftWrapper<'a> { ci: &'a ComponentInterface, type_helper_code: String, type_imports: BTreeSet, + has_async_fns: bool, } impl<'a> SwiftWrapper<'a> { pub fn new(config: Config, ci: &'a ComponentInterface) -> Self { @@ -353,6 +352,7 @@ impl<'a> SwiftWrapper<'a> { ci, type_helper_code, type_imports, + has_async_fns: ci.has_async_fns(), } } @@ -365,6 +365,10 @@ impl<'a> SwiftWrapper<'a> { .iter_types() .map(|t| SwiftCodeOracle.find(t)) .filter_map(|ct| ct.initialization_fn()) + .chain( + self.has_async_fns + .then(|| "uniffiInitContinuationCallback".into()), + ) .collect() } } @@ -400,7 +404,7 @@ impl SwiftCodeOracle { Type::Duration => Box::new(miscellany::DurationCodeType), Type::Enum { name, .. } => Box::new(enum_::EnumCodeType::new(name)), - Type::Object { name, .. } => Box::new(object::ObjectCodeType::new(name)), + Type::Object { name, imp, .. } => Box::new(object::ObjectCodeType::new(name, imp)), Type::Record { name, .. } => Box::new(record::RecordCodeType::new(name)), Type::CallbackInterface { name, .. } => { Box::new(callback_interface::CallbackInterfaceCodeType::new(name)) @@ -463,10 +467,10 @@ impl SwiftCodeOracle { FfiType::ForeignCallback => "ForeignCallback".into(), FfiType::ForeignExecutorHandle => "Int".into(), FfiType::ForeignExecutorCallback => "ForeignExecutorCallback".into(), - FfiType::FutureCallback { return_type } => { - format!("UniFfiFutureCallback{}", self.ffi_type_label(return_type)) + FfiType::RustFutureContinuationCallback => "UniFfiRustFutureContinuation".into(), + FfiType::RustFutureHandle | FfiType::RustFutureContinuationData => { + "UnsafeMutableRawPointer".into() } - FfiType::FutureCallbackData => "UnsafeMutableRawPointer".into(), } } @@ -474,7 +478,9 @@ impl SwiftCodeOracle { match ffi_type { FfiType::ForeignCallback | FfiType::ForeignExecutorCallback - | FfiType::FutureCallback { .. } => { + | FfiType::RustFutureHandle + | FfiType::RustFutureContinuationCallback + | FfiType::RustFutureContinuationData => { format!("{} _Nonnull", self.ffi_type_label_raw(ffi_type)) } _ => self.ffi_type_label_raw(ffi_type), @@ -484,6 +490,25 @@ impl SwiftCodeOracle { fn ffi_canonical_name(&self, ffi_type: &FfiType) -> String { self.ffi_type_label_raw(ffi_type) } + + /// Get the name of the protocol and class name for an object. + /// + /// For struct impls, the class name is the object name and the protocol name is derived from that. + /// For trait impls, the protocol name is the object name, and the class name is derived from that. + /// + /// This split is needed because of the `FfiConverter` protocol. For struct impls, `lower` + /// can only lower the concrete class. For trait impls, `lower` can lower anything that + /// implement the protocol. + fn object_names(&self, obj: &Object) -> (String, String) { + let class_name = self.class_name(obj.name()); + match obj.imp() { + ObjectImpl::Struct => (format!("{class_name}Protocol"), class_name), + ObjectImpl::Trait => { + let protocol_name = format!("{class_name}Impl"); + (class_name, protocol_name) + } + } + } } pub mod filters { @@ -558,11 +583,12 @@ pub mod filters { FfiType::ForeignCallback => "ForeignCallback _Nonnull".into(), FfiType::ForeignExecutorCallback => "UniFfiForeignExecutorCallback _Nonnull".into(), FfiType::ForeignExecutorHandle => "size_t".into(), - FfiType::FutureCallback { return_type } => format!( - "UniFfiFutureCallback{} _Nonnull", - SwiftCodeOracle.ffi_type_label_raw(return_type) - ), - FfiType::FutureCallbackData => "void* _Nonnull".into(), + FfiType::RustFutureContinuationCallback => { + "UniFfiRustFutureContinuation _Nonnull".into() + } + FfiType::RustFutureHandle | FfiType::RustFutureContinuationData => { + "void* _Nonnull".into() + } }) } @@ -619,13 +645,7 @@ pub mod filters { )) } - pub fn future_continuation_type(result: &ResultType) -> Result { - Ok(format!( - "CheckedContinuation<{}, Error>", - match &result.return_type { - Some(return_type) => type_name(return_type)?, - None => "()".into(), - } - )) + pub fn object_names(obj: &Object) -> Result<(String, String), askama::Error> { + Ok(SwiftCodeOracle.object_names(obj)) } } diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift/object.rs b/uniffi_bindgen/src/bindings/swift/gen_swift/object.rs index 241d694fce..02f2567d97 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift/object.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift/object.rs @@ -2,25 +2,33 @@ * 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::CodeType; +use crate::{backend::CodeType, interface::ObjectImpl}; #[derive(Debug)] pub struct ObjectCodeType { - id: String, + name: String, + imp: ObjectImpl, } impl ObjectCodeType { - pub fn new(id: String) -> Self { - Self { id } + pub fn new(name: String, imp: ObjectImpl) -> Self { + Self { name, imp } } } impl CodeType for ObjectCodeType { fn type_label(&self) -> String { - super::SwiftCodeOracle.class_name(&self.id) + super::SwiftCodeOracle.class_name(&self.name) } fn canonical_name(&self) -> String { - format!("Type{}", self.id) + format!("Type{}", self.name) + } + + fn initialization_fn(&self) -> Option { + match &self.imp { + ObjectImpl::Struct => None, + ObjectImpl::Trait => Some(format!("uniffiCallbackInit{}", self.name)), + } } } diff --git a/uniffi_bindgen/src/bindings/swift/templates/Async.swift b/uniffi_bindgen/src/bindings/swift/templates/Async.swift new file mode 100644 index 0000000000..fee7ca6ea6 --- /dev/null +++ b/uniffi_bindgen/src/bindings/swift/templates/Async.swift @@ -0,0 +1,62 @@ +private let UNIFFI_RUST_FUTURE_POLL_READY: Int8 = 0 +private let UNIFFI_RUST_FUTURE_POLL_MAYBE_READY: Int8 = 1 + +internal func uniffiRustCallAsync( + rustFutureFunc: () -> UnsafeMutableRawPointer, + pollFunc: (UnsafeMutableRawPointer, UnsafeMutableRawPointer) -> (), + completeFunc: (UnsafeMutableRawPointer, UnsafeMutablePointer) -> F, + freeFunc: (UnsafeMutableRawPointer) -> (), + liftFunc: (F) throws -> T, + errorHandler: ((RustBuffer) throws -> Error)? +) async throws -> T { + // Make sure to call uniffiEnsureInitialized() since future creation doesn't have a + // RustCallStatus param, so doesn't use makeRustCall() + uniffiEnsureInitialized() + let rustFuture = rustFutureFunc() + defer { + freeFunc(rustFuture) + } + var pollResult: Int8; + repeat { + pollResult = await withUnsafeContinuation { + pollFunc(rustFuture, ContinuationHolder($0).toOpaque()) + } + } while pollResult != UNIFFI_RUST_FUTURE_POLL_READY + + return try liftFunc(makeRustCall( + { completeFunc(rustFuture, $0) }, + errorHandler: errorHandler + )) +} + +// Callback handlers for an async calls. These are invoked by Rust when the future is ready. They +// lift the return value or error and resume the suspended function. +fileprivate func uniffiFutureContinuationCallback(ptr: UnsafeMutableRawPointer, pollResult: Int8) { + ContinuationHolder.fromOpaque(ptr).resume(pollResult) +} + +// Wraps UnsafeContinuation in a class so that we can use reference counting when passing it across +// the FFI +class ContinuationHolder { + let continuation: UnsafeContinuation + + init(_ continuation: UnsafeContinuation) { + self.continuation = continuation + } + + func resume(_ pollResult: Int8) { + self.continuation.resume(returning: pollResult) + } + + func toOpaque() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + static func fromOpaque(_ ptr: UnsafeRawPointer) -> ContinuationHolder { + return Unmanaged.fromOpaque(ptr).takeRetainedValue() + } +} + +fileprivate func uniffiInitContinuationCallback() { + {{ ci.ffi_rust_future_continuation_callback_set().name() }}(uniffiFutureContinuationCallback) +} diff --git a/uniffi_bindgen/src/bindings/swift/templates/AsyncTypes.swift b/uniffi_bindgen/src/bindings/swift/templates/AsyncTypes.swift deleted file mode 100644 index b7dcff516b..0000000000 --- a/uniffi_bindgen/src/bindings/swift/templates/AsyncTypes.swift +++ /dev/null @@ -1,29 +0,0 @@ -// Callbacks for async functions - -// Callback handlers for an async calls. These are invoked by Rust when the future is ready. They -// lift the return value or error and resume the suspended function. -{%- for result_type in ci.iter_async_result_types() %} -fileprivate func {{ result_type|future_callback }}( - rawContinutation: UnsafeRawPointer, - returnValue: {{ result_type.future_callback_param().borrow()|ffi_type_name }}, - callStatus: RustCallStatus) { - - let continuation = rawContinutation.bindMemory( - to: {{ result_type|future_continuation_type }}.self, - capacity: 1 - ) - - do { - try uniffiCheckCallStatus(callStatus: callStatus, errorHandler: {{ result_type|error_handler }}) - {%- match result_type.return_type %} - {%- when Some(return_type) %} - continuation.pointee.resume(returning: try {{ return_type|lift_fn }}(returnValue)) - {%- when None %} - continuation.pointee.resume(returning: ()) - {%- endmatch %} - } catch let error { - continuation.pointee.resume(throwing: error) - } -} - -{%- endfor %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h b/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h index d87977670f..87698e359f 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h +++ b/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h @@ -59,10 +59,8 @@ typedef struct RustCallStatus { // ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️ #endif // def UNIFFI_SHARED_H -// Callbacks for UniFFI Futures -{%- for ffi_type in ci.iter_future_callback_params() %} -typedef void (*UniFfiFutureCallback{{ ffi_type|ffi_canonical_name }})(const void * _Nonnull, {{ ffi_type|header_ffi_type_name }}, RustCallStatus); -{%- endfor %} +// Continuation callback for UniFFI Futures +typedef void (*UniFfiRustFutureContinuation)(void * _Nonnull, int8_t); // Scaffolding functions {%- for func in ci.iter_ffi_function_definitions() %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceImpl.swift b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceImpl.swift new file mode 100644 index 0000000000..157da46128 --- /dev/null +++ b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceImpl.swift @@ -0,0 +1,88 @@ +{%- if self.include_once_check("CallbackInterfaceRuntime.swift") %}{%- include "CallbackInterfaceRuntime.swift" %}{%- endif %} + +// Declaration and FfiConverters for {{ type_name }} Callback Interface + +fileprivate let {{ callback_handler }} : ForeignCallback = + { (handle: UniFFICallbackHandle, method: Int32, argsData: UnsafePointer, argsLen: Int32, out_buf: UnsafeMutablePointer) -> Int32 in + {% for meth in methods.iter() -%} + {%- let method_name = format!("invoke_{}", meth.name())|fn_name %} + + func {{ method_name }}(_ swiftCallbackInterface: {{ type_name }}, _ argsData: UnsafePointer, _ argsLen: Int32, _ out_buf: UnsafeMutablePointer) throws -> Int32 { + {%- if meth.arguments().len() > 0 %} + var reader = createReader(data: Data(bytes: argsData, count: Int(argsLen))) + {%- endif %} + + {%- match meth.return_type() %} + {%- when Some(return_type) %} + func makeCall() throws -> Int32 { + let result = {% 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 -%} + ) + var writer = [UInt8]() + {{ return_type|write_fn }}(result, into: &writer) + out_buf.pointee = RustBuffer(bytes: writer) + return UNIFFI_CALLBACK_SUCCESS + } + {%- when None %} + func makeCall() throws -> Int32 { + {% 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 -%} + ) + return UNIFFI_CALLBACK_SUCCESS + } + {%- endmatch %} + + {%- match meth.throws_type() %} + {%- when None %} + return try makeCall() + {%- when Some(error_type) %} + do { + return try makeCall() + } catch let error as {{ error_type|type_name }} { + out_buf.pointee = {{ error_type|lower_fn }}(error) + return UNIFFI_CALLBACK_ERROR + } + {%- endmatch %} + } + {%- endfor %} + + + switch method { + case IDX_CALLBACK_FREE: + {{ ffi_converter_name }}.handleMap.remove(handle: handle) + // Sucessful return + // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` + return UNIFFI_CALLBACK_SUCCESS + {% for meth in methods.iter() -%} + {% let method_name = format!("invoke_{}", meth.name())|fn_name -%} + case {{ loop.index }}: + guard let cb = {{ ffi_converter_name }}.handleMap.get(handle: handle) else { + out_buf.pointee = {{ Type::String.borrow()|lower_fn }}("No callback in handlemap; this is a Uniffi bug") + return UNIFFI_CALLBACK_UNEXPECTED_ERROR + } + do { + return try {{ method_name }}(cb, argsData, argsLen, out_buf) + } catch let error { + out_buf.pointee = {{ Type::String.borrow()|lower_fn }}(String(describing: error)) + return UNIFFI_CALLBACK_UNEXPECTED_ERROR + } + {% 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_core/src/ffi/foreigncallbacks.rs` + return UNIFFI_CALLBACK_UNEXPECTED_ERROR + } +} + +private func {{ callback_init }}() { + {{ ffi_init_callback.name() }}({{ callback_handler }}) +} diff --git a/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceRuntime.swift b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceRuntime.swift index 9ae62d1667..d03b7ccb3f 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceRuntime.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceRuntime.swift @@ -13,7 +13,7 @@ fileprivate class UniFFICallbackHandleMap { private var rightMap: [ObjectIdentifier: UniFFICallbackHandle] = [:] private let lock = NSLock() - private var currentHandle: UniFFICallbackHandle = 0 + private var currentHandle: UniFFICallbackHandle = 1 private let stride: UniFFICallbackHandle = 1 func insert(obj: T) -> UniFFICallbackHandle { diff --git a/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceTemplate.swift index aec8ded930..f9e75b2a3f 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceTemplate.swift @@ -1,122 +1,16 @@ {%- let cbi = ci|get_callback_interface_definition(name) %} -{%- let foreign_callback = format!("foreignCallback{}", canonical_type_name) %} -{%- if self.include_once_check("CallbackInterfaceRuntime.swift") %}{%- include "CallbackInterfaceRuntime.swift" %}{%- endif %} +{%- let callback_handler = format!("uniffiCallbackHandler{}", name) %} +{%- let callback_init = format!("uniffiCallbackInit{}", name) %} +{%- let methods = cbi.methods() %} +{%- let protocol_name = type_name.clone() %} +{%- let ffi_init_callback = cbi.ffi_init_callback() %} -// 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, argsData: UnsafePointer, argsLen: Int32, out_buf: UnsafeMutablePointer) -> Int32 in - {% for meth in cbi.methods() -%} - {%- let method_name = format!("invoke_{}", meth.name())|fn_name %} - - func {{ method_name }}(_ swiftCallbackInterface: {{ type_name }}, _ argsData: UnsafePointer, _ argsLen: Int32, _ out_buf: UnsafeMutablePointer) throws -> Int32 { - {%- if meth.arguments().len() > 0 %} - var reader = createReader(data: Data(bytes: argsData, count: Int(argsLen))) - {%- endif %} - - {%- match meth.return_type() %} - {%- when Some(return_type) %} - func makeCall() throws -> Int32 { - let result = {% 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 -%} - ) - var writer = [UInt8]() - {{ return_type|write_fn }}(result, into: &writer) - out_buf.pointee = RustBuffer(bytes: writer) - return UNIFFI_CALLBACK_SUCCESS - } - {%- when None %} - func makeCall() throws -> Int32 { - try 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 -%} - ) - return UNIFFI_CALLBACK_SUCCESS - } - {%- endmatch %} - - {%- match meth.throws_type() %} - {%- when None %} - return try makeCall() - {%- when Some(error_type) %} - do { - return try makeCall() - } catch let error as {{ error_type|type_name }} { - out_buf.pointee = {{ error_type|lower_fn }}(error) - return UNIFFI_CALLBACK_ERROR - } - {%- endmatch %} - } - {%- endfor %} - - - switch method { - case IDX_CALLBACK_FREE: - {{ ffi_converter_name }}.drop(handle: handle) - // Sucessful return - // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` - return UNIFFI_CALLBACK_SUCCESS - {% for meth in cbi.methods() -%} - {% let method_name = format!("invoke_{}", meth.name())|fn_name -%} - case {{ loop.index }}: - 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 UNIFFI_CALLBACK_UNEXPECTED_ERROR - } - do { - return try {{ method_name }}(cb, argsData, argsLen, out_buf) - } catch let error { - out_buf.pointee = {{ Type::String.borrow()|lower_fn }}(String(describing: error)) - return UNIFFI_CALLBACK_UNEXPECTED_ERROR - } - {% 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_core/src/ffi/foreigncallbacks.rs` - return UNIFFI_CALLBACK_UNEXPECTED_ERROR - } -} +{% include "Protocol.swift" %} +{% include "CallbackInterfaceImpl.swift" %} // FfiConverter protocol for callback interfaces fileprivate struct {{ ffi_converter_name }} { - private static let initCallbackOnce: () = { - // Swift ensures this initializer code will once run once, even when accessed by multiple threads. - try! rustCall { (err: UnsafeMutablePointer) in - {{ cbi.ffi_init_callback().name() }}({{ foreign_callback }}, err) - } - }() - - private static func ensureCallbackinitialized() { - _ = initCallbackOnce - } - - static func drop(handle: UniFFICallbackHandle) { - handleMap.remove(handle: handle) - } - - private static var handleMap = UniFFICallbackHandleMap<{{ type_name }}>() + fileprivate static var handleMap = UniFFICallbackHandleMap<{{ type_name }}>() } extension {{ ffi_converter_name }} : FfiConverter { @@ -125,7 +19,6 @@ extension {{ ffi_converter_name }} : FfiConverter { typealias FfiType = UniFFICallbackHandle public static func lift(_ handle: UniFFICallbackHandle) throws -> SwiftType { - ensureCallbackinitialized(); guard let callback = handleMap.get(handle: handle) else { throw UniffiInternalError.unexpectedStaleHandle } @@ -133,18 +26,15 @@ extension {{ ffi_converter_name }} : FfiConverter { } 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/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift b/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift index feccd6551e..a34b128e23 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift @@ -29,6 +29,7 @@ fileprivate enum UniffiInternalError: LocalizedError { fileprivate let CALL_SUCCESS: Int8 = 0 fileprivate let CALL_ERROR: Int8 = 1 fileprivate let CALL_PANIC: Int8 = 2 +fileprivate let CALL_CANCELLED: Int8 = 3 fileprivate extension RustCallStatus { init() { @@ -91,6 +92,9 @@ private func uniffiCheckCallStatus( throw UniffiInternalError.rustPanic("Rust panic") } + case CALL_CANCELLED: + throw CancellationError() + default: throw UniffiInternalError.unexpectedRustCallStatusCode } diff --git a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift index 38df547763..af28d6f142 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift @@ -1,18 +1,10 @@ {%- let obj = ci|get_object_definition(name) %} -public protocol {{ obj.name() }}Protocol { - {% for meth in obj.methods() -%} - {%- let func = meth -%} - {%- include "FunctionDocsTemplate.swift" %} - func {{ meth.name()|fn_name }}({% call swift::arg_list_protocol(meth) %}) {% call swift::async(meth) %} {% call swift::throws(meth) -%} - {%- match meth.return_type() -%} - {%- when Some with (return_type) %} -> {{ return_type|type_name -}} - {%- else -%} - {%- endmatch %} - {% endfor %} -} +{%- let (protocol_name, impl_class_name) = obj|object_names %} +{%- let methods = obj.methods() %} + +{% include "Protocol.swift" %} -{% let struct = obj %}{% include "StructureDocsTemplate.swift" %} -public class {{ type_name }}: {{ obj.name() }}Protocol { +public class {{ impl_class_name }}: {{ protocol_name }} { fileprivate let pointer: UnsafeMutableRawPointer // TODO: We'd like this to be `private` but for Swifty reasons, @@ -38,10 +30,9 @@ public class {{ type_name }}: {{ obj.name() }}Protocol { {% for cons in obj.alternate_constructors() %} - {%- let func = cons -%} {%- include "FunctionDocsTemplate.swift" %} - 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) %}) + public static func {{ cons.name()|fn_name }}({% call swift::arg_list_decl(cons) %}) {% call swift::throws(cons) %} -> {{ impl_class_name }} { + return {{ impl_class_name }}(unsafeFromRawPointer: {% call swift::to_ffi_call(cons) %}) } {% endfor %} @@ -51,25 +42,32 @@ public class {{ type_name }}: {{ obj.name() }}Protocol { {%- if meth.is_async() %} public func {{ meth.name()|fn_name }}({%- call swift::arg_list_decl(meth) -%}) async {% call swift::throws(meth) %}{% match meth.return_type() %}{% when Some with (return_type) %} -> {{ return_type|type_name }}{% when None %}{% endmatch %} { - // Suspend the function and call the scaffolding function, passing it a callback handler from - // `AsyncTypes.swift` - // - // Make sure to hold on to a reference to the continuation in the top-level scope so that - // it's not freed before the callback is invoked. - var continuation: {{ meth.result_type().borrow()|future_continuation_type }}? = nil - return {% call swift::try(meth) %} await withCheckedThrowingContinuation { - continuation = $0 - try! rustCall() { + return {% call swift::try(meth) %} await uniffiRustCallAsync( + rustFutureFunc: { {{ meth.ffi_func().name() }}( - self.pointer, - {% call swift::arg_list_lowered(meth) %} - FfiConverterForeignExecutor.lower(UniFfiForeignExecutor()), - {{ meth.result_type().borrow()|future_callback }}, - &continuation, - $0 + self.pointer + {%- for arg in meth.arguments() -%} + , + {{ arg|lower_fn }}({{ arg.name()|var_name }}) + {%- endfor %} ) - } - } + }, + pollFunc: {{ meth.ffi_rust_future_poll(ci) }}, + completeFunc: {{ meth.ffi_rust_future_complete(ci) }}, + freeFunc: {{ meth.ffi_rust_future_free(ci) }}, + {%- match meth.return_type() %} + {%- when Some(return_type) %} + liftFunc: {{ return_type|lift_fn }}, + {%- when None %} + liftFunc: { $0 }, + {%- endmatch %} + {%- match meth.throws_type() %} + {%- when Some with (e) %} + errorHandler: {{ e|ffi_converter_name }}.lift + {%- else %} + errorHandler: nil + {% endmatch %} + ) } {% else -%} @@ -95,10 +93,37 @@ public class {{ type_name }}: {{ obj.name() }}Protocol { {% endfor %} } +{%- if obj.is_trait_interface() %} +{%- let callback_handler = format!("uniffiCallbackInterface{}", name) %} +{%- let callback_init = format!("uniffiCallbackInit{}", name) %} +{%- let ffi_init_callback = obj.ffi_init_callback() %} +{% include "CallbackInterfaceImpl.swift" %} +{%- endif %} + public struct {{ ffi_converter_name }}: FfiConverter { + {%- if obj.is_trait_interface() %} + fileprivate static var handleMap = UniFFICallbackHandleMap<{{ type_name }}>() + {%- endif %} + typealias FfiType = UnsafeMutableRawPointer typealias SwiftType = {{ type_name }} + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> {{ type_name }} { + return {{ impl_class_name }}(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: {{ type_name }}) -> UnsafeMutableRawPointer { + {%- match obj.imp() %} + {%- when ObjectImpl::Struct %} + return value.pointer + {%- when ObjectImpl::Trait %} + guard let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: handleMap.insert(obj: value))) else { + fatalError("Cast to UnsafeMutableRawPointer failed") + } + return ptr + {%- endmatch %} + } + 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. @@ -115,14 +140,6 @@ public struct {{ ffi_converter_name }}: FfiConverter { // 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/uniffi_bindgen/src/bindings/swift/templates/Protocol.swift b/uniffi_bindgen/src/bindings/swift/templates/Protocol.swift new file mode 100644 index 0000000000..d2541c8674 --- /dev/null +++ b/uniffi_bindgen/src/bindings/swift/templates/Protocol.swift @@ -0,0 +1,10 @@ +public protocol {{ protocol_name }}: AnyObject { + { % for meth in methods.iter() -% } + { %- include "FunctionDocsTemplate.swift" % } + func {{ meth.name() | fn_name }}({ % call swift:: arg_list_protocol(meth) % }) { % call swift:: async(meth) -% } { % call swift:: throws (meth) -% } + { %- match meth.return_type() -% } + { %- when Some with(return_type) % } -> {{ return_type | type_name - }} + { %- else -% } + { %- endmatch % } + { % endfor % } +} diff --git a/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift index 7eb0b9240f..11b4c3e5d7 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift @@ -1,24 +1,30 @@ {%- if func.is_async() %} public func {{ func.name()|fn_name }}({%- call swift::arg_list_decl(func) -%}) async {% call swift::throws(func) %}{% match func.return_type() %}{% when Some with (return_type) %} -> {{ return_type|type_name }}{% when None %}{% endmatch %} { - var continuation: {{ func.result_type().borrow()|future_continuation_type }}? = nil - // Suspend the function and call the scaffolding function, passing it a callback handler from - // `AsyncTypes.swift` - // - // Make sure to hold on to a reference to the continuation in the top-level scope so that - // it's not freed before the callback is invoked. - return {% call swift::try(func) %} await withCheckedThrowingContinuation { - continuation = $0 - try! rustCall() { + return {% call swift::try(func) %} await uniffiRustCallAsync( + rustFutureFunc: { {{ func.ffi_func().name() }}( - {% call swift::arg_list_lowered(func) %} - FfiConverterForeignExecutor.lower(UniFfiForeignExecutor()), - {{ func.result_type().borrow()|future_callback }}, - &continuation, - $0 + {%- for arg in func.arguments() %} + {{ arg|lower_fn }}({{ arg.name()|var_name }}){% if !loop.last %},{% endif %} + {%- endfor %} ) - } - } + }, + pollFunc: {{ func.ffi_rust_future_poll(ci) }}, + completeFunc: {{ func.ffi_rust_future_complete(ci) }}, + freeFunc: {{ func.ffi_rust_future_free(ci) }}, + {%- match func.return_type() %} + {%- when Some(return_type) %} + liftFunc: {{ return_type|lift_fn }}, + {%- when None %} + liftFunc: { $0 }, + {%- endmatch %} + {%- match func.throws_type() %} + {%- when Some with (e) %} + errorHandler: {{ e|ffi_converter_name }}.lift + {%- else %} + errorHandler: nil + {% endmatch %} + ) } {% else %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/Types.swift b/uniffi_bindgen/src/bindings/swift/templates/Types.swift index dbde9c0d4c..aba34f4b0b 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/Types.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/Types.swift @@ -96,7 +96,3 @@ {%- else %} {%- endmatch %} {%- endfor %} - -{%- if ci.has_async_fns() %} -{%- include "AsyncTypes.swift" %} -{%- endif %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/macros.swift b/uniffi_bindgen/src/bindings/swift/templates/macros.swift index 0a125e6f61..bcf6938639 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/macros.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/macros.swift @@ -77,11 +77,11 @@ {%- macro async(func) %} -{%- if func.is_async() %}async{% endif %} +{%- if func.is_async() %}async {% endif %} {%- endmacro -%} {%- macro throws(func) %} -{%- if func.throws() %}throws{% endif %} +{%- if func.throws() %}throws {% endif %} {%- endmacro -%} {%- macro try(func) %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift b/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift index 5db82dfe15..ca5842d2b5 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift @@ -19,6 +19,10 @@ import {{ config.ffi_module_name() }} // Public interface members begin here. {{ type_helper_code }} +{%- if ci.has_async_fns() %} +{% include "Async.swift" %} +{%- endif %} + {%- for func in ci.function_definitions() %} {% include "TopLevelFunctionTemplate.swift" %} {%- endfor %} diff --git a/uniffi_bindgen/src/interface/callbacks.rs b/uniffi_bindgen/src/interface/callbacks.rs index e3bca4f966..9bafce25de 100644 --- a/uniffi_bindgen/src/interface/callbacks.rs +++ b/uniffi_bindgen/src/interface/callbacks.rs @@ -35,7 +35,7 @@ use uniffi_meta::Checksum; -use super::ffi::{FfiArgument, FfiFunction, FfiType}; +use super::ffi::FfiFunction; use super::object::Method; use super::{AsType, Type, TypeIterator}; @@ -77,13 +77,7 @@ impl CallbackInterface { } pub(super) fn derive_ffi_funcs(&mut self) { - self.ffi_init_callback.name = - uniffi_meta::init_callback_fn_symbol_name(&self.module_path, &self.name); - self.ffi_init_callback.arguments = vec![FfiArgument { - name: "callback_stub".to_string(), - type_: FfiType::ForeignCallback, - }]; - self.ffi_init_callback.return_type = None; + self.ffi_init_callback = FfiFunction::callback_init(&self.module_path, &self.name); } pub fn iter_types(&self) -> TypeIterator<'_> { diff --git a/uniffi_bindgen/src/interface/enum_.rs b/uniffi_bindgen/src/interface/enum_.rs index 15e20e2602..68fdee9e83 100644 --- a/uniffi_bindgen/src/interface/enum_.rs +++ b/uniffi_bindgen/src/interface/enum_.rs @@ -316,7 +316,7 @@ mod test { #[test] fn test_associated_data() { - const UDL: &str = r##" + const UDL: &str = r#" namespace test { void takes_an_enum(TestEnum e); void takes_an_enum_with_data(TestEnumWithData ed); @@ -338,7 +338,7 @@ mod test { One(); Two(); }; - "##; + "#; let ci = ComponentInterface::from_webidl(UDL, "crate_name").unwrap(); assert_eq!(ci.enum_definitions().count(), 3); assert_eq!(ci.function_definitions().len(), 4); diff --git a/uniffi_bindgen/src/interface/ffi.rs b/uniffi_bindgen/src/interface/ffi.rs index 81ba0674ad..8f23ccbb90 100644 --- a/uniffi_bindgen/src/interface/ffi.rs +++ b/uniffi_bindgen/src/interface/ffi.rs @@ -54,14 +54,11 @@ pub enum FfiType { ForeignExecutorHandle, /// Pointer to the callback function that's invoked to schedule calls with a ForeignExecutor ForeignExecutorCallback, - /// Pointer to a callback function to complete an async Rust function - FutureCallback { - /// Note: `return_type` is not optional because we have a void callback parameter like we - /// can have a void return. Instead, we use `UInt8` as a placeholder value. - return_type: Box, - }, - /// Opaque pointer passed to the FutureCallback - FutureCallbackData, + /// Pointer to a Rust future + RustFutureHandle, + /// Continuation function for a Rust future + RustFutureContinuationCallback, + RustFutureContinuationData, // TODO: you can imagine a richer structural typesystem here, e.g. `Ref` or something. // We don't need that yet and it's possible we never will, so it isn't here for now. } @@ -153,6 +150,19 @@ pub struct FfiFunction { } impl FfiFunction { + pub fn callback_init(module_path: &str, trait_name: &str) -> Self { + Self { + name: uniffi_meta::init_callback_fn_symbol_name(module_path, trait_name), + arguments: vec![FfiArgument { + name: "handle".to_string(), + type_: FfiType::ForeignCallback, + }], + return_type: None, + has_rust_call_status_arg: false, + ..Self::default() + } + } + pub fn name(&self) -> &str { &self.name } @@ -184,28 +194,8 @@ impl FfiFunction { ) { self.arguments = args.into_iter().collect(); if self.is_async() { - self.arguments.extend([ - // Used to schedule polls - FfiArgument { - name: "uniffi_executor".into(), - type_: FfiType::ForeignExecutorHandle, - }, - // Invoked when the future is ready - FfiArgument { - name: "uniffi_callback".into(), - type_: FfiType::FutureCallback { - return_type: Box::new(return_type.unwrap_or(FfiType::UInt8)), - }, - }, - // Data pointer passed to the callback - FfiArgument { - name: "uniffi_callback_data".into(), - type_: FfiType::FutureCallbackData, - }, - ]); - // Async scaffolding functions never return values. Instead, the callback is invoked - // when the Future is ready. - self.return_type = None; + self.return_type = Some(FfiType::RustFutureHandle); + self.has_rust_call_status_arg = false; } else { self.return_type = return_type; } diff --git a/uniffi_bindgen/src/interface/function.rs b/uniffi_bindgen/src/interface/function.rs index 9ce544a167..c83a4e44c0 100644 --- a/uniffi_bindgen/src/interface/function.rs +++ b/uniffi_bindgen/src/interface/function.rs @@ -35,8 +35,7 @@ use anyhow::Result; use super::ffi::{FfiArgument, FfiFunction, FfiType}; -use super::Literal; -use super::{AsType, ObjectImpl, Type, TypeIterator}; +use super::{AsType, ComponentInterface, Literal, ObjectImpl, Type, TypeIterator}; use uniffi_meta::Checksum; /// Represents a standalone function. @@ -253,12 +252,39 @@ pub trait Callable { fn arguments(&self) -> Vec<&Argument>; fn return_type(&self) -> Option; fn throws_type(&self) -> Option; + fn is_async(&self) -> bool; fn result_type(&self) -> ResultType { ResultType { return_type: self.return_type(), throws_type: self.throws_type(), } } + + // Quick way to get the rust future scaffolding function that corresponds to our return type. + + fn ffi_rust_future_poll(&self, ci: &ComponentInterface) -> String { + ci.ffi_rust_future_poll(self.return_type().map(Into::into)) + .name() + .to_owned() + } + + fn ffi_rust_future_cancel(&self, ci: &ComponentInterface) -> String { + ci.ffi_rust_future_cancel(self.return_type().map(Into::into)) + .name() + .to_owned() + } + + fn ffi_rust_future_complete(&self, ci: &ComponentInterface) -> String { + ci.ffi_rust_future_complete(self.return_type().map(Into::into)) + .name() + .to_owned() + } + + fn ffi_rust_future_free(&self, ci: &ComponentInterface) -> String { + ci.ffi_rust_future_free(self.return_type().map(Into::into)) + .name() + .to_owned() + } } impl Callable for Function { @@ -273,6 +299,10 @@ impl Callable for Function { fn throws_type(&self) -> Option { self.throws_type().cloned() } + + fn is_async(&self) -> bool { + self.is_async + } } // Needed because Askama likes to add extra refs to variables @@ -288,6 +318,10 @@ impl Callable for &T { fn throws_type(&self) -> Option { (*self).throws_type() } + + fn is_async(&self) -> bool { + (*self).is_async() + } } #[cfg(test)] @@ -298,7 +332,7 @@ mod test { #[test] fn test_minimal_and_rich_function() -> Result<()> { let ci = ComponentInterface::from_webidl( - r##" + r#" namespace test { void minimal(); [Throws=TestError] @@ -309,7 +343,7 @@ mod test { dictionary TestDict { u32 field; }; - "##, + "#, "crate_name", )?; diff --git a/uniffi_bindgen/src/interface/mod.rs b/uniffi_bindgen/src/interface/mod.rs index a567e4adb6..7be1dbbd2e 100644 --- a/uniffi_bindgen/src/interface/mod.rs +++ b/uniffi_bindgen/src/interface/mod.rs @@ -71,7 +71,7 @@ pub use ffi::{FfiArgument, FfiFunction, FfiType}; pub use uniffi_meta::Radix; use uniffi_meta::{ ConstructorMetadata, LiteralMetadata, NamespaceMetadata, ObjectMetadata, TraitMethodMetadata, - UNIFFI_CONTRACT_VERSION, + UniffiTraitMetadata, UNIFFI_CONTRACT_VERSION, }; pub type Literal = LiteralMetadata; @@ -241,27 +241,38 @@ impl ComponentInterface { let fielded = !e.is_flat(); // For flat errors, we should only generate read() methods if we need them to support // callback interface errors - let used_in_callback_interface = self + let used_in_foreign_interface = self .callback_interface_definitions() .iter() .flat_map(|cb| cb.methods()) + .chain( + self.object_definitions() + .iter() + .filter(|o| o.is_trait_interface()) + .flat_map(|o| o.methods()), + ) .any(|m| m.throws_type() == Some(&e.as_type())); - self.is_name_used_as_error(&e.name) && (fielded || used_in_callback_interface) + self.is_name_used_as_error(&e.name) && (fielded || used_in_foreign_interface) } /// Get details about all `Type::External` types. /// Returns an iterator of (name, crate_name, kind) - pub fn iter_external_types(&self) -> impl Iterator { + pub fn iter_external_types( + &self, + ) -> impl Iterator { self.types.iter_known_types().filter_map(|t| match t { Type::External { name, module_path, kind, + tagged, + .. } => Some(( name, module_path.split("::").next().unwrap().to_string(), *kind, + *tagged, )), _ => None, }) @@ -285,10 +296,6 @@ impl ComponentInterface { self.types.get_type_definition(name) } - pub fn is_callback_interface_throws_type(&self, type_: Type) -> bool { - self.callback_interface_throws_types.contains(&type_) - } - /// Iterate over all types contained in the given item. /// /// This method uses `iter_types` to iterate over the types contained within the given type, @@ -428,6 +435,115 @@ impl ComponentInterface { } } + /// Builtin FFI function to set the Rust Future continuation callback + pub fn ffi_rust_future_continuation_callback_set(&self) -> FfiFunction { + FfiFunction { + name: format!( + "ffi_{}_rust_future_continuation_callback_set", + self.ffi_namespace() + ), + arguments: vec![FfiArgument { + name: "callback".to_owned(), + type_: FfiType::RustFutureContinuationCallback, + }], + return_type: None, + is_async: false, + has_rust_call_status_arg: false, + is_object_free_function: false, + } + } + + /// Builtin FFI function to poll a Rust future. + pub fn ffi_rust_future_poll(&self, return_ffi_type: Option) -> FfiFunction { + FfiFunction { + name: self.rust_future_ffi_fn_name("rust_future_poll", return_ffi_type), + is_async: false, + arguments: vec![ + FfiArgument { + name: "handle".to_owned(), + type_: FfiType::RustFutureHandle, + }, + // Data to pass to the continuation + FfiArgument { + name: "uniffi_callback".to_owned(), + type_: FfiType::RustFutureContinuationData, + }, + ], + return_type: None, + has_rust_call_status_arg: false, + is_object_free_function: false, + } + } + + /// Builtin FFI function to complete a Rust future and get it's result. + /// + /// We generate one of these for each FFI return type. + pub fn ffi_rust_future_complete(&self, return_ffi_type: Option) -> FfiFunction { + FfiFunction { + name: self.rust_future_ffi_fn_name("rust_future_complete", return_ffi_type.clone()), + is_async: false, + arguments: vec![FfiArgument { + name: "handle".to_owned(), + type_: FfiType::RustFutureHandle, + }], + return_type: return_ffi_type, + has_rust_call_status_arg: true, + is_object_free_function: false, + } + } + + /// Builtin FFI function for cancelling a Rust Future + pub fn ffi_rust_future_cancel(&self, return_ffi_type: Option) -> FfiFunction { + FfiFunction { + name: self.rust_future_ffi_fn_name("rust_future_cancel", return_ffi_type), + is_async: false, + arguments: vec![FfiArgument { + name: "handle".to_owned(), + type_: FfiType::RustFutureHandle, + }], + return_type: None, + has_rust_call_status_arg: false, + is_object_free_function: false, + } + } + + /// Builtin FFI function for freeing a Rust Future + pub fn ffi_rust_future_free(&self, return_ffi_type: Option) -> FfiFunction { + FfiFunction { + name: self.rust_future_ffi_fn_name("rust_future_free", return_ffi_type), + is_async: false, + arguments: vec![FfiArgument { + name: "handle".to_owned(), + type_: FfiType::RustFutureHandle, + }], + return_type: None, + has_rust_call_status_arg: false, + is_object_free_function: false, + } + } + + fn rust_future_ffi_fn_name(&self, base_name: &str, return_ffi_type: Option) -> String { + let namespace = self.ffi_namespace(); + match return_ffi_type { + Some(t) => match t { + FfiType::UInt8 => format!("ffi_{namespace}_{base_name}_u8"), + FfiType::Int8 => format!("ffi_{namespace}_{base_name}_i8"), + FfiType::UInt16 => format!("ffi_{namespace}_{base_name}_u16"), + FfiType::Int16 => format!("ffi_{namespace}_{base_name}_i16"), + FfiType::UInt32 => format!("ffi_{namespace}_{base_name}_u32"), + FfiType::Int32 => format!("ffi_{namespace}_{base_name}_i32"), + FfiType::UInt64 => format!("ffi_{namespace}_{base_name}_u64"), + FfiType::Int64 => format!("ffi_{namespace}_{base_name}_i64"), + FfiType::Float32 => format!("ffi_{namespace}_{base_name}_f32"), + FfiType::Float64 => format!("ffi_{namespace}_{base_name}_f64"), + FfiType::RustArcPtr(_) => format!("ffi_{namespace}_{base_name}_pointer"), + FfiType::RustBuffer(_) => format!("ffi_{namespace}_{base_name}_rust_buffer"), + _ => unimplemented!("FFI return type: {t:?}"), + }, + None => format!("ffi_{namespace}_{base_name}_void"), + } + } + /// Does this interface contain async functions? pub fn has_async_fns(&self) -> bool { self.iter_ffi_function_definitions().any(|f| f.is_async()) @@ -459,11 +575,23 @@ impl ComponentInterface { self.iter_user_ffi_function_definitions() .cloned() .chain(self.iter_rust_buffer_ffi_function_definitions()) + .chain(self.iter_futures_ffi_function_definitons()) .chain(self.iter_checksum_ffi_functions()) .chain(self.ffi_foreign_executor_callback_set()) .chain([self.ffi_uniffi_contract_version()]) } + /// Alternate version of iter_ffi_function_definitions for languages that don't support async + pub fn iter_ffi_function_definitions_non_async( + &self, + ) -> impl Iterator + '_ { + self.iter_user_ffi_function_definitions() + .cloned() + .chain(self.iter_rust_buffer_ffi_function_definitions()) + .chain(self.iter_checksum_ffi_functions()) + .chain([self.ffi_uniffi_contract_version()]) + } + /// List all FFI functions definitions for user-defined interfaces /// /// This includes FFI functions for: @@ -496,6 +624,40 @@ impl ComponentInterface { .into_iter() } + /// List all FFI functions definitions for async functionality. + pub fn iter_futures_ffi_function_definitons(&self) -> impl Iterator + '_ { + let all_possible_return_ffi_types = [ + Some(FfiType::UInt8), + Some(FfiType::Int8), + Some(FfiType::UInt16), + Some(FfiType::Int16), + Some(FfiType::UInt32), + Some(FfiType::Int32), + Some(FfiType::UInt64), + Some(FfiType::Int64), + Some(FfiType::Float32), + Some(FfiType::Float64), + // RustBuffer and RustArcPtr have an inner field which doesn't affect the rust future + // complete scaffolding function, so we just use a placeholder value here. + Some(FfiType::RustArcPtr("".to_owned())), + Some(FfiType::RustBuffer(None)), + None, + ]; + + iter::once(self.ffi_rust_future_continuation_callback_set()).chain( + all_possible_return_ffi_types + .into_iter() + .flat_map(|return_type| { + [ + self.ffi_rust_future_poll(return_type.clone()), + self.ffi_rust_future_cancel(return_type.clone()), + self.ffi_rust_future_free(return_type.clone()), + self.ffi_rust_future_complete(return_type), + ] + }), + ) + } + /// The ffi_foreign_executor_callback_set FFI function /// /// We only include this in the FFI if the `ForeignExecutor` type is actually used @@ -618,14 +780,10 @@ impl ComponentInterface { if self.functions.iter().any(|f| f.name == defn.name) { bail!("duplicate function definition: \"{}\"", defn.name); } - if !matches!(self.types.get_type_definition(defn.name()), None) { + if self.types.get_type_definition(defn.name()).is_some() { bail!("Conflicting type definition for \"{}\"", defn.name()); } self.types.add_known_types(defn.iter_types())?; - if defn.is_async() { - // Async functions depend on the foreign executor - self.types.add_known_type(&Type::ForeignExecutor)?; - } self.functions.push(defn); Ok(()) @@ -648,13 +806,17 @@ impl ComponentInterface { .ok_or_else(|| anyhow!("add_method_meta: object {} not found", &method.object_name))?; self.types.add_known_types(method.iter_types())?; - if method.is_async() { - // Async functions depend on the foreign executor - self.types.add_known_type(&Type::ForeignExecutor)?; - } method.object_impl = object.imp; object.methods.push(method); + Ok(()) + } + pub(super) fn add_uniffitrait_meta(&mut self, meta: UniffiTraitMetadata) -> Result<()> { + let object = get_object(&mut self.objects, meta.self_name()) + .ok_or_else(|| anyhow!("add_uniffitrait_meta: object not found"))?; + let ut: UniffiTrait = meta.into(); + self.types.add_known_types(ut.iter_types())?; + object.uniffi_traits.push(ut); Ok(()) } @@ -720,7 +882,7 @@ impl ComponentInterface { // Because functions aren't first class types, we need to check here that // a function name hasn't already been used as a type name. for f in self.functions.iter() { - if !matches!(self.types.get_type_definition(f.name()), None) { + if self.types.get_type_definition(f.name()).is_some() { bail!("Conflicting type definition for \"{}\"", f.name()); } } diff --git a/uniffi_bindgen/src/interface/object.rs b/uniffi_bindgen/src/interface/object.rs index 55979acd75..2b4ca58ebd 100644 --- a/uniffi_bindgen/src/interface/object.rs +++ b/uniffi_bindgen/src/interface/object.rs @@ -94,7 +94,7 @@ pub struct Object { // a regular method (albeit with a generated name) // XXX - this should really be a HashSet, but not enough transient types support hash to make it worthwhile now. pub(super) uniffi_traits: Vec, - // We don't include the FfiFunc in the hash calculation, because: + // We don't include the FfiFuncs in the hash calculation, because: // - it is entirely determined by the other fields, // so excluding it is safe. // - its `name` property includes a checksum derived from the very @@ -102,6 +102,9 @@ pub struct Object { // avoids a weird circular dependency in the calculation. #[checksum_ignore] pub(super) ffi_func_free: FfiFunction, + // Ffi function to initialize the foreign callback for trait interfaces + #[checksum_ignore] + pub(super) ffi_init_callback: Option, } impl Object { @@ -124,6 +127,10 @@ impl Object { self.documentation.as_ref() } + pub fn is_trait_interface(&self) -> bool { + matches!(self.imp, ObjectImpl::Trait) + } + pub fn constructors(&self) -> Vec<&Constructor> { self.constructors.iter().collect() } @@ -161,8 +168,15 @@ impl Object { &self.ffi_func_free } + pub fn ffi_init_callback(&self) -> &FfiFunction { + self.ffi_init_callback + .as_ref() + .unwrap_or_else(|| panic!("No ffi_init_callback set for {}", &self.name)) + } + pub fn iter_ffi_function_definitions(&self) -> impl Iterator { iter::once(&self.ffi_func_free) + .chain(&self.ffi_init_callback) .chain(self.constructors.iter().map(|f| &f.ffi_func)) .chain(self.methods.iter().map(|f| &f.ffi_func)) .chain( @@ -186,6 +200,10 @@ impl Object { }]; self.ffi_func_free.return_type = None; self.ffi_func_free.is_object_free_function = true; + if self.is_trait_interface() { + self.ffi_init_callback = + Some(FfiFunction::callback_init(&self.module_path, &self.name)); + } for cons in self.constructors.iter_mut() { cons.derive_ffi_func(); @@ -232,11 +250,12 @@ impl From for Object { documentation: None, constructors: Default::default(), methods: Default::default(), - uniffi_traits: meta.uniffi_traits.into_iter().map(Into::into).collect(), + uniffi_traits: Default::default(), ffi_func_free: FfiFunction { name: ffi_free_name, ..Default::default() }, + ffi_init_callback: None, } } } @@ -602,6 +621,10 @@ impl Callable for Constructor { fn throws_type(&self) -> Option { self.throws_type().cloned() } + + fn is_async(&self) -> bool { + false + } } impl Callable for Method { @@ -616,6 +639,10 @@ impl Callable for Method { fn throws_type(&self) -> Option { self.throws_type().cloned() } + + fn is_async(&self) -> bool { + self.is_async + } } #[cfg(test)] diff --git a/uniffi_bindgen/src/lib.rs b/uniffi_bindgen/src/lib.rs index 170abc52ea..a05d82e580 100644 --- a/uniffi_bindgen/src/lib.rs +++ b/uniffi_bindgen/src/lib.rs @@ -98,7 +98,7 @@ use fs_err::{self as fs, File}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::io::prelude::*; use std::io::ErrorKind; -use std::{collections::HashMap, process::Command, str::FromStr}; +use std::{collections::HashMap, process::Command}; pub mod backend; pub mod bindings; @@ -116,9 +116,6 @@ use scaffolding::RustScaffolding; /// BindingsConfigs are initially loaded from `uniffi.toml` file. Then the trait methods are used /// to fill in missing values. pub trait BindingsConfig: DeserializeOwned { - /// key in the `bindings` table from `uniffi.toml` for this configuration - const TOML_KEY: &'static str; - /// Update missing values using the `ComponentInterface` fn update_from_ci(&mut self, ci: &ComponentInterface); @@ -134,63 +131,16 @@ pub trait BindingsConfig: DeserializeOwned { fn update_from_dependency_configs(&mut self, config_map: HashMap<&str, &Self>); } -fn load_bindings_config( - ci: &ComponentInterface, - crate_root: &Utf8Path, - config_file_override: Option<&Utf8Path>, -) -> Result { - // Load the config from the TOML value, falling back to an empty map if it doesn't exist - let toml_config = load_bindings_config_toml(crate_root, config_file_override)? - .and_then(|mut v| v.as_table_mut().and_then(|t| t.remove(BC::TOML_KEY))) - .unwrap_or_else(|| toml::Value::from(toml::value::Table::default())); - - let mut config: BC = toml_config.try_into()?; - config.update_from_ci(ci); - Ok(config) -} - /// Binding generator config with no members #[derive(Clone, Debug, Deserialize, Hash, PartialEq, PartialOrd, Ord, Eq)] pub struct EmptyBindingsConfig; impl BindingsConfig for EmptyBindingsConfig { - const TOML_KEY: &'static str = ""; - fn update_from_ci(&mut self, _ci: &ComponentInterface) {} fn update_from_cdylib_name(&mut self, _cdylib_name: &str) {} fn update_from_dependency_configs(&mut self, _config_map: HashMap<&str, &Self>) {} } -// Load the binding-specific config -// -// This function calculates the location of the config TOML file, parses it, and returns the result -// as a toml::Value -// -// If there is an error parsing the file then Err will be returned. If the file is missing or the -// entry for the bindings is missing, then Ok(None) will be returned. -fn load_bindings_config_toml( - crate_root: &Utf8Path, - config_file_override: Option<&Utf8Path>, -) -> Result> { - let config_path = match config_file_override { - Some(cfg) => cfg.to_owned(), - None => crate_root.join("uniffi.toml"), - }; - - if !config_path.exists() { - return Ok(None); - } - - let contents = fs::read_to_string(&config_path) - .with_context(|| format!("Failed to read config file from {config_path}"))?; - let mut full_config = toml::Value::from_str(&contents) - .with_context(|| format!("Failed to parse config file {config_path}"))?; - - Ok(full_config - .as_table_mut() - .and_then(|t| t.remove("bindings"))) -} - /// A trait representing a UniFFI Binding Generator /// /// External crates that implement binding generators, should implement this type @@ -207,10 +157,49 @@ pub trait BindingGenerator: Sized { /// - `out_dir`: The path to where the binding generator should write the output bindings fn write_bindings( &self, - ci: ComponentInterface, - config: Self::Config, + ci: &ComponentInterface, + config: &Self::Config, out_dir: &Utf8Path, ) -> Result<()>; + + /// Check if `library_path` used by library mode is valid for this generator + fn check_library_path(&self, library_path: &Utf8Path, cdylib_name: Option<&str>) -> Result<()>; +} + +struct BindingGeneratorDefault { + target_languages: Vec, + try_format_code: bool, +} + +impl BindingGenerator for BindingGeneratorDefault { + type Config = Config; + + fn write_bindings( + &self, + ci: &ComponentInterface, + config: &Self::Config, + out_dir: &Utf8Path, + ) -> Result<()> { + for &language in &self.target_languages { + bindings::write_bindings( + &config.bindings, + ci, + out_dir, + language, + self.try_format_code, + )?; + } + Ok(()) + } + + fn check_library_path(&self, library_path: &Utf8Path, cdylib_name: Option<&str>) -> Result<()> { + for &language in &self.target_languages { + if cdylib_name.is_none() && language != TargetLanguage::Swift { + bail!("Generate bindings for {language} requires a cdylib, but {library_path} was given"); + } + } + Ok(()) + } } /// Generate bindings for an external binding generator @@ -228,14 +217,18 @@ pub trait BindingGenerator: Sized { /// - `config_file_override`: The path to the configuration toml file, most likely called `uniffi.toml`. If [`None`], the function will try to guess based on the crate's root. /// - `out_dir_override`: The path to write the bindings to. If [`None`], it will be the path to the parent directory of the `udl_file` /// - `library_file`: The path to a dynamic library to attempt to extract the definitions from and extend the component interface with. No extensions to component interface occur if it's [`None`] +/// - `crate_name`: Override the default crate name that is guessed from UDL file path. pub fn generate_external_bindings( binding_generator: T, udl_file: impl AsRef, config_file_override: Option>, out_dir_override: Option>, library_file: Option>, + crate_name: Option<&str>, ) -> Result<()> { - let crate_name = crate_name_from_cargo_toml(udl_file.as_ref())?; + let crate_name = crate_name + .map(|c| Ok(c.to_string())) + .unwrap_or_else(|| crate_name_from_cargo_toml(udl_file.as_ref()))?; let mut component = parse_udl(udl_file.as_ref(), &crate_name)?; if let Some(ref library_file) = library_file { macro_metadata::add_to_ci_from_library(&mut component, library_file.as_ref())?; @@ -243,14 +236,17 @@ pub fn generate_external_bindings( let crate_root = &guess_crate_root(udl_file.as_ref()).context("Failed to guess crate root")?; let config_file_override = config_file_override.as_ref().map(|p| p.as_ref()); - let mut config = - load_bindings_config::(&component, crate_root, config_file_override)?; - config.update_from_ci(&component); let config = { - let mut config = - load_bindings_config::(&component, crate_root, config_file_override)?; + let mut config = load_initial_config::(crate_root, config_file_override)?; config.update_from_ci(&component); + + if config.bindings.doc_comments.unwrap_or_default() { + let path = udl_file.with_file_name("lib.rs"); + let documentation = uniffi_docs::extract_documentation_from_path(path)?; + component.attach_documentation(documentation); + }; + if let Some(ref library_file) = library_file { if let Some(cdylib_name) = crate::library_mode::calc_cdylib_name(library_file.as_ref()) { @@ -264,7 +260,7 @@ pub fn generate_external_bindings( udl_file.as_ref(), out_dir_override.as_ref().map(|p| p.as_ref()), )?; - binding_generator.write_bindings(component, config, &out_dir) + binding_generator.write_bindings(&component, &config, &out_dir) } // Generate the infrastructural Rust code for implementing the UDL interface, @@ -321,37 +317,17 @@ pub fn generate_bindings( crate_name: Option<&str>, try_format_code: bool, ) -> Result<()> { - let crate_name = crate_name - .map(|c| Ok(c.to_string())) - .unwrap_or_else(|| crate_name_from_cargo_toml(udl_file))?; - let mut component = parse_udl(udl_file, &crate_name)?; - if let Some(library_file) = library_file { - macro_metadata::add_to_ci_from_library(&mut component, library_file)?; - } - let crate_root = &guess_crate_root(udl_file).context("Failed to guess crate root")?; - - let mut config = Config::load_initial(crate_root, config_file_override)?; - - config.update_from_ci(&component); - - if config.bindings.doc_comments.unwrap_or_default() { - let path = udl_file.with_file_name("lib.rs"); - let documentation = uniffi_docs::extract_documentation_from_path(path)?; - component.attach_documentation(documentation); - } - - let out_dir = get_out_dir(udl_file, out_dir_override)?; - for language in target_languages { - bindings::write_bindings( - &config.bindings, - &component, - &out_dir, - language, + generate_external_bindings( + BindingGeneratorDefault { + target_languages, try_format_code, - )?; - } - - Ok(()) + }, + udl_file, + config_file_override, + out_dir_override, + library_file, + crate_name, + ) } pub fn print_repr(library_path: &Utf8Path) -> Result<()> { @@ -448,31 +424,31 @@ fn format_code_with_rustfmt(path: &Utf8Path) -> Result<()> { Ok(()) } +fn load_initial_config( + crate_root: &Utf8Path, + config_file_override: Option<&Utf8Path>, +) -> Result { + let path = match config_file_override { + Some(cfg) => Some(cfg.to_owned()), + None => crate_root.join("uniffi.toml").canonicalize_utf8().ok(), + }; + let toml_config = match path { + Some(path) => { + let contents = fs::read_to_string(path).context("Failed to read config file")?; + toml::de::from_str(&contents)? + } + None => toml::Value::from(toml::value::Table::default()), + }; + Ok(toml_config.try_into()?) +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Config { #[serde(default)] bindings: bindings::Config, } -impl Config { - fn load_initial( - crate_root: &Utf8Path, - config_file_override: Option<&Utf8Path>, - ) -> Result { - let path = match config_file_override { - Some(cfg) => Some(cfg.to_owned()), - None => crate_root.join("uniffi.toml").canonicalize_utf8().ok(), - }; - let toml_config = match path { - Some(path) => { - let contents = fs::read_to_string(path).context("Failed to read config file")?; - toml::de::from_str(&contents)? - } - None => toml::Value::from(toml::value::Table::default()), - }; - Ok(toml_config.try_into()?) - } - +impl BindingsConfig for Config { fn update_from_ci(&mut self, ci: &ComponentInterface) { self.bindings.kotlin.update_from_ci(ci); self.bindings.swift.update_from_ci(ci); diff --git a/uniffi_bindgen/src/library_mode.rs b/uniffi_bindgen/src/library_mode.rs index a60909c07f..f170ea5e91 100644 --- a/uniffi_bindgen/src/library_mode.rs +++ b/uniffi_bindgen/src/library_mode.rs @@ -16,8 +16,8 @@ /// - UniFFI can figure out the package/module names for each crate, eliminating the external /// package maps. use crate::{ - bindings::{self, TargetLanguage}, - macro_metadata, ComponentInterface, Config, Result, + bindings::TargetLanguage, load_initial_config, macro_metadata, BindingGenerator, + BindingGeneratorDefault, BindingsConfig, ComponentInterface, Result, }; use anyhow::{bail, Context}; use camino::Utf8Path; @@ -26,7 +26,9 @@ use std::{ collections::{HashMap, HashSet}, fs, }; -use uniffi_meta::{group_metadata, MetadataGroup}; +use uniffi_meta::{ + create_metadata_groups, fixup_external_type, group_metadata, Metadata, MetadataGroup, +}; /// Generate foreign bindings /// @@ -37,11 +39,33 @@ pub fn generate_bindings( target_languages: &[TargetLanguage], out_dir: &Utf8Path, try_format_code: bool, -) -> Result> { +) -> Result>> { + generate_external_bindings( + BindingGeneratorDefault { + target_languages: target_languages.into(), + try_format_code, + }, + library_path, + crate_name, + out_dir, + ) +} + +/// Generate foreign bindings +/// +/// Returns the list of sources used to generate the bindings, in no particular order. +pub fn generate_external_bindings( + binding_generator: T, + library_path: &Utf8Path, + crate_name: Option, + out_dir: &Utf8Path, +) -> Result>> { let cargo_metadata = MetadataCommand::new() .exec() .context("error running cargo metadata")?; let cdylib_name = calc_cdylib_name(library_path); + binding_generator.check_library_path(library_path, cdylib_name)?; + let mut sources = find_sources(&cargo_metadata, library_path, cdylib_name)?; for i in 0..sources.len() { // Partition up the sources list because we're eventually going to call @@ -53,7 +77,7 @@ pub fn generate_bindings( // Calculate which configs come from dependent crates let dependencies = HashSet::<&str>::from_iter(source.package.dependencies.iter().map(|d| d.name.as_str())); - let config_map: HashMap<&str, &Config> = other_sources + let config_map: HashMap<&str, &T::Config> = other_sources .filter_map(|s| { dependencies .contains(s.package.name.as_str()) @@ -77,18 +101,7 @@ pub fn generate_bindings( } for source in sources.iter() { - for &language in target_languages { - if cdylib_name.is_none() && language != TargetLanguage::Swift { - bail!("Generate bindings for {language} requires a cdylib, but {library_path} was given"); - } - bindings::write_bindings( - &source.config.bindings, - &source.ci, - out_dir, - language, - try_format_code, - )?; - } + binding_generator.write_bindings(&source.ci, &source.config, out_dir)?; } Ok(sources) @@ -96,7 +109,7 @@ pub fn generate_bindings( // A single source that we generate bindings for #[derive(Debug)] -pub struct Source { +pub struct Source { pub package: Package, pub crate_name: String, pub ci: ComponentInterface, @@ -116,13 +129,43 @@ pub fn calc_cdylib_name(library_path: &Utf8Path) -> Option<&str> { None } -fn find_sources( +fn find_sources( cargo_metadata: &cargo_metadata::Metadata, library_path: &Utf8Path, cdylib_name: Option<&str>, -) -> Result> { - group_metadata(macro_metadata::extract_from_library(library_path)?)? - .into_iter() +) -> Result>> { + let items = macro_metadata::extract_from_library(library_path)?; + let mut metadata_groups = create_metadata_groups(&items); + group_metadata(&mut metadata_groups, items)?; + + // Collect and process all UDL from all groups at the start - the fixups + // of external types makes this tricky to do as we finalize the group. + let mut udl_items: HashMap = HashMap::new(); + + for group in metadata_groups.values() { + let package = find_package_by_crate_name(cargo_metadata, &group.namespace.crate_name)?; + let crate_root = package + .manifest_path + .parent() + .context("manifest path has no parent")?; + let crate_name = group.namespace.crate_name.clone(); + if let Some(mut metadata_group) = load_udl_metadata(group, crate_root, &crate_name)? { + // fixup the items. + metadata_group.items = metadata_group + .items + .into_iter() + .map(|item| fixup_external_type(item, &metadata_groups)) + // some items are both in UDL and library metadata. For many that's fine but + // uniffi-traits aren't trivial to compare meaning we end up with dupes. + // We filter out such problematic items here. + .filter(|item| !matches!(item, Metadata::UniffiTrait { .. })) + .collect(); + udl_items.insert(crate_name, metadata_group); + }; + } + + metadata_groups + .into_values() .map(|group| { let package = find_package_by_crate_name(cargo_metadata, &group.namespace.crate_name)?; let crate_root = package @@ -131,11 +174,11 @@ fn find_sources( .context("manifest path has no parent")?; let crate_name = group.namespace.crate_name.clone(); let mut ci = ComponentInterface::new(&crate_name); - if let Some(metadata) = load_udl_metadata(&group, crate_root, &crate_name)? { + if let Some(metadata) = udl_items.remove(&crate_name) { ci.add_metadata(metadata)?; }; ci.add_metadata(group)?; - let mut config = Config::load_initial(crate_root, None)?; + let mut config = load_initial_config::(crate_root, None)?; if let Some(cdylib_name) = cdylib_name { config.update_from_cdylib_name(cdylib_name); } diff --git a/uniffi_bindgen/src/macro_metadata/ci.rs b/uniffi_bindgen/src/macro_metadata/ci.rs index 351443f66c..7ce6c3a70b 100644 --- a/uniffi_bindgen/src/macro_metadata/ci.rs +++ b/uniffi_bindgen/src/macro_metadata/ci.rs @@ -4,7 +4,9 @@ use crate::interface::{CallbackInterface, ComponentInterface, Enum, Record, Type}; use anyhow::{bail, Context}; -use uniffi_meta::{group_metadata, EnumMetadata, ErrorMetadata, Metadata, MetadataGroup}; +use uniffi_meta::{ + create_metadata_groups, group_metadata, EnumMetadata, ErrorMetadata, Metadata, MetadataGroup, +}; /// Add Metadata items to the ComponentInterface /// @@ -18,7 +20,9 @@ pub fn add_to_ci( iface: &mut ComponentInterface, metadata_items: Vec, ) -> anyhow::Result<()> { - for group in group_metadata(metadata_items)? { + let mut group_map = create_metadata_groups(&metadata_items); + group_metadata(&mut group_map, metadata_items)?; + for group in group_map.into_values() { if group.items.is_empty() { continue; } @@ -105,6 +109,9 @@ fn add_item_to_ci(iface: &mut ComponentInterface, item: Metadata) -> anyhow::Res })?; iface.add_object_meta(meta)?; } + Metadata::UniffiTrait(meta) => { + iface.add_uniffitrait_meta(meta)?; + } Metadata::CallbackInterface(meta) => { iface.types.add_known_type(&Type::CallbackInterface { module_path: meta.module_path.clone(), diff --git a/uniffi_bindgen/src/scaffolding/mod.rs b/uniffi_bindgen/src/scaffolding/mod.rs index 4febc14d24..f3759cf6fa 100644 --- a/uniffi_bindgen/src/scaffolding/mod.rs +++ b/uniffi_bindgen/src/scaffolding/mod.rs @@ -68,45 +68,20 @@ mod filters { }) } - pub fn type_ffi(type_: &FfiType) -> Result { - Ok(match type_ { - FfiType::Int8 => "i8".into(), - FfiType::UInt8 => "u8".into(), - FfiType::Int16 => "i16".into(), - FfiType::UInt16 => "u16".into(), - FfiType::Int32 => "i32".into(), - FfiType::UInt32 => "u32".into(), - FfiType::Int64 => "i64".into(), - FfiType::UInt64 => "u64".into(), - FfiType::Float32 => "f32".into(), - FfiType::Float64 => "f64".into(), - FfiType::RustArcPtr(_) => "*const std::os::raw::c_void".into(), - FfiType::RustBuffer(_) => "::uniffi::RustBuffer".into(), - FfiType::ForeignBytes => "::uniffi::ForeignBytes".into(), - FfiType::ForeignCallback => "::uniffi::ForeignCallback".into(), - FfiType::ForeignExecutorHandle => "::uniffi::ForeignExecutorHandle".into(), - FfiType::FutureCallback { return_type } => { - format!("::uniffi::FutureCallback<{}>", type_ffi(return_type)?) - } - FfiType::FutureCallbackData => "*const ()".into(), - FfiType::ForeignExecutorCallback => "::uniffi::ForeignExecutorCallback".into(), - }) - } - // Map a type to Rust code that specifies the FfiConverter implementation. // - // This outputs something like `` - pub fn ffi_converter(type_: &Type) -> Result { + // This outputs something like `>` + pub fn ffi_trait(type_: &Type, trait_name: &str) -> Result { Ok(match type_ { Type::External { name, kind: ExternalKind::Interface, .. } => { - format!("<::std::sync::Arc as ::uniffi::FfiConverter>") + format!("<::std::sync::Arc as ::uniffi::{trait_name}>") } _ => format!( - "<{} as ::uniffi::FfiConverter>", + "<{} as ::uniffi::{trait_name}>", type_rs(type_)? ), }) @@ -127,14 +102,6 @@ mod filters { }) } - // Map return types to their fully-qualified `FfiConverter` impl. - pub fn return_ffi_converter(callable: &T) -> Result { - let return_type = return_type(callable)?; - Ok(format!( - "<{return_type} as ::uniffi::FfiConverter>" - )) - } - // Turns a `crate-name` into the `crate_name` the .rs code needs to specify. pub fn crate_name_rs(nm: &str) -> Result { Ok(format!("r#{}", nm.to_string().to_snake_case())) diff --git a/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs index a196f76a9b..64c69e4d8e 100644 --- a/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs @@ -69,7 +69,7 @@ impl r#{{ trait_name }} for {{ trait_impl }} { let mut args_buf = Vec::new(); {% endif -%} {%- for arg in meth.arguments() %} - {{ arg.as_type().borrow()|ffi_converter }}::write(r#{{ arg.name() }}, &mut args_buf); + {{ arg.as_type().borrow()|ffi_trait("Lower") }}::write(r#{{ arg.name() }}, &mut args_buf); {%- endfor -%} let args_rbuf = uniffi::RustBuffer::from_vec(args_buf); diff --git a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs index 7cd9ba2ec5..94538ecaa8 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs @@ -14,9 +14,6 @@ with_try_read, {%- endif %} {%- endif %} - {%- if ci.is_callback_interface_throws_type(e.as_type()) %} - handle_unknown_callback_error, - {%- endif %} )] enum r#{{ e.name() }} { {%- for variant in e.variants() %} diff --git a/uniffi_bindgen/src/scaffolding/templates/ExternalTypesTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ExternalTypesTemplate.rs index 0a851be04e..ade1578897 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ExternalTypesTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ExternalTypesTemplate.rs @@ -1,14 +1,17 @@ // Support for external types. // Types with an external `FfiConverter`... -{% for (name, crate_name, kind) in ci.iter_external_types() %} +{% for (name, crate_name, kind, tagged) in ci.iter_external_types() %} // The FfiConverter for `{{ name }}` is defined in `{{ crate_name }}` +// If it has its existing FfiConverter defined with a UniFFITag, it needs forwarding. +{% if tagged %} {%- match kind %} {%- when ExternalKind::DataClass %} ::uniffi::ffi_converter_forward!(r#{{ name }}, ::{{ crate_name|crate_name_rs }}::UniFfiTag, crate::UniFfiTag); {%- when ExternalKind::Interface %} ::uniffi::ffi_converter_arc_forward!(r#{{ name }}, ::{{ crate_name|crate_name_rs }}::UniFfiTag, crate::UniFfiTag); {%- endmatch %} +{% endif %} {%- endfor %} // We generate support for each Custom Type and the builtin type it uses. diff --git a/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs index 1ad3d34bba..e2445c670d 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs @@ -21,13 +21,27 @@ pub trait r#{{ obj.name() }} { {{ arg.name() }}: {% if arg.by_ref() %}&{% endif %}{{ arg.as_type().borrow()|type_rs }}, {%- endfor %} ) - {%- match meth.return_type() %} - {%- when Some(return_type) %} -> {{ return_type|type_rs }}; - {%- when None %}; + {%- match (meth.return_type(), meth.throws_type()) %} + {%- when (Some(return_type), None) %} -> {{ return_type|type_rs }}; + {%- when (Some(return_type), Some(error_type)) %} -> ::std::result::Result::<{{ return_type|type_rs }}, {{ error_type|type_rs }}>; + {%- when (None, Some(error_type)) %} -> ::std::result::Result::<(), {{ error_type|type_rs }}>; + {%- when (None, None) %}; {%- endmatch %} {% endfor %} } {% when ObjectImpl::Struct %} +{%- for tm in obj.uniffi_traits() %} +{% match tm %} +{% when UniffiTrait::Debug { fmt }%} +#[uniffi::export(Debug)] +{% when UniffiTrait::Display { fmt }%} +#[uniffi::export(Display)] +{% when UniffiTrait::Hash { hash }%} +#[uniffi::export(Hash)] +{% when UniffiTrait::Eq { eq, ne }%} +#[uniffi::export(Eq)] +{% endmatch %} +{% endfor %} #[::uniffi::derive_object_for_udl] struct {{ obj.rust_name() }} { } @@ -73,65 +87,3 @@ impl {{ obj.rust_name() }} { {%- endfor %} {% endmatch %} - -{%- for tm in obj.uniffi_traits() %} -{# All magic methods get an explicit shim #} -{% match tm %} -{% when UniffiTrait::Debug { fmt }%} - {% call rs::method_decl_prelude(fmt) %} - { - uniffi::deps::static_assertions::assert_impl_all!({{ obj.rust_name() }}: std::fmt::Debug); // This object has a trait method which requires `Debug` be implemented. - format!( - "{:?}", - match as ::uniffi::FfiConverter>::try_lift(r#ptr) { - Ok(ref val) => val, - Err(err) => panic!("Failed to convert arg '{}': {}", "ptr", err), - } - ) - } - {% call rs::method_decl_postscript(fmt) %} -{% when UniffiTrait::Display { fmt }%} - {% call rs::method_decl_prelude(fmt) %} - { - uniffi::deps::static_assertions::assert_impl_all!({{ obj.rust_name() }}: std::fmt::Display); // This object has a trait method which requires `Display` be implemented. - format!( - "{}", - match as ::uniffi::FfiConverter>::try_lift(r#ptr) { - Ok(ref val) => val, - Err(err) => panic!("Failed to convert arg '{}': {}", "ptr", err), - } - ) - } - {% call rs::method_decl_postscript(fmt) %} -{% when UniffiTrait::Hash { hash }%} - {% call rs::method_decl_prelude(hash) %} - { - use ::std::hash::{Hash, Hasher}; - uniffi::deps::static_assertions::assert_impl_all!({{ obj.rust_name() }}: Hash); // This object has a trait method which requires `Hash` be implemented. - let mut s = ::std::collections::hash_map::DefaultHasher::new(); - Hash::hash(match as ::uniffi::FfiConverter>::try_lift(r#ptr) { - Ok(ref val) => val, - Err(err) => panic!("Failed to convert arg '{}': {}", "ptr", err), - }, &mut s); - s.finish() - } - {% call rs::method_decl_postscript(hash) %} -{% when UniffiTrait::Eq { eq, ne }%} - {# PartialEq::Eq #} - {% call rs::method_decl_prelude(eq) %} - { - use ::std::cmp::PartialEq; - uniffi::deps::static_assertions::assert_impl_all!({{ obj.rust_name() }}: PartialEq); // This object has a trait method which requires `PartialEq` be implemented. - PartialEq::eq({% call rs::_arg_list_rs_call(eq) -%}) - } - {% call rs::method_decl_postscript(eq) %} - {# PartialEq::Ne #} - {% call rs::method_decl_prelude(ne) %} - { - use ::std::cmp::PartialEq; - uniffi::deps::static_assertions::assert_impl_all!({{ obj.rust_name() }}: PartialEq); // This object has a trait method which requires `PartialEq` be implemented. - PartialEq::ne({% call rs::_arg_list_rs_call(ne) -%}) - } - {% call rs::method_decl_postscript(ne) %} -{% endmatch %} -{% endfor %} diff --git a/uniffi_bindgen/src/scaffolding/templates/macros.rs b/uniffi_bindgen/src/scaffolding/templates/macros.rs index 67c19b0476..8b1f94cd1c 100644 --- a/uniffi_bindgen/src/scaffolding/templates/macros.rs +++ b/uniffi_bindgen/src/scaffolding/templates/macros.rs @@ -1,42 +1,6 @@ {# // Template to receive calls into rust. -#} -{%- macro to_rs_call(func) -%} -r#{{ func.name() }}({% call _arg_list_rs_call(func) -%}) -{%- endmacro -%} - -{%- macro _arg_list_rs_call(func) %} - {%- for arg in func.full_arguments() %} - match {{- arg.as_type().borrow()|ffi_converter }}::try_lift(r#{{ arg.name() }}) { - {%- if arg.by_ref() %} - {# args passed by reference get special treatment for traits and their Box<> #} - {%- if arg.is_trait_ref() %} - Ok(ref val) => &**val, - {%- else %} - Ok(ref val) => val, - {%- endif %} - {%- else %} - {# args not passed by reference get passed directly #} - Ok(val) => val, - {%- endif %} - - {#- If this function returns an error, we attempt to downcast errors doing arg - conversions to this error. If the downcast fails or the function doesn't - return an error, we just panic. - -#} - {%- match func.throws_type() -%} - {%- when Some with (e) %} - Err(err) => return Err(uniffi::lower_anyhow_error_or_panic::(err, "{{ arg.name() }}")), - {%- else %} - Err(err) => panic!("Failed to convert arg '{}': {}", "{{ arg.name() }}", err), - {%- endmatch %} - } - {%- if !loop.last %},{% endif %} - {%- endfor %} -{%- endmacro -%} - -{#- // Arglist as used in the _UniFFILib function declarations. // Note unfiltered name but type_ffi filters. -#} @@ -58,26 +22,7 @@ r#{{ func.name() }}({% call _arg_list_rs_call(func) -%}) {% macro return_signature(func) %} {%- match func.return_type() %} -{%- when Some with (return_type) %} -> {{ return_type|ffi_converter }}::ReturnType +{%- when Some with (return_type) %} -> {{ return_type|ffi_trait("LowerReturn") }}::ReturnType {%- else -%} {%- endmatch -%} {%- endmacro -%} - -{%- macro method_decl_prelude(meth) %} -#[doc(hidden)] -#[no_mangle] -#[allow(clippy::let_unit_value,clippy::unit_arg)] // The generated code uses the unit type like other types to keep things uniform -pub extern "C" fn r#{{ meth.ffi_func().name() }}( - {%- call arg_list_ffi_decl(meth.ffi_func()) %} -) {% call return_signature(meth) %} { - uniffi::deps::log::debug!("{{ meth.ffi_func().name() }}"); - uniffi::rust_call(call_status, || { - {{ meth|return_ffi_converter }}::lower_return( -{%- endmacro %} - -{%- macro method_decl_postscript(meth) %} - {% if meth.throws() %}.map_err(Into::into){% endif %} - ) - }) -} -{% endmacro %} diff --git a/uniffi_checksum_derive/src/lib.rs b/uniffi_checksum_derive/src/lib.rs index bf1a42fc15..e600982d08 100644 --- a/uniffi_checksum_derive/src/lib.rs +++ b/uniffi_checksum_derive/src/lib.rs @@ -81,9 +81,12 @@ pub fn checksum_derive(input: TokenStream) -> TokenStream { .named .iter() .map(|field| field.ident.as_ref().unwrap()); - let field_stmts = field_idents - .clone() - .map(|ident| quote! { Checksum::checksum(#ident, state); }); + let field_stmts = fields.named.iter() + .filter(|field| !has_ignore_attribute(&field.attrs)) + .map(|field| { + let ident = field.ident.as_ref().unwrap(); + quote! { Checksum::checksum(#ident, state); } + }); quote! { Self::#ident { #(#field_idents,)* } => { #discriminant; diff --git a/uniffi_core/Cargo.toml b/uniffi_core/Cargo.toml index 6a8b894927..7b9ab4fb72 100644 --- a/uniffi_core/Cargo.toml +++ b/uniffi_core/Cargo.toml @@ -17,6 +17,7 @@ async-compat = { version = "0.2.1", optional = true } bytes = "1.3" camino = "1.0.8" log = "0.4" +once_cell = "1.10.0" # Enable "async" so that receivers implement Future, no need for "std" since we don't block on them. oneshot = { version = "0.1", features = ["async"] } # Regular dependencies diff --git a/uniffi_core/src/ffi/callbackinterface.rs b/uniffi_core/src/ffi/callbackinterface.rs index 8379289122..7be66880bb 100644 --- a/uniffi_core/src/ffi/callbackinterface.rs +++ b/uniffi_core/src/ffi/callbackinterface.rs @@ -113,7 +113,7 @@ //! type and then returns to client code. //! -use crate::{FfiConverter, ForeignCallback, ForeignCallbackCell, RustBuffer}; +use crate::{ForeignCallback, ForeignCallbackCell, Lift, LiftReturn, RustBuffer}; use std::fmt; /// The method index used by the Drop trait to communicate to the foreign language side that Rust has finished with it, @@ -169,7 +169,7 @@ impl ForeignCallbackInternals { /// Invoke a callback interface method on the foreign side and return the result pub fn invoke_callback(&self, handle: u64, method: u32, args: RustBuffer) -> R where - R: FfiConverter, + R: LiftReturn, { let mut ret_rbuf = RustBuffer::new(); let callback = self.callback_cell.get(); @@ -189,7 +189,7 @@ impl ForeignCallbackInternals { CallbackResult::Error => R::lift_callback_error(ret_rbuf), CallbackResult::UnexpectedError => { let reason = if !ret_rbuf.is_empty() { - match >::try_lift(ret_rbuf) { + match >::try_lift(ret_rbuf) { Ok(s) => s, Err(e) => { log::error!("{{ trait_name }} Error reading ret_buf: {e}"); @@ -232,3 +232,77 @@ impl fmt::Display for UnexpectedUniFFICallbackError { } impl std::error::Error for UnexpectedUniFFICallbackError {} + +// Autoref-based specialization for converting UnexpectedUniFFICallbackError into error types. +// +// For more details, see: +// https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md + +// Define two ZST types: +// - One implements `try_convert_unexpected_callback_error` by always returning an error value. +// - The specialized version implements it using `From` + +#[doc(hidden)] +#[derive(Debug)] +pub struct UnexpectedUniFFICallbackErrorConverterGeneric; + +impl UnexpectedUniFFICallbackErrorConverterGeneric { + pub fn try_convert_unexpected_callback_error( + &self, + e: UnexpectedUniFFICallbackError, + ) -> anyhow::Result { + Err(e.into()) + } +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct UnexpectedUniFFICallbackErrorConverterSpecialized; + +impl UnexpectedUniFFICallbackErrorConverterSpecialized { + pub fn try_convert_unexpected_callback_error( + &self, + e: UnexpectedUniFFICallbackError, + ) -> anyhow::Result + where + E: From, + { + Ok(E::from(e)) + } +} + +// Macro to convert an UnexpectedUniFFICallbackError value for a particular type. This is used in +// the `ConvertError` implementation. +#[doc(hidden)] +#[macro_export] +macro_rules! convert_unexpected_error { + ($error:ident, $ty:ty) => {{ + // Trait for generic conversion, implemented for all &T. + pub trait GetConverterGeneric { + fn get_converter(&self) -> $crate::UnexpectedUniFFICallbackErrorConverterGeneric; + } + + impl GetConverterGeneric for &T { + fn get_converter(&self) -> $crate::UnexpectedUniFFICallbackErrorConverterGeneric { + $crate::UnexpectedUniFFICallbackErrorConverterGeneric + } + } + // Trait for specialized conversion, implemented for all T that implements + // `Into`. I.e. it's implemented for UnexpectedUniFFICallbackError when + // ErrorType implements From. + pub trait GetConverterSpecialized { + fn get_converter(&self) -> $crate::UnexpectedUniFFICallbackErrorConverterSpecialized; + } + + impl> GetConverterSpecialized for T { + fn get_converter(&self) -> $crate::UnexpectedUniFFICallbackErrorConverterSpecialized { + $crate::UnexpectedUniFFICallbackErrorConverterSpecialized + } + } + // Here's the hack. Because of the auto-ref rules, this will use `GetConverterSpecialized` + // if it's implemented and `GetConverterGeneric` if not. + (&$error) + .get_converter() + .try_convert_unexpected_callback_error($error) + }}; +} diff --git a/uniffi_core/src/ffi/foreignbytes.rs b/uniffi_core/src/ffi/foreignbytes.rs index 5ec93118ad..9516f61844 100644 --- a/uniffi_core/src/ffi/foreignbytes.rs +++ b/uniffi_core/src/ffi/foreignbytes.rs @@ -81,7 +81,7 @@ mod test { use super::*; #[test] fn test_foreignbytes_access() { - let v = vec![1u8, 2, 3]; + let v = [1u8, 2, 3]; let fbuf = unsafe { ForeignBytes::from_raw_parts(v.as_ptr(), 3) }; assert_eq!(fbuf.len(), 3); assert_eq!(fbuf.as_slice(), &[1u8, 2, 3]); @@ -111,7 +111,7 @@ mod test { #[test] #[should_panic] fn test_foreignbytes_provided_len_must_be_non_negative() { - let v = vec![0u8, 1, 2]; + let v = [0u8, 1, 2]; let fbuf = unsafe { ForeignBytes::from_raw_parts(v.as_ptr(), -1) }; fbuf.as_slice(); } diff --git a/uniffi_core/src/ffi/foreignexecutor.rs b/uniffi_core/src/ffi/foreignexecutor.rs index b02c053c2d..7b1cb9bd80 100644 --- a/uniffi_core/src/ffi/foreignexecutor.rs +++ b/uniffi_core/src/ffi/foreignexecutor.rs @@ -246,6 +246,8 @@ mod test { is_shutdown: bool, } + unsafe impl Send for MockEventLoopInner {} + static FOREIGN_EXECUTOR_CALLBACK_INIT: Once = Once::new(); impl MockEventLoop { diff --git a/uniffi_core/src/ffi/rustbuffer.rs b/uniffi_core/src/ffi/rustbuffer.rs index 5344ab123c..e09e3be89a 100644 --- a/uniffi_core/src/ffi/rustbuffer.rs +++ b/uniffi_core/src/ffi/rustbuffer.rs @@ -49,6 +49,7 @@ use crate::ffi::{rust_call, ForeignBytes, RustCallStatus}; /// This struct is based on `ByteBuffer` from the `ffi-support` crate, but modified /// to retain unallocated capacity rather than truncating to the occupied length. #[repr(C)] +#[derive(Debug)] pub struct RustBuffer { /// The allocated capacity of the underlying `Vec`. /// In Rust this is a `usize`, but we use an `i32` for compatibility with JNA. diff --git a/uniffi_core/src/ffi/rustcalls.rs b/uniffi_core/src/ffi/rustcalls.rs index 5a84a05209..53265393c0 100644 --- a/uniffi_core/src/ffi/rustcalls.rs +++ b/uniffi_core/src/ffi/rustcalls.rs @@ -8,10 +8,10 @@ //! //! It handles: //! - Catching panics -//! - Adapting the result of `FfiConverter::lower_return()` into either a return value or an +//! - Adapting the result of `Return::lower_return()` into either a return value or an //! exception -use crate::{FfiConverter, FfiDefault, RustBuffer, UniFfiTag}; +use crate::{FfiDefault, Lower, RustBuffer, UniFfiTag}; use std::mem::MaybeUninit; use std::panic; @@ -19,13 +19,14 @@ use std::panic; /// /// ## Usage /// -/// - The consumer code creates a `RustCallStatus` with an empty `RustBuffer` and `CALL_SUCCESS` -/// (0) as the status code +/// - The consumer code creates a [RustCallStatus] with an empty [RustBuffer] and +/// [RustCallStatusCode::Success] (0) as the status code /// - A pointer to this object is passed to the rust FFI function. This is an /// "out parameter" which will be updated with any error that occurred during the function's /// execution. -/// - After the call, if `code` is `CALL_ERROR` then `error_buf` will be updated to contain -/// the serialized error object. The consumer is responsible for freeing `error_buf`. +/// - After the call, if `code` is [RustCallStatusCode::Error] or [RustCallStatusCode::UnexpectedError] +/// then `error_buf` will be updated to contain a serialized error object. See +/// [RustCallStatusCode] for what gets serialized. The consumer is responsible for freeing `error_buf`. /// /// ## Layout/fields /// @@ -38,20 +39,9 @@ use std::panic; /// RustBuffer error_buf; /// }; /// ``` -/// -/// #### The `code` field. -/// -/// - `CALL_SUCCESS` (0) for successful calls -/// - `CALL_ERROR` (1) for calls that returned an `Err` value -/// - `CALL_PANIC` (2) for calls that panicked -/// -/// #### The `error_buf` field. -/// -/// - For `CALL_ERROR` this is a `RustBuffer` with the serialized error. The consumer code is -/// responsible for freeing this `RustBuffer`. #[repr(C)] pub struct RustCallStatus { - pub code: i8, + pub code: RustCallStatusCode, // code is signed because unsigned types are experimental in Kotlin pub error_buf: MaybeUninit, // error_buf is MaybeUninit to avoid dropping the value that the consumer code sends in: @@ -65,19 +55,49 @@ pub struct RustCallStatus { // leak the first `RustBuffer`. } +impl RustCallStatus { + pub fn cancelled() -> Self { + Self { + code: RustCallStatusCode::Cancelled, + error_buf: MaybeUninit::new(RustBuffer::new()), + } + } + + pub fn error(message: impl Into) -> Self { + Self { + code: RustCallStatusCode::UnexpectedError, + error_buf: MaybeUninit::new(>::lower(message.into())), + } + } +} + impl Default for RustCallStatus { fn default() -> Self { Self { - code: 0, + code: RustCallStatusCode::Success, error_buf: MaybeUninit::uninit(), } } } -#[allow(dead_code)] -const CALL_SUCCESS: i8 = 0; // CALL_SUCCESS is set by the calling code -const CALL_ERROR: i8 = 1; -const CALL_PANIC: i8 = 2; +/// Result of a FFI call to a Rust function +#[repr(i8)] +#[derive(Debug, PartialEq, Eq)] +pub enum RustCallStatusCode { + /// Successful call. + Success = 0, + /// Expected error, corresponding to the `Result::Err` variant. [RustCallStatus::error_buf] + /// will contain the serialized error. + Error = 1, + /// Unexpected error. [RustCallStatus::error_buf] will contain a serialized message string + UnexpectedError = 2, + /// Async function cancelled. [RustCallStatus::error_buf] will be empty and does not need to + /// be freed. + /// + /// This is only returned for async functions and only if the bindings code uses the + /// [rust_future_cancel] call. + Cancelled = 3, +} /// Handle a scaffolding calls /// @@ -86,10 +106,10 @@ const CALL_PANIC: i8 = 2; /// - For errors that should be translated into thrown exceptions in the foreign code, serialize /// the error into a `RustBuffer`, then return `Ok(buf)` /// - The success type, must implement `FfiDefault`. -/// - `FfiConverter::lower_return` returns `Result<>` types that meet the above criteria> +/// - `Return::lower_return` returns `Result<>` types that meet the above criteria> /// - If the function returns a `Ok` value it will be unwrapped and returned /// - If the function returns a `Err` value: -/// - `out_status.code` will be set to `CALL_ERROR` +/// - `out_status.code` will be set to [RustCallStatusCode::Error]. /// - `out_status.error_buf` will be set to a newly allocated `RustBuffer` containing the error. The calling /// code is responsible for freeing the `RustBuffer` /// - `FfiDefault::ffi_default()` is returned, although foreign code should ignore this value @@ -125,11 +145,11 @@ where }); match result { // Happy path. Note: no need to update out_status in this case because the calling code - // initializes it to CALL_SUCCESS + // initializes it to [RustCallStatusCode::Success] Ok(Ok(v)) => Some(v), // Callback returned an Err. Ok(Err(buf)) => { - out_status.code = CALL_ERROR; + out_status.code = RustCallStatusCode::Error; unsafe { // Unsafe because we're setting the `MaybeUninit` value, see above for safety // invariants. @@ -139,7 +159,7 @@ where } // Callback panicked Err(cause) => { - out_status.code = CALL_PANIC; + out_status.code = RustCallStatusCode::UnexpectedError; // Try to coerce the cause into a RustBuffer containing a String. Since this code can // panic, we need to use a second catch_unwind(). let message_result = panic::catch_unwind(panic::AssertUnwindSafe(move || { @@ -152,7 +172,7 @@ where "Unknown panic!".to_string() }; log::error!("Caught a panic calling rust code: {:?}", message); - >::lower(message) + >::lower(message) })); if let Ok(buf) = message_result { unsafe { @@ -172,11 +192,11 @@ where #[cfg(test)] mod test { use super::*; - use crate::test_util::TestError; + use crate::{test_util::TestError, Lift, LowerReturn}; fn create_call_status() -> RustCallStatus { RustCallStatus { - code: 0, + code: RustCallStatusCode::Success, error_buf: MaybeUninit::new(RustBuffer::new()), } } @@ -193,33 +213,31 @@ mod test { fn test_rust_call() { let mut status = create_call_status(); let return_value = rust_call(&mut status, || { - as FfiConverter>::lower_return(test_callback(0)) + as LowerReturn>::lower_return(test_callback(0)) }); - assert_eq!(status.code, CALL_SUCCESS); + assert_eq!(status.code, RustCallStatusCode::Success); assert_eq!(return_value, 100); rust_call(&mut status, || { - as FfiConverter>::lower_return(test_callback(1)) + as LowerReturn>::lower_return(test_callback(1)) }); - assert_eq!(status.code, CALL_ERROR); + assert_eq!(status.code, RustCallStatusCode::Error); unsafe { assert_eq!( - >::try_lift(status.error_buf.assume_init()) - .unwrap(), + >::try_lift(status.error_buf.assume_init()).unwrap(), TestError("Error".to_owned()) ); } let mut status = create_call_status(); rust_call(&mut status, || { - as FfiConverter>::lower_return(test_callback(2)) + as LowerReturn>::lower_return(test_callback(2)) }); - assert_eq!(status.code, CALL_PANIC); + assert_eq!(status.code, RustCallStatusCode::UnexpectedError); unsafe { assert_eq!( - >::try_lift(status.error_buf.assume_init()) - .unwrap(), + >::try_lift(status.error_buf.assume_init()).unwrap(), "Unexpected value: 2" ); } diff --git a/uniffi_core/src/ffi/rustfuture.rs b/uniffi_core/src/ffi/rustfuture.rs index 27e1b2a14b..a9fd64646e 100644 --- a/uniffi_core/src/ffi/rustfuture.rs +++ b/uniffi_core/src/ffi/rustfuture.rs @@ -8,68 +8,28 @@ //! //! # The big picture //! -//! What happens when you call an async function exported from the Rust API? +//! We implement async foreign functions using a simplified version of the Future API: //! -//! 1. You make a call to a generated async function in the foreign bindings. -//! 2. That function suspends itself, then makes a scaffolding call. In addition to the normal -//! arguments, it passes a `ForeignExecutor` and callback. -//! 3. Rust uses the `ForeignExecutor` to schedules polls of the Future until it's ready. Then -//! invokes the callback. -//! 4. The callback resumes the suspended async function from (2), closing the loop. +//! 0. At startup, register a [RustFutureContinuationCallback] by calling +//! rust_future_continuation_callback_set. +//! 1. Call the scaffolding function to get a [RustFutureHandle] +//! 2a. In a loop: +//! - Call [rust_future_poll] +//! - Suspend the function until the [rust_future_poll] continuation function is called +//! - If the continuation was function was called with [RustFuturePoll::Ready], then break +//! otherwise continue. +//! 2b. If the async function is cancelled, then call [rust_future_cancel]. This causes the +//! continuation function to be called with [RustFuturePoll::Ready] and the [RustFuture] to +//! enter a cancelled state. +//! 3. Call [rust_future_complete] to get the result of the future. +//! 4. Call [rust_future_free] to free the future, ideally in a finally block. This: +//! - Releases any resources held by the future +//! - Calls any continuation callbacks that have not been called yet //! -//! # Anatomy of an async call -//! -//! Let's consider the following Rust function: -//! -//! ```rust,ignore -//! #[uniffi::export] -//! async fn hello() -> bool { -//! true -//! } -//! ``` -//! -//! In Rust, this `async fn` syntax is strictly equivalent to a normal function that returns a -//! `Future`: -//! -//! ```rust,ignore -//! #[uniffi::export] -//! fn hello() -> impl Future { /* … */ } -//! ``` -//! -//! `uniffi-bindgen` will generate a scaffolding function for each exported async function: -//! -//! ```rust,ignore -//! // The `hello` function, as seen from the outside. It inputs 3 extra arguments: -//! // - executor: used to schedule polls of the future -//! // - callback: invoked when the future is ready -//! // - callback_data: opaque pointer that's passed to the callback. It points to any state needed to -//! // resume the async function. -//! #[no_mangle] -//! pub extern "C" fn _uniffi_hello( -//! // ...If the function inputted arguments, the lowered versions would go here -//! uniffi_executor: ForeignExecutor, -//! uniffi_callback: >::FutureCallback, -//! uniffi_callback_data: *const (), -//! uniffi_call_status: &mut ::uniffi::RustCallStatus -//! ) { -//! ::uniffi::call_with_output(uniffi_call_status, || { -//! let uniffi_rust_future = RustFuture::<_, bool, crate::UniFFITag,>::new( -//! future: hello(), // the future! -//! uniffi_executor, -//! uniffi_callback, -//! uniffi_callback_data, -//! ); -//! uniffi_rust_future.wake(); -//! }) -//! } -//! ``` -//! -//! Rust will continue to poll the future until it's ready, after that. The callback will -//! eventually be invoked with these arguments: -//! - callback_data -//! - FfiConverter::ReturnType (the type that would be returned by a sync function) -//! - RustCallStatus (used to signal errors/panics when executing the future) -//! - Rust will stop polling the future, even if it's waker is invoked again. +//! Note: Technically, the foreign code calls the scaffolding versions of the `rust_future_*` +//! functions. These are generated by the scaffolding macro, specially prefixed, and extern "C", +//! and manually monomorphized in the case of [rust_future_complete]. See +//! `uniffi_macros/src/setup_scaffolding.rs` for details. //! //! ## How does `Future` work exactly? //! @@ -115,477 +75,690 @@ //! [`Waker`]: https://doc.rust-lang.org/std/task/struct.Waker.html //! [`RawWaker`]: https://doc.rust-lang.org/std/task/struct.RawWaker.html -use crate::{ - ffi::foreignexecutor::RustTaskCallbackCode, rust_call_with_out_status, schedule_raw, - FfiConverter, FfiDefault, ForeignExecutor, ForeignExecutorHandle, RustCallStatus, -}; use std::{ - cell::UnsafeCell, future::Future, + marker::PhantomData, + mem, + ops::Deref, panic, pin::Pin, - sync::{ - atomic::{AtomicU32, Ordering}, - Arc, - }, - task::{Context, Poll, RawWaker, RawWakerVTable, Waker}, + sync::{Arc, Mutex}, + task::{Context, Poll, Wake}, }; -/// Callback that we invoke when a `RustFuture` is ready. +use once_cell::sync::OnceCell; + +use crate::{rust_call_with_out_status, FfiDefault, LowerReturn, RustCallStatus}; + +/// Result code for [rust_future_poll]. This is passed to the continuation function. +#[repr(i8)] +#[derive(Debug, PartialEq, Eq)] +pub enum RustFuturePoll { + /// The future is ready and is waiting for [rust_future_complete] to be called + Ready = 0, + /// The future might be ready and [rust_future_poll] should be called again + MaybeReady = 1, +} + +/// Foreign callback that's passed to [rust_future_poll] /// -/// The foreign code passes a pointer to one of these callbacks along with an opaque data pointer. -/// When the future is ready, we invoke the callback. -pub type FutureCallback = - extern "C" fn(callback_data: *const (), result: T, status: RustCallStatus); +/// The Rust side of things calls this when the foreign side should call [rust_future_poll] again +/// to continue progress on the future. +pub type RustFutureContinuationCallback = extern "C" fn(callback_data: *const (), RustFuturePoll); + +/// Opaque handle for a Rust future that's stored by the foreign language code +#[repr(transparent)] +pub struct RustFutureHandle(*const ()); + +/// Stores the global continuation callback +static RUST_FUTURE_CONTINUATION_CALLBACK_CELL: OnceCell = + OnceCell::new(); + +/// Set the global RustFutureContinuationCallback. +pub fn rust_future_continuation_callback_set(callback: RustFutureContinuationCallback) { + if let Err(existing) = RUST_FUTURE_CONTINUATION_CALLBACK_CELL.set(callback) { + // Don't panic if this to be called multiple times with the same callback. + if existing != callback { + panic!("Attempt to set the RustFuture continuation callback twice"); + } + } +} -/// Future that the foreign code is awaiting +fn call_continuation(data: *const (), poll_code: RustFuturePoll) { + let callback = RUST_FUTURE_CONTINUATION_CALLBACK_CELL + .get() + .expect("RustFuture continuation callback not set. This is likely a uniffi bug."); + callback(data, poll_code) +} + +// === Public FFI API === + +/// Create a new [RustFutureHandle] /// -/// RustFuture is always stored inside a Pin>. The `Arc<>` allows it to be shared between -/// wakers and Pin<> signals that it must not move, since this would break any self-references in -/// the future. -pub struct RustFuture +/// For each exported async function, UniFFI will create a scaffolding function that uses this to +/// create the [RustFutureHandle] to pass to the foreign code. +pub fn rust_future_new(future: F, tag: UT) -> RustFutureHandle where - // The future needs to be `Send`, since it will move to whatever thread the foreign executor - // chooses. However, it doesn't need to be `Sync', since we don't share references between - // threads (see do_wake()). - F: Future + Send, - T: FfiConverter, + // F is the future type returned by the exported async function. It needs to be Send + `static + // since it will move between threads for an indeterminate amount of time as the foreign + // executor calls polls it and the Rust executor wakes it. It does not need to by `Sync`, + // since we synchronize all access to the values. + F: Future + Send + 'static, + // T is the output of the Future. It needs to implement [LowerReturn]. Also it must be Send + + // 'static for the same reason as F. + T: LowerReturn + Send + 'static, + // The UniFfiTag ZST. The Send + 'static bound is to keep rustc happy. + UT: Send + 'static, { - future: UnsafeCell, - executor: ForeignExecutor, - wake_counter: AtomicU32, - callback: T::FutureCallback, - callback_data: *const (), + // Create a RustFuture and coerce to `Arc`, which is what we use to + // implement the FFI + let future_ffi = RustFuture::new(future, tag) as Arc>; + // Box the Arc, to convert the wide pointer into a normal sized pointer so that we can pass it + // to the foreign code. + let boxed_ffi = Box::new(future_ffi); + // We can now create a RustFutureHandle + RustFutureHandle(Box::into_raw(boxed_ffi) as *mut ()) } -// Mark `RustFuture` as `Send` + `Sync`, since we will be sharing it between threads. -// This means we need to serialize access to any fields that aren't `Send` + `Sync` (`future`, `callback`, and `callback_data`). -// See `do_wake()` for details on this. +/// Poll a Rust future +/// +/// When the future is ready to progress the continuation will be called with the `data` value and +/// a [RustFuturePoll] value. For each [rust_future_poll] call the continuation will be called +/// exactly once. +/// +/// # Safety +/// +/// The [RustFutureHandle] must not previously have been passed to [rust_future_free] +pub unsafe fn rust_future_poll(handle: RustFutureHandle, data: *const ()) { + let future = &*(handle.0 as *mut Arc>); + future.clone().ffi_poll(data) +} -unsafe impl Send for RustFuture -where - F: Future + Send, - T: FfiConverter, -{ +/// Cancel a Rust future +/// +/// Any current and future continuations will be immediately called with RustFuturePoll::Ready. +/// +/// This is needed for languages like Swift, which continuation to wait for the continuation to be +/// called when tasks are cancelled. +/// +/// # Safety +/// +/// The [RustFutureHandle] must not previously have been passed to [rust_future_free] +pub unsafe fn rust_future_cancel(handle: RustFutureHandle) { + let future = &*(handle.0 as *mut Arc>); + future.clone().ffi_cancel() } -unsafe impl Sync for RustFuture +/// Complete a Rust future +/// +/// Note: the actually extern "C" scaffolding functions can't be generic, so we generate one for +/// each supported FFI type. +/// +/// # Safety +/// +/// - The [RustFutureHandle] must not previously have been passed to [rust_future_free] +/// - The `T` param must correctly correspond to the [rust_future_new] call. It must +/// be `>::ReturnType` +pub unsafe fn rust_future_complete( + handle: RustFutureHandle, + out_status: &mut RustCallStatus, +) -> ReturnType { + let future = &*(handle.0 as *mut Arc>); + future.ffi_complete(out_status) +} + +/// Free a Rust future, dropping the strong reference and releasing all references held by the +/// future. +/// +/// # Safety +/// +/// The [RustFutureHandle] must not previously have been passed to [rust_future_free] +pub unsafe fn rust_future_free(handle: RustFutureHandle) { + let future = Box::from_raw(handle.0 as *mut Arc>); + future.ffi_free() +} + +/// Thread-safe storage for [RustFutureContinuationCallback] data +/// +/// The basic guarantee is that all data pointers passed in are passed out exactly once to the +/// foreign continuation callback. This enables us to uphold the [rust_future_poll] guarantee. +/// +/// [ContinuationDataCell] also tracks cancellation, which is closely tied to continuation data. +#[derive(Debug)] +enum ContinuationDataCell { + /// No continuations set, neither wake() nor cancel() called. + Empty, + /// `wake()` was called when there was no continuation set. The next time `store` is called, + /// the continuation should be immediately invoked with `RustFuturePoll::MaybeReady` + Waked, + /// The future has been cancelled, any future `store` calls should immediately result in the + /// continuation being called with `RustFuturePoll::Ready`. + Cancelled, + /// Continuation data set, the next time `wake()` is called is called, we should invoke the + /// continuation with it. + Set(*const ()), +} + +impl ContinuationDataCell { + fn new() -> Self { + Self::Empty + } + + /// Store new continuation data if we are in the `Empty` state. If we are in the `Waked` or + /// `Cancelled` state, call the continuation immediately with the data. + fn store(&mut self, data: *const ()) { + match self { + Self::Empty => *self = Self::Set(data), + Self::Set(old_data) => { + log::error!( + "store: observed `Self::Set` state. Is poll() being called from multiple threads at once?" + ); + call_continuation(*old_data, RustFuturePoll::Ready); + *self = Self::Set(data); + } + Self::Waked => { + *self = Self::Empty; + call_continuation(data, RustFuturePoll::MaybeReady); + } + Self::Cancelled => { + call_continuation(data, RustFuturePoll::Ready); + } + } + } + + fn wake(&mut self) { + match self { + // If we had a continuation set, then call it and transition to the `Empty` state. + Self::Set(old_data) => { + let old_data = *old_data; + *self = Self::Empty; + call_continuation(old_data, RustFuturePoll::MaybeReady); + } + // If we were in the `Empty` state, then transition to `Waked`. The next time `store` + // is called, we will immediately call the continuation. + Self::Empty => *self = Self::Waked, + // This is a no-op if we were in the `Cancelled` or `Waked` state. + _ => (), + } + } + + fn cancel(&mut self) { + if let Self::Set(old_data) = mem::replace(self, Self::Cancelled) { + call_continuation(old_data, RustFuturePoll::Ready); + } + } + + fn is_cancelled(&self) -> bool { + matches!(self, Self::Cancelled) + } +} + +// ContinuationDataCell is Send + Sync as long we handle the *const () pointer correctly + +unsafe impl Send for ContinuationDataCell {} +unsafe impl Sync for ContinuationDataCell {} + +/// Wraps the actual future we're polling +struct WrappedFuture where - F: Future + Send, - T: FfiConverter, + // See rust_future_new for an explanation of these trait bounds + F: Future + Send + 'static, + T: LowerReturn + Send + 'static, + UT: Send + 'static, { + // Note: this could be a single enum, but that would make it easy to mess up the future pinning + // guarantee. For example you might want to call `std::mem::take()` to try to get the result, + // but if the future happened to be stored that would move and break all internal references. + future: Option, + result: Option>, } -impl RustFuture +impl WrappedFuture where - F: Future + Send, - T: FfiConverter, + // See rust_future_new for an explanation of these trait bounds + F: Future + Send + 'static, + T: LowerReturn + Send + 'static, + UT: Send + 'static, { - pub fn new( - future: F, - executor_handle: ForeignExecutorHandle, - callback: T::FutureCallback, - callback_data: *const (), - ) -> Pin> { - let executor = - >::try_lift(executor_handle) - .expect("Error lifting ForeignExecutorHandle"); - Arc::pin(Self { - future: UnsafeCell::new(future), - wake_counter: AtomicU32::new(0), - executor, - callback, - callback_data, - }) - } - - /// Wake up soon and poll our future. - /// - /// This method ensures that a call to `do_wake()` is scheduled. Only one call will be scheduled - /// at any time, even if `wake_soon` called multiple times from multiple threads. - pub fn wake(self: Pin>) { - if self.wake_counter.fetch_add(1, Ordering::Relaxed) == 0 { - self.schedule_do_wake(); + fn new(future: F) -> Self { + Self { + future: Some(future), + result: None, } } - /// Schedule `do_wake`. - /// - /// `self` is consumed but _NOT_ dropped, it's purposely leaked via `into_raw()`. - /// `wake_callback()` will call `from_raw()` to reverse the leak. - fn schedule_do_wake(self: Pin>) { - unsafe { - let handle = self.executor.handle; - let raw_ptr = Arc::into_raw(Pin::into_inner_unchecked(self)); - // SAFETY: The `into_raw()` / `from_raw()` contract guarantees that our executor cannot - // be dropped before we call `from_raw()` on the raw pointer. This means we can safely - // use its handle to schedule a callback. - if !schedule_raw(handle, 0, Self::wake_callback, raw_ptr as *const ()) { - // There was an error scheduling the callback, drop the arc reference since - // `wake_callback()` will never be called + // Poll the future and check if it's ready or not + fn poll(&mut self, context: &mut Context<'_>) -> bool { + if self.result.is_some() { + true + } else if let Some(future) = &mut self.future { + // SAFETY: We can call Pin::new_unchecked because: + // - This is the only time we get a &mut to `self.future` + // - We never poll the future after it's moved (for example by using take()) + // - We never move RustFuture, which contains us. + // - RustFuture is private to this module so no other code can move it. + let pinned = unsafe { Pin::new_unchecked(future) }; + // Run the poll and lift the result if it's ready + let mut out_status = RustCallStatus::default(); + let result: Option> = rust_call_with_out_status( + &mut out_status, + // This closure uses a `&mut F` value, which means it's not UnwindSafe by + // default. If the future panics, it may be in an invalid state. // - // Note: specifying the `` generic is a good safety measure. Things would go - // very bad if Rust inferred the wrong type. - // - // However, the `Pin<>` part doesn't matter since its `repr(transparent)`. - Arc::::decrement_strong_count(raw_ptr); + // However, we can safely use `AssertUnwindSafe` since a panic will lead the `None` + // case below and we will never poll the future again. + panic::AssertUnwindSafe(|| match pinned.poll(context) { + Poll::Pending => Ok(Poll::Pending), + Poll::Ready(v) => T::lower_return(v).map(Poll::Ready), + }), + ); + match result { + Some(Poll::Pending) => false, + Some(Poll::Ready(v)) => { + self.future = None; + self.result = Some(Ok(v)); + true + } + None => { + self.future = None; + self.result = Some(Err(out_status)); + true + } } + } else { + log::error!("poll with neither future nor result set"); + true } } - extern "C" fn wake_callback(self_ptr: *const (), status_code: RustTaskCallbackCode) { - // No matter what, call `Arc::from_raw()` to balance the `Arc::into_raw()` call in - // `schedule_do_wake()`. - let task = unsafe { Pin::new_unchecked(Arc::from_raw(self_ptr as *const Self)) }; - if status_code == RustTaskCallbackCode::Success { - // Only drive the future forward on `RustTaskCallbackCode::Success`. - // `RUST_TASK_CALLBACK_CANCELED` indicates the foreign executor has been cancelled / - // shutdown and we should not continue. - task.do_wake(); + fn complete(&mut self, out_status: &mut RustCallStatus) -> T::ReturnType { + let mut return_value = T::ReturnType::ffi_default(); + match self.result.take() { + Some(Ok(v)) => return_value = v, + Some(Err(call_status)) => *out_status = call_status, + None => *out_status = RustCallStatus::cancelled(), } + self.free(); + return_value } - // Does the work for wake, we take care to ensure this always runs in a serialized fashion. - fn do_wake(self: Pin>) { - // Store 1 in `waker_counter`, which we'll use at the end of this call. - self.wake_counter.store(1, Ordering::Relaxed); - - // Pin<&mut> from our UnsafeCell. &mut is is safe, since this is the only reference we - // ever take to `self.future` and calls to this function are serialized. Pin<> is safe - // since we never move the future out of `self.future`. - let future = unsafe { Pin::new_unchecked(&mut *self.future.get()) }; - let waker = self.make_waker(); - - // Run the poll and lift the result if it's ready - let mut out_status = RustCallStatus::default(); - let result: Option> = rust_call_with_out_status( - &mut out_status, - // This closure uses a `&mut F` value, which means it's not UnwindSafe by default. If - // the closure panics, the future may be in an invalid state. - // - // However, we can safely use `AssertUnwindSafe` since a panic will lead the `Err()` - // case below. In that case, we will never run `do_wake()` again and will no longer - // access the future. - panic::AssertUnwindSafe(|| match future.poll(&mut Context::from_waker(&waker)) { - Poll::Pending => Ok(Poll::Pending), - Poll::Ready(v) => T::lower_return(v).map(Poll::Ready), - }), - ); + fn free(&mut self) { + self.future = None; + self.result = None; + } +} - // All the main work is done, time to finish up - match result { - Some(Poll::Pending) => { - // Since we set `wake_counter` to 1 at the start of this function... - // - If it's > 1 now, then some other code tried to wake us while we were polling - // the future. Schedule another poll in this case. - // - If it's still 1, then exit after decrementing it. The next call to `wake()` - // will schedule a poll. - if self.wake_counter.fetch_sub(1, Ordering::Relaxed) > 1 { - self.schedule_do_wake(); - } - } - // Success! Call our callback. - // - // Don't decrement `wake_counter'. This way, if wake() is called in the future, we - // will just ignore it - Some(Poll::Ready(v)) => { - T::invoke_future_callback(self.callback, self.callback_data, v, out_status); - } - // Error/panic polling the future. Call the callback with a default value. - // `out_status` contains the error code and serialized error. Again, don't decrement - // `wake_counter'. - None => { - T::invoke_future_callback( - self.callback, - self.callback_data, - T::ReturnType::ffi_default(), - out_status, - ); - } - }; +// If F and T are Send, then WrappedFuture is too +// +// Rust will not mark it Send by default when T::ReturnType is a raw pointer. This is promising +// that we will treat the raw pointer properly, for example by not returning it twice. +unsafe impl Send for WrappedFuture +where + // See rust_future_new for an explanation of these trait bounds + F: Future + Send + 'static, + T: LowerReturn + Send + 'static, + UT: Send + 'static, +{ +} + +/// Future that the foreign code is awaiting +struct RustFuture +where + // See rust_future_new for an explanation of these trait bounds + F: Future + Send + 'static, + T: LowerReturn + Send + 'static, + UT: Send + 'static, +{ + // This Mutex should never block if our code is working correctly, since there should not be + // multiple threads calling [Self::poll] and/or [Self::complete] at the same time. + future: Mutex>, + continuation_data: Mutex, + // UT is used as the generic parameter for [LowerReturn]. + // Let's model this with PhantomData as a function that inputs a UT value. + _phantom: PhantomData ()>, +} + +impl RustFuture +where + // See rust_future_new for an explanation of these trait bounds + F: Future + Send + 'static, + T: LowerReturn + Send + 'static, + UT: Send + 'static, +{ + fn new(future: F, _tag: UT) -> Arc { + Arc::new(Self { + future: Mutex::new(WrappedFuture::new(future)), + continuation_data: Mutex::new(ContinuationDataCell::new()), + _phantom: PhantomData, + }) } - fn make_waker(self: &Pin>) -> Waker { - // This is safe as long as we implement the waker interface correctly. - unsafe { - Waker::from_raw(RawWaker::new( - self.clone().into_raw(), - &Self::RAW_WAKER_VTABLE, - )) + fn poll(self: Arc, data: *const ()) { + let ready = self.is_cancelled() || { + let mut locked = self.future.lock().unwrap(); + let waker: std::task::Waker = Arc::clone(&self).into(); + locked.poll(&mut Context::from_waker(&waker)) + }; + if ready { + call_continuation(data, RustFuturePoll::Ready) + } else { + self.continuation_data.lock().unwrap().store(data); } } - /// SAFETY: Ensure that all calls to `into_raw()` are balanced with a call to `from_raw()` - fn into_raw(self: Pin>) -> *const () { - unsafe { Arc::into_raw(Pin::into_inner_unchecked(self)) as *const () } + fn is_cancelled(&self) -> bool { + self.continuation_data.lock().unwrap().is_cancelled() } - /// Consume a pointer to get an arc - /// - /// SAFETY: The pointer must have come from `into_raw()` or was cloned with `raw_clone()`. - /// Once a pointer is passed into this function, it should no longer be used. - fn from_raw(self_ptr: *const ()) -> Pin> { - unsafe { Pin::new_unchecked(Arc::from_raw(self_ptr as *const Self)) } + fn wake(&self) { + self.continuation_data.lock().unwrap().wake(); } - // Implement the waker interface by defining a RawWakerVTable - // - // We could also handle this by implementing the `Wake` interface, but that uses an `Arc` - // instead of a `Pin>` and in theory it could try to move the `T` value out of the arc - // with something like `Arc::try_unwrap()` which would break the pinning contract with - // `Future`. Implementing this using `RawWakerVTable` allows us verify that this doesn't - // happen. - const RAW_WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new( - Self::raw_clone, - Self::raw_wake, - Self::raw_wake_by_ref, - Self::raw_drop, - ); - - /// This function will be called when the `RawWaker` gets cloned, e.g. when - /// the `Waker` in which the `RawWaker` is stored gets cloned. - unsafe fn raw_clone(self_ptr: *const ()) -> RawWaker { - Arc::increment_strong_count(self_ptr as *const Self); - RawWaker::new(self_ptr, &Self::RAW_WAKER_VTABLE) - } - - /// This function will be called when `wake` is called on the `Waker`. It - /// must wake up the task associated with this `RawWaker`. - unsafe fn raw_wake(self_ptr: *const ()) { - Self::from_raw(self_ptr).wake() - } - - /// This function will be called when `wake_by_ref` is called on the - /// `Waker`. It must wake up the task associated with this `RawWaker`. - unsafe fn raw_wake_by_ref(self_ptr: *const ()) { - // This could be optimized by only incrementing the strong count if we end up calling - // `schedule_do_wake()`, but it's not clear that's worth the extra complexity - Arc::increment_strong_count(self_ptr as *const Self); - Self::from_raw(self_ptr).wake() - } - - /// This function gets called when a `RawWaker` gets dropped. - /// This function gets called when a `RawWaker` gets dropped. - unsafe fn raw_drop(self_ptr: *const ()) { - drop(Self::from_raw(self_ptr)) + fn cancel(&self) { + self.continuation_data.lock().unwrap().cancel(); + } + + fn complete(&self, call_status: &mut RustCallStatus) -> T::ReturnType { + self.future.lock().unwrap().complete(call_status) + } + + fn free(self: Arc) { + // Call cancel() to send any leftover data to the continuation callback + self.continuation_data.lock().unwrap().cancel(); + // Ensure we drop our inner future, releasing all held references + self.future.lock().unwrap().free(); } } -#[cfg(test)] -mod tests { - use super::*; - use crate::{test_util::TestError, try_lift_from_rust_buffer, MockEventLoop}; - use std::sync::Weak; +impl Wake for RustFuture +where + // See rust_future_new for an explanation of these trait bounds + F: Future + Send + 'static, + T: LowerReturn + Send + 'static, + UT: Send + 'static, +{ + fn wake(self: Arc) { + self.deref().wake() + } - // Mock future that we can manually control using an Option<> - struct MockFuture(Option>); + fn wake_by_ref(self: &Arc) { + self.deref().wake() + } +} - impl Future for MockFuture { - type Output = Result; +/// RustFuture FFI trait. This allows `Arc>` to be cast to +/// `Arc>`, which is needed to implement the public FFI API. In particular, this +/// allows you to use RustFuture functionality without knowing the concrete Future type, which is +/// unnamable. +/// +/// This is parametrized on the ReturnType rather than the `T` directly, to reduce the number of +/// scaffolding functions we need to generate. If it was parametrized on `T`, then we would need +/// to create a poll, cancel, complete, and free scaffolding function for each exported async +/// function. That would add ~1kb binary size per exported function based on a quick estimate on a +/// x86-64 machine . By parametrizing on `T::ReturnType` we can instead monomorphize by hand and +/// only create those functions for each of the 13 possible FFI return types. +#[doc(hidden)] +trait RustFutureFfi { + fn ffi_poll(self: Arc, data: *const ()); + fn ffi_cancel(&self); + fn ffi_complete(&self, call_status: &mut RustCallStatus) -> ReturnType; + fn ffi_free(self: Arc); +} - fn poll(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll { - match &self.0 { - Some(v) => Poll::Ready(v.clone()), - None => Poll::Pending, - } - } +impl RustFutureFfi for RustFuture +where + // See rust_future_new for an explanation of these trait bounds + F: Future + Send + 'static, + T: LowerReturn + Send + 'static, + UT: Send + 'static, +{ + fn ffi_poll(self: Arc, data: *const ()) { + self.poll(data) } - // Type alias for the RustFuture we'll use in our tests - type TestRustFuture = RustFuture, crate::UniFfiTag>; + fn ffi_cancel(&self) { + self.cancel() + } - // Stores the result that we send to the foreign code - #[derive(Default)] - struct MockForeignResult { - value: i8, - status: RustCallStatus, + fn ffi_complete(&self, call_status: &mut RustCallStatus) -> T::ReturnType { + self.complete(call_status) } - extern "C" fn mock_foreign_callback(data_ptr: *const (), value: i8, status: RustCallStatus) { - println!("mock_foreign_callback: {value} {data_ptr:?}"); - let result: &mut Option = - unsafe { &mut *(data_ptr as *mut Option) }; - *result = Some(MockForeignResult { value, status }); + fn ffi_free(self: Arc) { + self.free(); } +} - // Bundles everything together so that we can run some tests - struct TestFutureEnvironment { - rust_future: Pin>, - foreign_result: Pin>>, +#[cfg(test)] +mod tests { + use super::*; + use crate::{test_util::TestError, Lift, RustBuffer, RustCallStatusCode}; + use once_cell::sync::OnceCell; + use std::task::Waker; + + // Sender/Receiver pair that we use for testing + struct Channel { + result: Option>, + waker: Option, } - impl TestFutureEnvironment { - fn new(eventloop: &Arc) -> Self { - let foreign_result = Box::pin(None); - let foreign_result_ptr = &*foreign_result as *const Option<_> as *const (); + struct Sender(Arc>); - let rust_future = TestRustFuture::new( - MockFuture(None), - eventloop.new_handle(), - mock_foreign_callback, - foreign_result_ptr, - ); - Self { - rust_future, - foreign_result, + impl Sender { + fn wake(&self) { + let inner = self.0.lock().unwrap(); + if let Some(waker) = &inner.waker { + waker.wake_by_ref(); } } - fn wake(&self) { - self.rust_future.clone().wake(); + fn send(&self, value: Result) { + let mut inner = self.0.lock().unwrap(); + if inner.result.replace(value).is_some() { + panic!("value already sent"); + } + if let Some(waker) = &inner.waker { + waker.wake_by_ref(); + } } + } - fn rust_future_weak(&self) -> Weak { - // It seems like there's not a great way to get an &Arc from a Pin, so we need to - // do a little dance here - Arc::downgrade(&Pin::into_inner(Clone::clone(&self.rust_future))) - } + struct Receiver(Arc>); + + impl Future for Receiver { + type Output = Result; - fn complete_future(&self, value: Result) { - unsafe { - (*self.rust_future.future.get()).0 = Some(value); + fn poll( + self: Pin<&mut Self>, + context: &mut Context<'_>, + ) -> Poll> { + let mut inner = self.0.lock().unwrap(); + match &inner.result { + Some(v) => Poll::Ready(v.clone()), + None => { + inner.waker = Some(context.waker().clone()); + Poll::Pending + } } } } + // Create a sender and rust future that we can use for testing + fn channel() -> (Sender, Arc>) { + let channel = Arc::new(Mutex::new(Channel { + result: None, + waker: None, + })); + let rust_future = RustFuture::new(Receiver(channel.clone()), crate::UniFfiTag); + (Sender(channel), rust_future) + } + + /// Poll a Rust future and get an OnceCell that's set when the continuation is called + fn poll(rust_future: &Arc>) -> Arc> { + let cell = Arc::new(OnceCell::new()); + let cell_ptr = Arc::into_raw(cell.clone()) as *const (); + rust_future.clone().ffi_poll(cell_ptr); + cell + } + + fn setup_continuation_callback() { + let _ = RUST_FUTURE_CONTINUATION_CALLBACK_CELL.set(poll_continuation); + } + + extern "C" fn poll_continuation(data: *const (), code: RustFuturePoll) { + let cell = unsafe { Arc::from_raw(data as *const OnceCell) }; + cell.set(code).expect("Error setting OnceCell"); + } + + fn complete(rust_future: Arc>) -> (RustBuffer, RustCallStatus) { + let mut out_status_code = RustCallStatus::default(); + let return_value = rust_future.ffi_complete(&mut out_status_code); + (return_value, out_status_code) + } + #[test] - fn test_wake() { - let eventloop = MockEventLoop::new(); - let mut test_env = TestFutureEnvironment::new(&eventloop); - // Initially, we shouldn't have a result and nothing should be scheduled - assert!(test_env.foreign_result.is_none()); - assert_eq!(eventloop.call_count(), 0); - - // wake() should schedule a call - test_env.wake(); - assert_eq!(eventloop.call_count(), 1); - - // When that call runs, we should still not have a result yet - eventloop.run_all_calls(); - assert!(test_env.foreign_result.is_none()); - assert_eq!(eventloop.call_count(), 0); - - // Multiple wakes should only result in 1 scheduled call - test_env.wake(); - test_env.wake(); - assert_eq!(eventloop.call_count(), 1); - - // Make the future ready, which should call mock_foreign_callback and set the result - test_env.complete_future(Ok(true)); - eventloop.run_all_calls(); - let result = test_env - .foreign_result - .take() - .expect("Expected result to be set"); - assert_eq!(result.value, 1); - assert_eq!(result.status.code, 0); - assert_eq!(eventloop.call_count(), 0); - - // Future wakes shouldn't schedule any calls - test_env.wake(); - assert_eq!(eventloop.call_count(), 0); + fn test_success() { + setup_continuation_callback(); + let (sender, rust_future) = channel(); + + // Test polling the rust future before it's ready + let continuation_result = poll(&rust_future); + assert_eq!(continuation_result.get(), None); + sender.wake(); + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::MaybeReady)); + + // Test polling the rust future when it's ready + let continuation_result = poll(&rust_future); + assert_eq!(continuation_result.get(), None); + sender.send(Ok("All done".into())); + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::MaybeReady)); + + // Future polls should immediately return ready + let continuation_result = poll(&rust_future); + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::Ready)); + + // Complete the future + let (return_buf, call_status) = complete(rust_future); + assert_eq!(call_status.code, RustCallStatusCode::Success); + assert_eq!( + >::try_lift(return_buf).unwrap(), + "All done" + ); } #[test] fn test_error() { - let eventloop = MockEventLoop::new(); - let mut test_env = TestFutureEnvironment::new(&eventloop); - test_env.complete_future(Err("Something went wrong".into())); - test_env.wake(); - eventloop.run_all_calls(); - let result = test_env - .foreign_result - .take() - .expect("Expected result to be set"); - assert_eq!(result.status.code, 1); + setup_continuation_callback(); + let (sender, rust_future) = channel(); + + let continuation_result = poll(&rust_future); + assert_eq!(continuation_result.get(), None); + sender.send(Err("Something went wrong".into())); + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::MaybeReady)); + + let continuation_result = poll(&rust_future); + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::Ready)); + + let (_, call_status) = complete(rust_future); + assert_eq!(call_status.code, RustCallStatusCode::Error); unsafe { assert_eq!( - try_lift_from_rust_buffer::( - result.status.error_buf.assume_init() + >::try_lift_from_rust_buffer( + call_status.error_buf.assume_init() ) .unwrap(), - String::from("Something went wrong"), + TestError::from("Something went wrong"), ) } - assert_eq!(eventloop.call_count(), 0); } + // Once `complete` is called, the inner future should be released, even if wakers still hold a + // reference to the RustFuture #[test] - fn test_raw_clone_and_drop() { - let test_env = TestFutureEnvironment::new(&MockEventLoop::new()); - let waker = test_env.rust_future.make_waker(); - let weak_ref = test_env.rust_future_weak(); - assert_eq!(weak_ref.strong_count(), 2); - let waker2 = waker.clone(); - assert_eq!(weak_ref.strong_count(), 3); - drop(waker); - assert_eq!(weak_ref.strong_count(), 2); - drop(waker2); - assert_eq!(weak_ref.strong_count(), 1); - drop(test_env); - assert_eq!(weak_ref.strong_count(), 0); - assert!(weak_ref.upgrade().is_none()); + fn test_cancel() { + setup_continuation_callback(); + let (_sender, rust_future) = channel(); + + let continuation_result = poll(&rust_future); + assert_eq!(continuation_result.get(), None); + rust_future.ffi_cancel(); + // Cancellation should immediately invoke the callback with RustFuturePoll::Ready + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::Ready)); + + // Future polls should immediately invoke the callback with RustFuturePoll::Ready + let continuation_result = poll(&rust_future); + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::Ready)); + + let (_, call_status) = complete(rust_future); + assert_eq!(call_status.code, RustCallStatusCode::Cancelled); } + // Once `free` is called, the inner future should be released, even if wakers still hold a + // reference to the RustFuture #[test] - fn test_raw_wake() { - let eventloop = MockEventLoop::new(); - let test_env = TestFutureEnvironment::new(&eventloop); - let waker = test_env.rust_future.make_waker(); - let weak_ref = test_env.rust_future_weak(); - // `test_env` and `waker` both hold a strong reference to the `RustFuture` - assert_eq!(weak_ref.strong_count(), 2); - - // wake_by_ref() should schedule a wake - waker.wake_by_ref(); - assert_eq!(eventloop.call_count(), 1); - - // Once the wake runs, the strong could should not have changed - eventloop.run_all_calls(); - assert_eq!(weak_ref.strong_count(), 2); - - // wake() should schedule a wake - waker.wake(); - assert_eq!(eventloop.call_count(), 1); - - // Once the wake runs, the strong have decremented, since wake() consumes the waker - eventloop.run_all_calls(); - assert_eq!(weak_ref.strong_count(), 1); + fn test_release_future() { + setup_continuation_callback(); + let (sender, rust_future) = channel(); + // Create a weak reference to the channel to use to check if rust_future has dropped its + // future. + let channel_weak = Arc::downgrade(&sender.0); + drop(sender); + // Create an extra ref to rust_future, simulating a waker that still holds a reference to + // it + let rust_future2 = rust_future.clone(); + + // Complete the rust future + rust_future.ffi_free(); + // Even though rust_future is still alive, the channel shouldn't be + assert!(Arc::strong_count(&rust_future2) > 0); + assert_eq!(channel_weak.strong_count(), 0); + assert!(channel_weak.upgrade().is_none()); } - // Test trying to create a RustFuture before the executor is shutdown. + // If `free` is called with a continuation still stored, we should call it them then. // - // The main thing we're testing is that we correctly drop the Future in this case + // This shouldn't happen in practice, but it seems like good defensive programming #[test] - fn test_executor_shutdown() { - let eventloop = MockEventLoop::new(); - eventloop.shutdown(); - let test_env = TestFutureEnvironment::new(&eventloop); - let weak_ref = test_env.rust_future_weak(); - // When we wake the future, it should try to schedule a callback and fail. This should - // cause the future to be dropped - test_env.wake(); - drop(test_env); - assert!(weak_ref.upgrade().is_none()); - } - - // Similar run a similar test to the last, but simulate an executor shutdown after the future was - // scheduled, but before the callback is called. + fn test_complete_with_stored_continuation() { + setup_continuation_callback(); + let (_sender, rust_future) = channel(); + + let continuation_result = poll(&rust_future); + rust_future.ffi_free(); + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::Ready)); + } + + // Test what happens if we see a `wake()` call while we're polling the future. This can + // happen, for example, with futures that are handled by a tokio thread pool. We should + // schedule another poll of the future in this case. #[test] - fn test_executor_shutdown_after_schedule() { - let eventloop = MockEventLoop::new(); - let test_env = TestFutureEnvironment::new(&eventloop); - let weak_ref = test_env.rust_future_weak(); - test_env.complete_future(Ok(true)); - test_env.wake(); - eventloop.shutdown(); - eventloop.run_all_calls(); - - // Test that the foreign async side wasn't completed. Even though we could have - // driven the future to completion, we shouldn't have since the executor was shutdown - assert!(test_env.foreign_result.is_none()); - // Also test that we've dropped all references to the future - drop(test_env); - assert!(weak_ref.upgrade().is_none()); + fn test_wake_during_poll() { + setup_continuation_callback(); + let mut first_time = true; + let future = std::future::poll_fn(move |ctx| { + if first_time { + first_time = false; + // Wake the future while we are in the middle of polling it + ctx.waker().clone().wake(); + Poll::Pending + } else { + // The second time we're polled, we're ready + Poll::Ready("All done".to_owned()) + } + }); + let rust_future: Arc> = + RustFuture::new(future, crate::UniFfiTag); + let continuation_result = poll(&rust_future); + // The continuation function should called immediately + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::MaybeReady)); + // A second poll should finish the future + let continuation_result = poll(&rust_future); + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::Ready)); + let (return_buf, call_status) = complete(rust_future); + assert_eq!(call_status.code, RustCallStatusCode::Success); + assert_eq!( + >::try_lift(return_buf).unwrap(), + "All done" + ); } } diff --git a/uniffi_core/src/ffi_converter_impls.rs b/uniffi_core/src/ffi_converter_impls.rs index 44707399bf..af18f3873b 100644 --- a/uniffi_core/src/ffi_converter_impls.rs +++ b/uniffi_core/src/ffi_converter_impls.rs @@ -22,17 +22,18 @@ /// consumer crates. To do this, it defines blanket impls like `impl FFIConverter for u8`. /// "UT" means an abitrary `UniFfiTag` type. use crate::{ - check_remaining, ffi_converter_default_return, ffi_converter_rust_buffer_lift_and_lower, - lower_into_rust_buffer, metadata, try_lift_from_rust_buffer, FfiConverter, FutureCallback, - LiftRef, MetadataBuffer, Result, RustBuffer, RustCallStatus, UnexpectedUniFFICallbackError, + check_remaining, derive_ffi_traits, ffi_converter_rust_buffer_lift_and_lower, metadata, + ConvertError, FfiConverter, ForeignExecutor, Lift, LiftReturn, Lower, LowerReturn, + MetadataBuffer, Result, RustBuffer, UnexpectedUniFFICallbackError, }; use anyhow::bail; use bytes::buf::{Buf, BufMut}; use paste::paste; use std::{ collections::HashMap, - convert::{Infallible, TryFrom}, + convert::TryFrom, error::Error, + sync::Arc, time::{Duration, SystemTime}, }; @@ -44,8 +45,6 @@ macro_rules! impl_ffi_converter_for_num_primitive { ($T:ty, $type_code:expr) => { paste! { unsafe impl FfiConverter for $T { - ffi_converter_default_return!(UT); - type FfiType = $T; fn lower(obj: $T) -> Self::FfiType { @@ -87,8 +86,6 @@ impl_ffi_converter_for_num_primitive!(f64, metadata::codes::TYPE_F64); /// Booleans are passed as an `i8` in order to avoid problems with handling /// C-compatible boolean values on JVM-based languages. unsafe impl FfiConverter for bool { - ffi_converter_default_return!(UT); - type FfiType = i8; fn lower(obj: bool) -> Self::FfiType { @@ -115,68 +112,6 @@ unsafe impl FfiConverter for bool { const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_BOOL); } -/// Support unit-type returns via the FFI. -unsafe impl FfiConverter for () { - // This actually isn't used, but we need to specify something - type FfiType = (); - // Returning `()` is FFI-safe, since it gets translated into a void return - type ReturnType = (); - // However, we can't use `FutureCallback<()>` since passing `()` as an argument is not - // FFI-safe. So we used an arbitrary non-ZST type instead. - type FutureCallback = FutureCallback; - - fn try_lift(_: Self::FfiType) -> Result<()> { - Ok(()) - } - - fn lower(_: ()) -> Self::FfiType {} - - fn write(_: (), _: &mut Vec) {} - - fn try_read(_: &mut &[u8]) -> Result<()> { - Ok(()) - } - - fn lower_return(_: ()) -> Result { - Ok(()) - } - - fn invoke_future_callback( - callback: Self::FutureCallback, - callback_data: *const (), - _return_value: (), - call_status: RustCallStatus, - ) { - callback(callback_data, 0, call_status) - } - - const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_UNIT); -} - -unsafe impl FfiConverter for Infallible { - ffi_converter_default_return!(UT); - - type FfiType = RustBuffer; - - fn try_lift(_: Self::FfiType) -> Result { - unreachable!() - } - - fn lower(_: Infallible) -> Self::FfiType { - unreachable!() - } - - fn write(_: Infallible, _: &mut Vec) { - unreachable!() - } - - fn try_read(_: &mut &[u8]) -> Result { - unreachable!() - } - - const TYPE_ID_META: MetadataBuffer = MetadataBuffer::new(); -} - /// Support for passing Strings via the FFI. /// /// Unlike many other implementations of `FfiConverter`, this passes a struct containing @@ -189,8 +124,6 @@ unsafe impl FfiConverter for Infallible { /// followed by utf8-encoded bytes. (It's a signed integer because unsigned types are /// currently experimental in Kotlin). unsafe impl FfiConverter for String { - ffi_converter_default_return!(UT); - type FfiType = RustBuffer; // This returns a struct with a raw pointer to the underlying bytes, so it's very @@ -252,7 +185,6 @@ unsafe impl FfiConverter for String { /// if the total offset should be added to or subtracted from the unix epoch. unsafe impl FfiConverter for SystemTime { ffi_converter_rust_buffer_lift_and_lower!(UT); - ffi_converter_default_return!(UT); fn write(obj: SystemTime, buf: &mut Vec) { let mut sign = 1; @@ -298,7 +230,6 @@ unsafe impl FfiConverter for SystemTime { /// and 999,999,999. unsafe impl FfiConverter for Duration { ffi_converter_rust_buffer_lift_and_lower!(UT); - ffi_converter_default_return!(UT); fn write(obj: Duration, buf: &mut Vec) { buf.put_u64(obj.as_secs()); @@ -313,18 +244,18 @@ unsafe impl FfiConverter for Duration { const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_DURATION); } -/// Support for passing optional values via the FFI. -/// -/// Optional values are currently always passed by serializing to a buffer. -/// We write either a zero byte for `None`, or a one byte followed by the containing -/// item for `Some`. -/// -/// In future we could do the same optimization as rust uses internally, where the -/// `None` option is represented as a null pointer and the `Some` as a valid pointer, -/// but that seems more fiddly and less safe in the short term, so it can wait. -unsafe impl> FfiConverter for Option { - ffi_converter_rust_buffer_lift_and_lower!(UT); - ffi_converter_default_return!(UT); +// Support for passing optional values via the FFI. +// +// Optional values are currently always passed by serializing to a buffer. +// We write either a zero byte for `None`, or a one byte followed by the containing +// item for `Some`. +// +// In future we could do the same optimization as rust uses internally, where the +// `None` option is represented as a null pointer and the `Some` as a valid pointer, +// but that seems more fiddly and less safe in the short term, so it can wait. + +unsafe impl> Lower for Option { + type FfiType = RustBuffer; fn write(obj: Option, buf: &mut Vec) { match obj { @@ -336,6 +267,17 @@ unsafe impl> FfiConverter for Option { } } + fn lower(obj: Option) -> RustBuffer { + Self::lower_into_rust_buffer(obj) + } + + const TYPE_ID_META: MetadataBuffer = + MetadataBuffer::from_code(metadata::codes::TYPE_OPTION).concat(T::TYPE_ID_META); +} + +unsafe impl> Lift for Option { + type FfiType = RustBuffer; + fn try_read(buf: &mut &[u8]) -> Result> { check_remaining(buf, 1)?; Ok(match buf.get_i8() { @@ -345,95 +287,130 @@ unsafe impl> FfiConverter for Option { }) } + fn try_lift(buf: RustBuffer) -> Result> { + Self::try_lift_from_rust_buffer(buf) + } + const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_OPTION).concat(T::TYPE_ID_META); } -/// Support for passing vectors of values via the FFI. -/// -/// Vectors are currently always passed by serializing to a buffer. -/// We write a `i32` item count followed by each item in turn. -/// (It's a signed type due to limits of the JVM). -/// -/// Ideally we would pass `Vec` directly as a `RustBuffer` rather -/// than serializing, and perhaps even pass other vector types using a -/// similar struct. But that's for future work. -unsafe impl> FfiConverter for Vec { - ffi_converter_rust_buffer_lift_and_lower!(UT); - ffi_converter_default_return!(UT); +// Support for passing vectors of values via the FFI. +// +// Vectors are currently always passed by serializing to a buffer. +// We write a `i32` item count followed by each item in turn. +// (It's a signed type due to limits of the JVM). +// +// Ideally we would pass `Vec` directly as a `RustBuffer` rather +// than serializing, and perhaps even pass other vector types using a +// similar struct. But that's for future work. + +unsafe impl> Lower for Vec { + type FfiType = RustBuffer; fn write(obj: Vec, buf: &mut Vec) { // TODO: would be nice not to panic here :-/ let len = i32::try_from(obj.len()).unwrap(); buf.put_i32(len); // We limit arrays to i32::MAX items for item in obj { - >::write(item, buf); + >::write(item, buf); } } + fn lower(obj: Vec) -> RustBuffer { + Self::lower_into_rust_buffer(obj) + } + + const TYPE_ID_META: MetadataBuffer = + MetadataBuffer::from_code(metadata::codes::TYPE_VEC).concat(T::TYPE_ID_META); +} + +/// Support for associative arrays via the FFI - `record` in UDL. +/// HashMaps are currently always passed by serializing to a buffer. +/// We write a `i32` entries count followed by each entry (string +/// key followed by the value) in turn. +/// (It's a signed type due to limits of the JVM). +unsafe impl> Lift for Vec { + type FfiType = RustBuffer; + fn try_read(buf: &mut &[u8]) -> Result> { check_remaining(buf, 4)?; let len = usize::try_from(buf.get_i32())?; let mut vec = Vec::with_capacity(len); for _ in 0..len { - vec.push(>::try_read(buf)?) + vec.push(>::try_read(buf)?) } Ok(vec) } + fn try_lift(buf: RustBuffer) -> Result> { + Self::try_lift_from_rust_buffer(buf) + } + const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_VEC).concat(T::TYPE_ID_META); } -/// Support for associative arrays via the FFI. -/// Note that because of webidl limitations, -/// the key must always be of the String type. -/// -/// HashMaps are currently always passed by serializing to a buffer. -/// We write a `i32` entries count followed by each entry (string -/// key followed by the value) in turn. -/// (It's a signed type due to limits of the JVM). -unsafe impl FfiConverter for HashMap +unsafe impl Lower for HashMap where - K: FfiConverter + std::hash::Hash + Eq, - V: FfiConverter, + K: Lower + std::hash::Hash + Eq, + V: Lower, { - ffi_converter_rust_buffer_lift_and_lower!(UT); - ffi_converter_default_return!(UT); + type FfiType = RustBuffer; fn write(obj: HashMap, buf: &mut Vec) { // TODO: would be nice not to panic here :-/ let len = i32::try_from(obj.len()).unwrap(); buf.put_i32(len); // We limit HashMaps to i32::MAX entries for (key, value) in obj { - >::write(key, buf); - >::write(value, buf); + >::write(key, buf); + >::write(value, buf); } } + fn lower(obj: HashMap) -> RustBuffer { + Self::lower_into_rust_buffer(obj) + } + + const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_HASH_MAP) + .concat(K::TYPE_ID_META) + .concat(V::TYPE_ID_META); +} + +unsafe impl Lift for HashMap +where + K: Lift + std::hash::Hash + Eq, + V: Lift, +{ + type FfiType = RustBuffer; + fn try_read(buf: &mut &[u8]) -> Result> { check_remaining(buf, 4)?; let len = usize::try_from(buf.get_i32())?; let mut map = HashMap::with_capacity(len); for _ in 0..len { - let key = >::try_read(buf)?; - let value = >::try_read(buf)?; + let key = >::try_read(buf)?; + let value = >::try_read(buf)?; map.insert(key, value); } Ok(map) } + fn try_lift(buf: RustBuffer) -> Result> { + Self::try_lift_from_rust_buffer(buf) + } + const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_HASH_MAP) .concat(K::TYPE_ID_META) .concat(V::TYPE_ID_META); } -/// FFI support for ForeignSchedulers +/// FFI support for [ForeignExecutor] /// /// These are passed over the FFI as opaque pointer-sized types representing the foreign executor. /// The foreign bindings may use an actual pointer to the executor object, or a usized integer /// handle. -unsafe impl FfiConverter for crate::ForeignExecutor { +unsafe impl FfiConverter for ForeignExecutor { type FfiType = crate::ForeignExecutorHandle; // Passing these back to the foreign bindings is currently not supported @@ -452,7 +429,7 @@ unsafe impl FfiConverter for crate::ForeignExecutor { } fn try_lift(executor: Self::FfiType) -> Result { - Ok(crate::ForeignExecutor::new(executor)) + Ok(ForeignExecutor::new(executor)) } fn try_read(buf: &mut &[u8]) -> Result { @@ -465,114 +442,121 @@ unsafe impl FfiConverter for crate::ForeignExecutor { >::try_lift(crate::ForeignExecutorHandle(usize_val as *const ())) } - ffi_converter_default_return!(UT); - const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_FOREIGN_EXECUTOR); } -/// Support `Result<>` via the FFI. -/// -/// This is currently supported for function returns. Lifting/lowering Result<> arguments is not -/// implemented. -unsafe impl FfiConverter for Result -where - R: FfiConverter, - E: FfiConverter + Error + Send + Sync + 'static, -{ - type FfiType = (); // Placeholder while lower/lift/serializing is unimplemented - type ReturnType = R::ReturnType; - type FutureCallback = R::FutureCallback; +derive_ffi_traits!(blanket u8); +derive_ffi_traits!(blanket i8); +derive_ffi_traits!(blanket u16); +derive_ffi_traits!(blanket i16); +derive_ffi_traits!(blanket u32); +derive_ffi_traits!(blanket i32); +derive_ffi_traits!(blanket u64); +derive_ffi_traits!(blanket i64); +derive_ffi_traits!(blanket f32); +derive_ffi_traits!(blanket f64); +derive_ffi_traits!(blanket bool); +derive_ffi_traits!(blanket String); +derive_ffi_traits!(blanket Duration); +derive_ffi_traits!(blanket SystemTime); +derive_ffi_traits!(blanket ForeignExecutor); + +// For composite types, derive LowerReturn, LiftReturn, etc, from Lift/Lower. +// +// Note that this means we don't get specialized return handling. For example, if we could return +// an `Option>` we would always return that type directly and never throw. +derive_ffi_traits!(impl LowerReturn for Option where Option: Lower); +derive_ffi_traits!(impl LiftReturn for Option where Option: Lift); +derive_ffi_traits!(impl LiftRef for Option where Option: Lift); + +derive_ffi_traits!(impl LowerReturn for Vec where Vec: Lower); +derive_ffi_traits!(impl LiftReturn for Vec where Vec: Lift); +derive_ffi_traits!(impl LiftRef for Vec where Vec: Lift); + +derive_ffi_traits!(impl LowerReturn for HashMap where HashMap: Lower); +derive_ffi_traits!(impl LiftReturn for HashMap where HashMap: Lift); +derive_ffi_traits!(impl LiftRef for HashMap where HashMap: Lift); + +// For Arc we derive all the traits, but have to write it all out because we need an unsized T bound +derive_ffi_traits!(impl Lower for Arc where Arc: FfiConverter, T: ?Sized); +derive_ffi_traits!(impl Lift for Arc where Arc: FfiConverter, T: ?Sized); +derive_ffi_traits!(impl LowerReturn for Arc where Arc: Lower, T: ?Sized); +derive_ffi_traits!(impl LiftReturn for Arc where Arc: Lift, T: ?Sized); +derive_ffi_traits!(impl LiftRef for Arc where Arc: Lift, T: ?Sized); + +// Implement LowerReturn/LiftReturn for the unit type (void returns) + +unsafe impl LowerReturn for () { + type ReturnType = (); - fn try_lift(_: Self::FfiType) -> Result { - unimplemented!("try_lift"); + fn lower_return(_: ()) -> Result { + Ok(()) } - fn lower(_: Self) -> Self::FfiType { - unimplemented!("lower"); - } + const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_UNIT); +} - fn write(_: Self, _: &mut Vec) { - unimplemented!("write"); - } +unsafe impl LiftReturn for () { + fn lift_callback_return(_buf: RustBuffer) -> Self {} - fn try_read(_: &mut &[u8]) -> Result { - unimplemented!("try_read"); - } + const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_UNIT); +} + +// Implement LowerReturn/LiftReturn for `Result`. This is where we handle exceptions/Err +// results. + +unsafe impl LowerReturn for Result +where + R: LowerReturn, + E: Lower + Error + Send + Sync + 'static, +{ + type ReturnType = R::ReturnType; fn lower_return(v: Self) -> Result { match v { Ok(r) => R::lower_return(r), - Err(e) => Err(lower_into_rust_buffer(e)), + Err(e) => Err(E::lower_into_rust_buffer(e)), } } - fn handle_failed_lift(arg_name: &str, err: anyhow::Error) -> RustBuffer { + fn handle_failed_lift(arg_name: &str, err: anyhow::Error) -> Self { match err.downcast::() { - Ok(actual_error) => lower_into_rust_buffer(actual_error), + Ok(actual_error) => Err(actual_error), Err(ohno) => panic!("Failed to convert arg '{arg_name}': {ohno}"), } } + const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_RESULT) + .concat(R::TYPE_ID_META) + .concat(E::TYPE_ID_META); +} + +unsafe impl LiftReturn for Result +where + R: LiftReturn, + E: Lift + ConvertError, +{ fn lift_callback_return(buf: RustBuffer) -> Self { - Ok(try_lift_from_rust_buffer::(buf) - .expect("Error reading callback interface result")) + Ok(R::lift_callback_return(buf)) } fn lift_callback_error(buf: RustBuffer) -> Self { - Err(try_lift_from_rust_buffer::(buf) - .expect("Error reading callback interface Err result")) + match E::try_lift_from_rust_buffer(buf) { + Ok(lifted_error) => Err(lifted_error), + Err(anyhow_error) => { + Self::handle_callback_unexpected_error(UnexpectedUniFFICallbackError { + reason: format!("Error lifting from rust buffer: {anyhow_error}"), + }) + } + } } fn handle_callback_unexpected_error(e: UnexpectedUniFFICallbackError) -> Self { - Err(E::handle_callback_unexpected_error(e)) - } - - fn invoke_future_callback( - callback: Self::FutureCallback, - callback_data: *const (), - return_value: Self::ReturnType, - call_status: RustCallStatus, - ) { - R::invoke_future_callback(callback, callback_data, return_value, call_status) + Err(E::try_convert_unexpected_callback_error(e).unwrap_or_else(|e| panic!("{e}"))) } const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_RESULT) .concat(R::TYPE_ID_META) .concat(E::TYPE_ID_META); } - -macro_rules! simple_lift_ref_impl { - ($($t:ty),* $(,)?) => { - $( - impl LiftRef for $t { - type LiftType = $t; - } - )* - } -} - -simple_lift_ref_impl!( - u8, i8, u16, i16, u32, i32, u64, i64, f32, f64, bool, String, Duration, SystemTime -); - -impl LiftRef for Option -where - Option: FfiConverter, -{ - type LiftType = Self; -} - -impl LiftRef for Vec -where - Vec: FfiConverter, -{ - type LiftType = Self; -} - -impl LiftRef for HashMap -where - HashMap: FfiConverter, -{ - type LiftType = Self; -} diff --git a/uniffi_core/src/ffi_converter_traits.rs b/uniffi_core/src/ffi_converter_traits.rs index a08d249c85..3b5914e32f 100644 --- a/uniffi_core/src/ffi_converter_traits.rs +++ b/uniffi_core/src/ffi_converter_traits.rs @@ -2,54 +2,70 @@ * 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/. */ +//! Traits that define how to transfer values via the FFI layer. +//! +//! These traits define how to pass values over the FFI in various ways: as arguments or as return +//! values, from Rust to the foreign side and vice-versa. These traits are mainly used by the +//! proc-macro generated code. The goal is to allow the proc-macros to go from a type name to the +//! correct function for a given FFI operation. +//! +//! The traits form a sort-of tree structure from general to specific: +//! ```ignore +//! +//! [FfiConverter] +//! | +//! ----------------------------- +//! | | +//! [Lower] [Lift] +//! | | +//! | -------------- +//! | | | +//! [LowerReturn] [LiftRef] [LiftReturn] +//! ``` +//! +//! The `derive_ffi_traits` macro can be used to derive the specific traits from the general ones. +//! Here's the main ways we implement these traits: +//! +//! * For most types we implement [FfiConverter] and use [derive_ffi_traits] to implement the rest +//! * If a type can only be lifted/lowered, then we implement [Lift] or [Lower] and use +//! [derive_ffi_traits] to implement the rest +//! * If a type needs special-case handling, like `Result<>` and `()`, we implement the traits +//! directly. +//! +//! FfiConverter has a generic parameter, that's filled in with a type local to the UniFFI consumer crate. +//! This allows us to work around the Rust orphan rules for remote types. See +//! `https://mozilla.github.io/uniffi-rs/internals/lifting_and_lowering.html#code-generation-and-the-fficonverter-trait` +//! for details. +//! +//! ## Safety +//! +//! All traits are unsafe (implementing it requires `unsafe impl`) because we can't guarantee +//! that it's safe to pass your type out to foreign-language code and back again. Buggy +//! implementations of this trait might violate some assumptions made by the generated code, +//! or might not match with the corresponding code in the generated foreign-language bindings. +//! These traits should not be used directly, only in generated code, and the generated code should +//! have fixture tests to test that everything works correctly together. + use std::{borrow::Borrow, sync::Arc}; -use crate::{ - try_lift_from_rust_buffer, FfiDefault, MetadataBuffer, Result, RustBuffer, RustCallStatus, - UnexpectedUniFFICallbackError, -}; +use anyhow::bail; +use bytes::Buf; -/// Trait defining how to transfer values via the FFI layer. -/// -/// The `FfiConverter` trait defines how to pass values of a particular type back-and-forth over -/// the uniffi generated FFI layer, both as standalone argument or return values, and as -/// part of serialized compound data structures. `FfiConverter` is mainly used in generated code. -/// The goal is to make it easy for the code generator to name the correct FFI-related function for -/// a given type. -/// -/// FfiConverter has a generic parameter, that's filled in with a type local to the UniFFI consumer crate. -/// This allows us to work around the Rust orphan rules for remote types. See -/// `https://mozilla.github.io/uniffi-rs/internals/lifting_and_lowering.html#code-generation-and-the-fficonverter-trait` -/// for details. -/// -/// ## Scope -/// -/// It could be argued that FfiConverter handles too many concerns and should be split into -/// separate traits (like `FfiLiftAndLower`, `FfiSerialize`, `FfiReturn`). However, there are good -/// reasons to keep all the functionality in one trait. -/// -/// The main reason is that splitting the traits requires fairly complex blanket implementations, -/// for example `impl FfiReturn for T: FfiLiftAndLower`. Writing these impls often -/// means fighting with `rustc` over what counts as a conflicting implementation. In fact, as of -/// Rust 1.66, that previous example conflicts with `impl FfiReturn for ()`, since other -/// crates can implement `impl FfiReturn for ()`. In general, this path gets -/// complicated very quickly and that distracts from the logic that we want to define, which is -/// fairly simple. +use crate::{FfiDefault, MetadataBuffer, Result, RustBuffer, UnexpectedUniFFICallbackError}; + +/// Generalized FFI conversions /// -/// The main downside of having a single `FfiConverter` trait is that we need to implement it for -/// types that only support some of the functionality. For example, `Result<>` supports returning -/// values, but not lifting/lowering/serializing them. This is a bit weird, but works okay -- -/// especially since `FfiConverter` is rarely used outside of generated code. +/// This trait is not used directly by the code generation, but implement this and calling +/// [derive_ffi_traits] is a simple way to implement all the traits that are. /// /// ## Safety /// -/// This is an unsafe trait (implementing it requires `unsafe impl`) because we can't guarantee +/// All traits are unsafe (implementing it requires `unsafe impl`) because we can't guarantee /// that it's safe to pass your type out to foreign-language code and back again. Buggy /// implementations of this trait might violate some assumptions made by the generated code, /// or might not match with the corresponding code in the generated foreign-language bindings. -/// -/// In general, you should not need to implement this trait by hand, and should instead rely on -/// implementations generated from your component UDL via the `uniffi-bindgen scaffolding` command. +/// These traits should not be used directly, only in generated code, and the generated code should +/// have fixture tests to test that everything works correctly together. pub unsafe trait FfiConverter: Sized { /// The low-level type used for passing values of this type over the FFI. /// @@ -60,18 +76,9 @@ pub unsafe trait FfiConverter: Sized { /// the data for transfer. In theory it could be possible to build a matching /// `#[repr(C)]` struct for a complex data type and pass that instead, but explicit /// serialization is simpler and safer as a starting point. - type FfiType; - - /// The type that should be returned by scaffolding functions for this type. - /// - /// This is usually the same as `FfiType`, but `Result<>` has specialized handling. - type ReturnType: FfiDefault; - - /// The `FutureCallback` type used for async functions /// - /// This is almost always `FutureCallback`. The one exception is the - /// unit type, see that `FfiConverter` impl for details. - type FutureCallback: Copy; + /// If a type implements multiple FFI traits, `FfiType` must be the same for all of them. + type FfiType: FfiDefault; /// Lower a rust value of the target type, into an FFI value of type Self::FfiType. /// @@ -83,24 +90,6 @@ pub unsafe trait FfiConverter: Sized { /// the foreign language code, e.g. by boxing the value and passing a pointer. fn lower(obj: Self) -> Self::FfiType; - /// Lower this value for scaffolding function return - /// - /// This method converts values into the `Result<>` type that [rust_call] expects. For - /// successful calls, return `Ok(lower_return)`. For errors that should be translated into - /// thrown exceptions on the foreign code, serialize the error into a RustBuffer and return - /// `Err(buf)` - fn lower_return(obj: Self) -> Result; - - /// If possible, get a serialized error for failed argument lifts - /// - /// By default, we just panic and let `rust_call` handle things. However, for `Result<_, E>` - /// returns, if the anyhow error can be downcast to `E`, then serialize that and return it. - /// This results in the foreign code throwing a "normal" exception, rather than an unexpected - /// exception. - fn handle_failed_lift(arg_name: &str, e: anyhow::Error) -> RustBuffer { - panic!("Failed to convert arg '{arg_name}': {e}") - } - /// Lift a rust value of the target type, from an FFI value of type Self::FfiType. /// /// This trait method is used for receiving data from the foreign language code in rust, @@ -111,32 +100,6 @@ pub unsafe trait FfiConverter: Sized { /// values of type Self::FfiType, this method is fallible. fn try_lift(v: Self::FfiType) -> Result; - /// Lift a Rust value for a callback interface method result - fn lift_callback_return(buf: RustBuffer) -> Self { - try_lift_from_rust_buffer(buf).expect("Error reading callback interface result") - } - - /// Lift a Rust value for a callback interface method error result - /// - /// This is called for "expected errors" -- the callback method returns a Result<> type and the - /// foreign code throws an exception that corresponds to the error type. - fn lift_callback_error(_buf: RustBuffer) -> Self { - panic!("Callback interface method returned unexpected error") - } - - /// Lift a Rust value for an unexpected callback interface error - /// - /// The main reason this is called is when the callback interface throws an error type that - /// doesn't match the Rust trait definition. It's also called for corner cases, like when the - /// foreign code doesn't follow the FFI contract. - /// - /// The default implementation panics unconditionally. Errors used in callback interfaces - /// handle this using the `From` impl that the library author - /// must provide. - fn handle_callback_unexpected_error(e: UnexpectedUniFFICallbackError) -> Self { - panic!("Callback interface failure: {e}") - } - /// Write a rust value into a buffer, to send over the FFI in serialized form. /// /// This trait method can be used for sending data from rust to the foreign language code, @@ -161,15 +124,9 @@ pub unsafe trait FfiConverter: Sized { /// from it (but will not mutate the actual contents of the slice). fn try_read(buf: &mut &[u8]) -> Result; - /// Invoke a `FutureCallback` to complete a async call - fn invoke_future_callback( - callback: Self::FutureCallback, - callback_data: *const (), - return_value: Self::ReturnType, - call_status: RustCallStatus, - ); - - /// Type ID metadata, serialized into a [MetadataBuffer] + /// Type ID metadata, serialized into a [MetadataBuffer]. + /// + /// If a type implements multiple FFI traits, `TYPE_ID_META` must be the same for all of them. const TYPE_ID_META: MetadataBuffer; } @@ -183,32 +140,20 @@ pub unsafe trait FfiConverter: Sized { /// /// ## Safety /// -/// This has the same safety considerations as FfiConverter +/// All traits are unsafe (implementing it requires `unsafe impl`) because we can't guarantee +/// that it's safe to pass your type out to foreign-language code and back again. Buggy +/// implementations of this trait might violate some assumptions made by the generated code, +/// or might not match with the corresponding code in the generated foreign-language bindings. +/// These traits should not be used directly, only in generated code, and the generated code should +/// have fixture tests to test that everything works correctly together. pub unsafe trait FfiConverterArc: Send + Sync { - type FfiType; - type ReturnType: FfiDefault; - type FutureCallback: Copy; + type FfiType: FfiDefault; fn lower(obj: Arc) -> Self::FfiType; - fn lower_return(obj: Arc) -> Result; fn try_lift(v: Self::FfiType) -> Result>; - fn lift_callback_return(buf: RustBuffer) -> Arc { - try_lift_from_rust_buffer(buf).expect("Error reading callback interface result") - } - fn lift_callback_error(_buf: RustBuffer) -> Arc { - panic!("Callback interface method returned unexpected error") - } - fn handle_callback_unexpected_error(e: UnexpectedUniFFICallbackError) -> Arc { - panic!("Callback interface failure: {e}") - } fn write(obj: Arc, buf: &mut Vec); fn try_read(buf: &mut &[u8]) -> Result>; - fn invoke_future_callback( - callback: Self::FutureCallback, - callback_data: *const (), - return_value: Self::ReturnType, - call_status: RustCallStatus, - ); + const TYPE_ID_META: MetadataBuffer; } @@ -217,60 +162,305 @@ where T: FfiConverterArc + ?Sized, { type FfiType = T::FfiType; - type ReturnType = T::ReturnType; - type FutureCallback = T::FutureCallback; fn lower(obj: Self) -> Self::FfiType { T::lower(obj) } - fn lower_return(obj: Self) -> Result { - T::lower_return(obj) - } - fn try_lift(v: Self::FfiType) -> Result { T::try_lift(v) } - fn lift_callback_return(buf: RustBuffer) -> Self { - T::lift_callback_error(buf) + fn write(obj: Self, buf: &mut Vec) { + T::write(obj, buf) } - fn lift_callback_error(buf: RustBuffer) -> Self { - T::lift_callback_error(buf) + fn try_read(buf: &mut &[u8]) -> Result { + T::try_read(buf) } - fn handle_callback_unexpected_error(e: UnexpectedUniFFICallbackError) -> Self { - T::handle_callback_unexpected_error(e) + const TYPE_ID_META: MetadataBuffer = T::TYPE_ID_META; +} + +/// Lift values passed by the foreign code over the FFI into Rust values +/// +/// This is used by the code generation to handle arguments. It's usually derived from +/// [FfiConverter], except for types that only support lifting but not lowering. +/// +/// See [FfiConverter] for a discussion of the methods +/// +/// ## Safety +/// +/// All traits are unsafe (implementing it requires `unsafe impl`) because we can't guarantee +/// that it's safe to pass your type out to foreign-language code and back again. Buggy +/// implementations of this trait might violate some assumptions made by the generated code, +/// or might not match with the corresponding code in the generated foreign-language bindings. +/// These traits should not be used directly, only in generated code, and the generated code should +/// have fixture tests to test that everything works correctly together. +pub unsafe trait Lift: Sized { + type FfiType; + + fn try_lift(v: Self::FfiType) -> Result; + + fn try_read(buf: &mut &[u8]) -> Result; + + /// Convenience method + fn try_lift_from_rust_buffer(v: RustBuffer) -> Result { + let vec = v.destroy_into_vec(); + let mut buf = vec.as_slice(); + let value = Self::try_read(&mut buf)?; + match Buf::remaining(&buf) { + 0 => Ok(value), + n => bail!("junk data left in buffer after lifting (count: {n})",), + } } - fn write(obj: Self, buf: &mut Vec) { - T::write(obj, buf) + const TYPE_ID_META: MetadataBuffer; +} + +/// Lower Rust values to pass them to the foreign code +/// +/// This is used to pass arguments to callback interfaces. It's usually derived from +/// [FfiConverter], except for types that only support lowering but not lifting. +/// +/// See [FfiConverter] for a discussion of the methods +/// +/// ## Safety +/// +/// All traits are unsafe (implementing it requires `unsafe impl`) because we can't guarantee +/// that it's safe to pass your type out to foreign-language code and back again. Buggy +/// implementations of this trait might violate some assumptions made by the generated code, +/// or might not match with the corresponding code in the generated foreign-language bindings. +/// These traits should not be used directly, only in generated code, and the generated code should +/// have fixture tests to test that everything works correctly together. +pub unsafe trait Lower: Sized { + type FfiType: FfiDefault; + + fn lower(obj: Self) -> Self::FfiType; + + fn write(obj: Self, buf: &mut Vec); + + /// Convenience method + fn lower_into_rust_buffer(obj: Self) -> RustBuffer { + let mut buf = ::std::vec::Vec::new(); + Self::write(obj, &mut buf); + RustBuffer::from_vec(buf) } - fn try_read(buf: &mut &[u8]) -> Result { - T::try_read(buf) + const TYPE_ID_META: MetadataBuffer; +} + +/// Return Rust values to the foreign code +/// +/// This is usually derived from [Lift], but we special case types like `Result<>` and `()`. +/// +/// ## Safety +/// +/// All traits are unsafe (implementing it requires `unsafe impl`) because we can't guarantee +/// that it's safe to pass your type out to foreign-language code and back again. Buggy +/// implementations of this trait might violate some assumptions made by the generated code, +/// or might not match with the corresponding code in the generated foreign-language bindings. +/// These traits should not be used directly, only in generated code, and the generated code should +/// have fixture tests to test that everything works correctly together. +pub unsafe trait LowerReturn: Sized { + /// The type that should be returned by scaffolding functions for this type. + /// + /// When derived, it's the same as `FfiType`. + type ReturnType: FfiDefault; + + /// Lower this value for scaffolding function return + /// + /// This method converts values into the `Result<>` type that [rust_call] expects. For + /// successful calls, return `Ok(lower_return)`. For errors that should be translated into + /// thrown exceptions on the foreign code, serialize the error into a RustBuffer and return + /// `Err(buf)` + fn lower_return(obj: Self) -> Result; + + /// If possible, get a serialized error for failed argument lifts + /// + /// By default, we just panic and let `rust_call` handle things. However, for `Result<_, E>` + /// returns, if the anyhow error can be downcast to `E`, then serialize that and return it. + /// This results in the foreign code throwing a "normal" exception, rather than an unexpected + /// exception. + fn handle_failed_lift(arg_name: &str, e: anyhow::Error) -> Self { + panic!("Failed to convert arg '{arg_name}': {e}") } - fn invoke_future_callback( - callback: Self::FutureCallback, - callback_data: *const (), - return_value: Self::ReturnType, - call_status: RustCallStatus, - ) { - T::invoke_future_callback(callback, callback_data, return_value, call_status) + const TYPE_ID_META: MetadataBuffer; +} + +/// Return foreign values to Rust +/// +/// This is usually derived from [Lower], but we special case types like `Result<>` and `()`. +/// +/// ## Safety +/// +/// All traits are unsafe (implementing it requires `unsafe impl`) because we can't guarantee +/// that it's safe to pass your type out to foreign-language code and back again. Buggy +/// implementations of this trait might violate some assumptions made by the generated code, +/// or might not match with the corresponding code in the generated foreign-language bindings. +/// These traits should not be used directly, only in generated code, and the generated code should +/// have fixture tests to test that everything works correctly together. +pub unsafe trait LiftReturn: Sized { + /// Lift a Rust value for a callback interface method result + fn lift_callback_return(buf: RustBuffer) -> Self; + + /// Lift a Rust value for a callback interface method error result + /// + /// This is called for "expected errors" -- the callback method returns a Result<> type and the + /// foreign code throws an exception that corresponds to the error type. + fn lift_callback_error(_buf: RustBuffer) -> Self { + panic!("Callback interface method returned unexpected error") } - const TYPE_ID_META: MetadataBuffer = T::TYPE_ID_META; + /// Lift a Rust value for an unexpected callback interface error + /// + /// The main reason this is called is when the callback interface throws an error type that + /// doesn't match the Rust trait definition. It's also called for corner cases, like when the + /// foreign code doesn't follow the FFI contract. + /// + /// The default implementation panics unconditionally. Errors used in callback interfaces + /// handle this using the `From` impl that the library author + /// must provide. + fn handle_callback_unexpected_error(e: UnexpectedUniFFICallbackError) -> Self { + panic!("Callback interface failure: {e}") + } + + const TYPE_ID_META: MetadataBuffer; +} + +/// Lift references +/// +/// This is usually derived from [Lift] and also implemented for the inner `T` value of smart +/// pointers. For example, if `Lift` is implemented for `Arc`, then we implement this to lift +/// +/// ## Safety +/// +/// All traits are unsafe (implementing it requires `unsafe impl`) because we can't guarantee +/// that it's safe to pass your type out to foreign-language code and back again. Buggy +/// implementations of this trait might violate some assumptions made by the generated code, +/// or might not match with the corresponding code in the generated foreign-language bindings. +/// These traits should not be used directly, only in generated code, and the generated code should +/// have fixture tests to test that everything works correctly together. +/// `&T` using the Arc. +pub unsafe trait LiftRef { + type LiftType: Lift + Borrow; } -/// Trait used to lift references +pub trait ConvertError: Sized { + fn try_convert_unexpected_callback_error(e: UnexpectedUniFFICallbackError) -> Result; +} + +/// Derive FFI traits +/// +/// This can be used to derive: +/// * [Lower] and [Lift] from [FfiConverter] +/// * [LowerReturn] from [Lower] +/// * [LiftReturn] and [LiftRef] from [Lift] /// -/// This is needed because if we see a `&T` parameter, it's not clear which FfiConverter to use to -/// lift it. Normally it's just `T`, but for interfaces it's `Arc` and for callback interfaces -/// it's `Box`. +/// Usage: +/// ```ignore /// -/// This trait provides the proc-macros with a way to name the correct type. -pub trait LiftRef { - type LiftType: FfiConverter + Borrow; +/// // Derive everything from [FfiConverter] for all Uniffi tags +/// ::uniffi::derive_ffi_traits!(blanket Foo) +/// // Derive everything from [FfiConverter] for the local crate::UniFfiTag +/// ::uniffi::derive_ffi_traits!(local Foo) +/// // To derive a specific trait, write out the impl item minus the actual block +/// ::uniffi::derive_ffi_traits!(impl LowerReturn for Option) +/// ``` +#[macro_export] +#[allow(clippy::crate_in_macro_def)] +macro_rules! derive_ffi_traits { + (blanket $ty:ty) => { + $crate::derive_ffi_traits!(impl Lower for $ty); + $crate::derive_ffi_traits!(impl Lift for $ty); + $crate::derive_ffi_traits!(impl LowerReturn for $ty); + $crate::derive_ffi_traits!(impl LiftReturn for $ty); + $crate::derive_ffi_traits!(impl LiftRef for $ty); + $crate::derive_ffi_traits!(impl ConvertError for $ty); + }; + + (local $ty:ty) => { + $crate::derive_ffi_traits!(impl Lower for $ty); + $crate::derive_ffi_traits!(impl Lift for $ty); + $crate::derive_ffi_traits!(impl LowerReturn for $ty); + $crate::derive_ffi_traits!(impl LiftReturn for $ty); + $crate::derive_ffi_traits!(impl LiftRef for $ty); + $crate::derive_ffi_traits!(impl ConvertError for $ty); + }; + + (impl $(<$($generic:ident),*>)? $(::uniffi::)? Lower<$ut:path> for $ty:ty $(where $($where:tt)*)?) => { + unsafe impl $(<$($generic),*>)* $crate::Lower<$ut> for $ty $(where $($where)*)* + { + type FfiType = >::FfiType; + + fn lower(obj: Self) -> Self::FfiType { + >::lower(obj) + } + + fn write(obj: Self, buf: &mut ::std::vec::Vec) { + >::write(obj, buf) + } + + const TYPE_ID_META: $crate::MetadataBuffer = >::TYPE_ID_META; + } + }; + + (impl $(<$($generic:ident),*>)? $(::uniffi::)? Lift<$ut:path> for $ty:ty $(where $($where:tt)*)?) => { + unsafe impl $(<$($generic),*>)* $crate::Lift<$ut> for $ty $(where $($where)*)* + { + type FfiType = >::FfiType; + + fn try_lift(v: Self::FfiType) -> $crate::deps::anyhow::Result { + >::try_lift(v) + } + + fn try_read(buf: &mut &[u8]) -> $crate::deps::anyhow::Result { + >::try_read(buf) + } + + const TYPE_ID_META: $crate::MetadataBuffer = >::TYPE_ID_META; + } + }; + + (impl $(<$($generic:ident),*>)? $(::uniffi::)? LowerReturn<$ut:path> for $ty:ty $(where $($where:tt)*)?) => { + unsafe impl $(<$($generic),*>)* $crate::LowerReturn<$ut> for $ty $(where $($where)*)* + { + type ReturnType = >::FfiType; + + fn lower_return(obj: Self) -> $crate::deps::anyhow::Result { + Ok(>::lower(obj)) + } + + const TYPE_ID_META: $crate::MetadataBuffer =>::TYPE_ID_META; + } + }; + + (impl $(<$($generic:ident),*>)? $(::uniffi::)? LiftReturn<$ut:path> for $ty:ty $(where $($where:tt)*)?) => { + unsafe impl $(<$($generic),*>)* $crate::LiftReturn<$ut> for $ty $(where $($where)*)* + { + fn lift_callback_return(buf: $crate::RustBuffer) -> Self { + >::try_lift_from_rust_buffer(buf) + .expect("Error reading callback interface result") + } + + const TYPE_ID_META: $crate::MetadataBuffer = >::TYPE_ID_META; + } + }; + + (impl $(<$($generic:ident),*>)? $(::uniffi::)? LiftRef<$ut:path> for $ty:ty $(where $($where:tt)*)?) => { + unsafe impl $(<$($generic),*>)* $crate::LiftRef<$ut> for $ty $(where $($where)*)* + { + type LiftType = Self; + } + }; + + (impl $(<$($generic:ident),*>)? $(::uniffi::)? ConvertError<$ut:path> for $ty:ty $(where $($where:tt)*)?) => { + impl $(<$($generic),*>)* $crate::ConvertError<$ut> for $ty $(where $($where)*)* + { + fn try_convert_unexpected_callback_error(e: $crate::UnexpectedUniFFICallbackError) -> $crate::deps::anyhow::Result { + $crate::convert_unexpected_error!(e, $ty) + } + } + }; } diff --git a/uniffi_core/src/lib.rs b/uniffi_core/src/lib.rs index 7444ccca88..9003b08f9b 100644 --- a/uniffi_core/src/lib.rs +++ b/uniffi_core/src/lib.rs @@ -44,7 +44,9 @@ mod ffi_converter_traits; pub mod metadata; pub use ffi::*; -pub use ffi_converter_traits::{FfiConverter, FfiConverterArc, LiftRef}; +pub use ffi_converter_traits::{ + ConvertError, FfiConverter, FfiConverterArc, Lift, LiftRef, LiftReturn, Lower, LowerReturn, +}; pub use metadata::*; // Re-export the libs that we use in the generated code, @@ -147,60 +149,6 @@ pub fn check_remaining(buf: &[u8], num_bytes: usize) -> Result<()> { Ok(()) } -/// Helper function to lower an `anyhow::Error` that's wrapping an error type -pub fn lower_anyhow_error_or_panic(err: anyhow::Error, arg_name: &str) -> RustBuffer -where - E: 'static + FfiConverter + Sync + Send + std::fmt::Debug + std::fmt::Display, -{ - match err.downcast::() { - Ok(actual_error) => lower_into_rust_buffer(actual_error), - Err(ohno) => panic!("Failed to convert arg '{arg_name}': {ohno}"), - } -} - -/// Helper function to create a RustBuffer with a single value -pub fn lower_into_rust_buffer, UT>(obj: T) -> RustBuffer { - let mut buf = ::std::vec::Vec::new(); - T::write(obj, &mut buf); - RustBuffer::from_vec(buf) -} - -/// Helper function to deserialize a RustBuffer with a single value -pub fn try_lift_from_rust_buffer, UT>(v: RustBuffer) -> Result { - let vec = v.destroy_into_vec(); - let mut buf = vec.as_slice(); - let value = T::try_read(&mut buf)?; - match Buf::remaining(&buf) { - 0 => Ok(value), - n => bail!("junk data left in buffer after lifting (count: {n})",), - } -} - -/// Macro to implement returning values by simply lowering them and returning them -/// -/// This is what we use for all FfiConverters except for `Result`. This would be nicer as a -/// trait default, but Rust doesn't support defaults on associated types. -#[macro_export] -macro_rules! ffi_converter_default_return { - ($uniffi_tag:ty) => { - type ReturnType = >::FfiType; - type FutureCallback = $crate::FutureCallback; - - fn lower_return(v: Self) -> ::std::result::Result { - Ok(>::lower(v)) - } - - fn invoke_future_callback( - callback: Self::FutureCallback, - callback_data: *const (), - return_value: Self::ReturnType, - call_status: $crate::RustCallStatus, - ) { - callback(callback_data, return_value, call_status); - } - }; -} - /// Macro to implement lowering/lifting using a `RustBuffer` /// /// For complex types where it's too fiddly or too unsafe to convert them into a special-purpose @@ -214,11 +162,21 @@ macro_rules! ffi_converter_rust_buffer_lift_and_lower { type FfiType = $crate::RustBuffer; fn lower(v: Self) -> $crate::RustBuffer { - $crate::lower_into_rust_buffer::(v) + let mut buf = ::std::vec::Vec::new(); + >::write(v, &mut buf); + $crate::RustBuffer::from_vec(buf) } fn try_lift(buf: $crate::RustBuffer) -> $crate::Result { - $crate::try_lift_from_rust_buffer::(buf) + let vec = buf.destroy_into_vec(); + let mut buf = vec.as_slice(); + let value = >::try_read(&mut buf)?; + match $crate::deps::bytes::Buf::remaining(&buf) { + 0 => Ok(value), + n => $crate::deps::anyhow::bail!( + "junk data left in buffer after lifting (count: {n})", + ), + } } }; } @@ -232,11 +190,13 @@ macro_rules! ffi_converter_forward { ($T:ty, $existing_impl_tag:ty, $new_impl_tag:ty) => { ::uniffi::do_ffi_converter_forward!( FfiConverter, - Self, + $T, $T, $existing_impl_tag, $new_impl_tag ); + + $crate::derive_ffi_traits!(local $T); }; } @@ -248,11 +208,13 @@ macro_rules! ffi_converter_arc_forward { ($T:ty, $existing_impl_tag:ty, $new_impl_tag:ty) => { ::uniffi::do_ffi_converter_forward!( FfiConverterArc, - ::std::sync::Arc, + ::std::sync::Arc<$T>, $T, $existing_impl_tag, $new_impl_tag ); + + // Note: no need to call derive_ffi_traits! because there is a blanket impl for all Arc }; } @@ -263,19 +225,11 @@ macro_rules! do_ffi_converter_forward { ($trait:ident, $rust_type:ty, $T:ty, $existing_impl_tag:ty, $new_impl_tag:ty) => { unsafe impl $crate::$trait<$new_impl_tag> for $T { type FfiType = <$T as $crate::$trait<$existing_impl_tag>>::FfiType; - type ReturnType = <$T as $crate::$trait<$existing_impl_tag>>::FfiType; - type FutureCallback = <$T as $crate::$trait<$existing_impl_tag>>::FutureCallback; fn lower(obj: $rust_type) -> Self::FfiType { <$T as $crate::$trait<$existing_impl_tag>>::lower(obj) } - fn lower_return( - v: $rust_type, - ) -> ::std::result::Result { - <$T as $crate::$trait<$existing_impl_tag>>::lower_return(v) - } - fn try_lift(v: Self::FfiType) -> $crate::Result<$rust_type> { <$T as $crate::$trait<$existing_impl_tag>>::try_lift(v) } @@ -288,27 +242,9 @@ macro_rules! do_ffi_converter_forward { <$T as $crate::$trait<$existing_impl_tag>>::try_read(buf) } - fn invoke_future_callback( - callback: Self::FutureCallback, - callback_data: *const (), - return_value: Self::ReturnType, - call_status: $crate::RustCallStatus, - ) { - <$T as $crate::$trait<$existing_impl_tag>>::invoke_future_callback( - callback, - callback_data, - return_value, - call_status, - ) - } - const TYPE_ID_META: ::uniffi::MetadataBuffer = <$T as $crate::$trait<$existing_impl_tag>>::TYPE_ID_META; } - - impl $crate::LiftRef<$new_impl_tag> for $T { - type LiftType = <$T as $crate::LiftRef<$existing_impl_tag>>::LiftType; - } }; } @@ -353,9 +289,8 @@ pub mod test_util { pub struct TestError(pub String); // Use FfiConverter to simplify lifting TestError out of RustBuffer to check it - unsafe impl FfiConverter for TestError { + unsafe impl FfiConverter for TestError { ffi_converter_rust_buffer_lift_and_lower!(UniFfiTag); - ffi_converter_default_return!(UniFfiTag); fn write(obj: TestError, buf: &mut Vec) { >::write(obj.0, buf); @@ -382,4 +317,6 @@ pub mod test_util { Self(v.into()) } } + + derive_ffi_traits!(blanket TestError); } diff --git a/uniffi_core/src/metadata.rs b/uniffi_core/src/metadata.rs index e8af045a0c..770d2b36d5 100644 --- a/uniffi_core/src/metadata.rs +++ b/uniffi_core/src/metadata.rs @@ -38,6 +38,7 @@ pub mod codes { pub const UDL_FILE: u8 = 8; pub const CALLBACK_INTERFACE: u8 = 9; pub const TRAIT_METHOD: u8 = 10; + pub const UNIFFI_TRAIT: u8 = 11; pub const UNKNOWN: u8 = 255; // Type codes diff --git a/uniffi_macros/src/custom.rs b/uniffi_macros/src/custom.rs index d3274ca5ac..9d8e5acde6 100644 --- a/uniffi_macros/src/custom.rs +++ b/uniffi_macros/src/custom.rs @@ -2,7 +2,7 @@ * 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::util::{ident_to_string, mod_path, tagged_impl_header}; +use crate::util::{derive_all_ffi_traits, ident_to_string, mod_path, tagged_impl_header}; use proc_macro2::{Ident, TokenStream}; use quote::quote; use syn::Path; @@ -15,41 +15,40 @@ pub(crate) fn expand_ffi_converter_custom_type( udl_mode: bool, ) -> syn::Result { let impl_spec = tagged_impl_header("FfiConverter", ident, udl_mode); - let lift_ref_impl_spec = tagged_impl_header("LiftRef", ident, udl_mode); + let derive_ffi_traits = derive_all_ffi_traits(ident, udl_mode); let name = ident_to_string(ident); let mod_path = mod_path()?; Ok(quote! { #[automatically_derived] unsafe #impl_spec { - type FfiType = <#builtin as ::uniffi::FfiConverter>::FfiType; + // Note: the builtin type needs to implement both `Lower` and `Lift'. We use the + // `Lower` trait to get the associated type `FfiType` and const `TYPE_ID_META`. These + // can't differ between `Lower` and `Lift`. + type FfiType = <#builtin as ::uniffi::Lower>::FfiType; fn lower(obj: #ident ) -> Self::FfiType { - <#builtin as ::uniffi::FfiConverter>::lower(<#ident as crate::UniffiCustomTypeConverter>::from_custom(obj)) + <#builtin as ::uniffi::Lower>::lower(<#ident as crate::UniffiCustomTypeConverter>::from_custom(obj)) } fn try_lift(v: Self::FfiType) -> uniffi::Result<#ident> { - <#ident as crate::UniffiCustomTypeConverter>::into_custom(<#builtin as ::uniffi::FfiConverter>::try_lift(v)?) + <#ident as crate::UniffiCustomTypeConverter>::into_custom(<#builtin as ::uniffi::Lift>::try_lift(v)?) } fn write(obj: #ident, buf: &mut Vec) { - <#builtin as ::uniffi::FfiConverter>::write(<#ident as crate::UniffiCustomTypeConverter>::from_custom(obj), buf); + <#builtin as ::uniffi::Lower>::write(<#ident as crate::UniffiCustomTypeConverter>::from_custom(obj), buf); } fn try_read(buf: &mut &[u8]) -> uniffi::Result<#ident> { - <#ident as crate::UniffiCustomTypeConverter>::into_custom(<#builtin as ::uniffi::FfiConverter>::try_read(buf)?) + <#ident as crate::UniffiCustomTypeConverter>::into_custom(<#builtin as ::uniffi::Lift>::try_read(buf)?) } - ::uniffi::ffi_converter_default_return!(crate::UniFfiTag); - const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::TYPE_CUSTOM) .concat_str(#mod_path) .concat_str(#name) - .concat(<#builtin as ::uniffi::FfiConverter>::TYPE_ID_META); + .concat(<#builtin as ::uniffi::Lower>::TYPE_ID_META); } - #lift_ref_impl_spec { - type LiftType = Self; - } + #derive_ffi_traits }) } diff --git a/uniffi_macros/src/enum_.rs b/uniffi_macros/src/enum_.rs index 9034a9540a..32abfa08cc 100644 --- a/uniffi_macros/src/enum_.rs +++ b/uniffi_macros/src/enum_.rs @@ -3,7 +3,7 @@ use quote::quote; use syn::{Data, DataEnum, DeriveInput, Field, Index}; use crate::util::{ - create_metadata_items, ident_to_string, mod_path, tagged_impl_header, + create_metadata_items, derive_all_ffi_traits, ident_to_string, mod_path, tagged_impl_header, try_metadata_value_from_usize, try_read_field, }; @@ -39,7 +39,6 @@ pub(crate) fn enum_ffi_converter_impl( ident, enum_, udl_mode, - false, quote! { ::uniffi::metadata::codes::TYPE_ENUM }, ) } @@ -48,13 +47,11 @@ pub(crate) fn rich_error_ffi_converter_impl( ident: &Ident, enum_: &DataEnum, udl_mode: bool, - handle_unknown_callback_error: bool, ) -> TokenStream { enum_or_error_ffi_converter_impl( ident, enum_, udl_mode, - handle_unknown_callback_error, quote! { ::uniffi::metadata::codes::TYPE_ENUM }, ) } @@ -63,12 +60,11 @@ fn enum_or_error_ffi_converter_impl( ident: &Ident, enum_: &DataEnum, udl_mode: bool, - handle_unknown_callback_error: bool, metadata_type_code: TokenStream, ) -> TokenStream { let name = ident_to_string(ident); let impl_spec = tagged_impl_header("FfiConverter", ident, udl_mode); - let lift_ref_impl_spec = tagged_impl_header("LiftRef", ident, udl_mode); + let derive_ffi_traits = derive_all_ffi_traits(ident, udl_mode); let mod_path = match mod_path() { Ok(p) => p, Err(e) => return e.into_compile_error(), @@ -109,14 +105,10 @@ fn enum_or_error_ffi_converter_impl( }) }; - let handle_callback_unexpected_error = - handle_callback_unexpected_error_fn(handle_unknown_callback_error); - quote! { #[automatically_derived] unsafe #impl_spec { ::uniffi::ffi_converter_rust_buffer_lift_and_lower!(crate::UniFfiTag); - ::uniffi::ffi_converter_default_return!(crate::UniFfiTag); fn write(obj: Self, buf: &mut ::std::vec::Vec) { #write_impl @@ -126,16 +118,12 @@ fn enum_or_error_ffi_converter_impl( #try_read_impl } - #handle_callback_unexpected_error - const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code(#metadata_type_code) .concat_str(#mod_path) .concat_str(#name); } - #lift_ref_impl_spec { - type LiftType = Self; - } + #derive_ffi_traits } } @@ -144,7 +132,7 @@ fn write_field(f: &Field) -> TokenStream { let ty = &f.ty; quote! { - <#ty as ::uniffi::FfiConverter>::write(#ident, buf); + <#ty as ::uniffi::Lower>::write(#ident, buf); } } @@ -196,7 +184,7 @@ pub fn variant_metadata(enum_: &DataEnum) -> syn::Result> { .concat_value(#fields_len) #( .concat_str(#field_names) - .concat(<#field_types as ::uniffi::FfiConverter>::TYPE_ID_META) + .concat(<#field_types as ::uniffi::Lower>::TYPE_ID_META) // field defaults not yet supported for enums .concat_bool(false) )* @@ -205,20 +193,3 @@ pub fn variant_metadata(enum_: &DataEnum) -> syn::Result> { ) .collect() } - -/// Generate the `handle_callback_unexpected_error()` implementation -/// -/// If handle_unknown_callback_error is true, this will use the `From` -/// implementation that the library author must provide. -/// -/// If handle_unknown_callback_error is false, then we won't generate any code, falling back to the default -/// implementation which panics. -pub(crate) fn handle_callback_unexpected_error_fn( - handle_unknown_callback_error: bool, -) -> Option { - handle_unknown_callback_error.then(|| quote! { - fn handle_callback_unexpected_error(e: ::uniffi::UnexpectedUniFFICallbackError) -> Self { - >::from(e) - } - }) -} diff --git a/uniffi_macros/src/error.rs b/uniffi_macros/src/error.rs index a60db41753..cb25df5d68 100644 --- a/uniffi_macros/src/error.rs +++ b/uniffi_macros/src/error.rs @@ -6,10 +6,10 @@ use syn::{ }; use crate::{ - enum_::{handle_callback_unexpected_error_fn, rich_error_ffi_converter_impl, variant_metadata}, + enum_::{rich_error_ffi_converter_impl, variant_metadata}, util::{ - chain, create_metadata_items, either_attribute_arg, ident_to_string, kw, mod_path, - parse_comma_separated, tagged_impl_header, try_metadata_value_from_usize, + chain, create_metadata_items, derive_ffi_traits, either_attribute_arg, ident_to_string, kw, + mod_path, parse_comma_separated, tagged_impl_header, try_metadata_value_from_usize, AttributeSliceExt, UniffiAttributeArgs, }, }; @@ -69,20 +69,9 @@ fn error_ffi_converter_impl( udl_mode: bool, ) -> TokenStream { if attr.flat.is_some() { - flat_error_ffi_converter_impl( - ident, - enum_, - udl_mode, - attr.with_try_read.is_some(), - attr.handle_unknown_callback_error.is_some(), - ) + flat_error_ffi_converter_impl(ident, enum_, udl_mode, attr.with_try_read.is_some()) } else { - rich_error_ffi_converter_impl( - ident, - enum_, - udl_mode, - attr.handle_unknown_callback_error.is_some(), - ) + rich_error_ffi_converter_impl(ident, enum_, udl_mode) } } @@ -94,18 +83,17 @@ fn flat_error_ffi_converter_impl( ident: &Ident, enum_: &DataEnum, udl_mode: bool, - implement_try_read: bool, - handle_unknown_callback_error: bool, + implement_lift: bool, ) -> TokenStream { let name = ident_to_string(ident); - let impl_spec = tagged_impl_header("FfiConverter", ident, udl_mode); - let lift_ref_impl_spec = tagged_impl_header("LiftRef", ident, udl_mode); + let lower_impl_spec = tagged_impl_header("Lower", ident, udl_mode); + let derive_ffi_traits = derive_ffi_traits(ident, udl_mode, &["ConvertError"]); let mod_path = match mod_path() { Ok(p) => p, Err(e) => return e.into_compile_error(), }; - let write_impl = { + let lower_impl = { let match_arms = enum_.variants.iter().enumerate().map(|(i, v)| { let v_ident = &v.ident; let idx = Index::from(i + 1); @@ -113,18 +101,34 @@ fn flat_error_ffi_converter_impl( quote! { Self::#v_ident { .. } => { ::uniffi::deps::bytes::BufMut::put_i32(buf, #idx); - <::std::string::String as ::uniffi::FfiConverter>::write(error_msg, buf); + <::std::string::String as ::uniffi::Lower>::write(error_msg, buf); } } }); quote! { - let error_msg = ::std::string::ToString::to_string(&obj); - match obj { #(#match_arms)* } + #[automatically_derived] + unsafe #lower_impl_spec { + type FfiType = ::uniffi::RustBuffer; + + fn write(obj: Self, buf: &mut ::std::vec::Vec) { + let error_msg = ::std::string::ToString::to_string(&obj); + match obj { #(#match_arms)* } + } + + fn lower(obj: Self) -> ::uniffi::RustBuffer { + >::lower_into_rust_buffer(obj) + } + + const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::TYPE_ENUM) + .concat_str(#mod_path) + .concat_str(#name); + } } }; - let try_read_impl = if implement_try_read { + let lift_impl = implement_lift.then(|| { + let lift_impl_spec = tagged_impl_header("Lift", ident, udl_mode); let match_arms = enum_.variants.iter().enumerate().map(|(i, v)| { let v_ident = &v.ident; let idx = Index::from(i + 1); @@ -134,42 +138,31 @@ fn flat_error_ffi_converter_impl( } }); quote! { - Ok(match ::uniffi::deps::bytes::Buf::get_i32(buf) { - #(#match_arms)* - v => ::uniffi::deps::anyhow::bail!("Invalid #ident enum value: {}", v), - }) - } - } else { - quote! { ::std::panic!("try_read not supported for flat errors") } - }; - - let handle_callback_unexpected_error = - handle_callback_unexpected_error_fn(handle_unknown_callback_error); - - quote! { - #[automatically_derived] - unsafe #impl_spec { - ::uniffi::ffi_converter_rust_buffer_lift_and_lower!(crate::UniFfiTag); - ::uniffi::ffi_converter_default_return!(crate::UniFfiTag); + #[automatically_derived] + unsafe #lift_impl_spec { + type FfiType = ::uniffi::RustBuffer; + + fn try_read(buf: &mut &[::std::primitive::u8]) -> ::uniffi::deps::anyhow::Result { + Ok(match ::uniffi::deps::bytes::Buf::get_i32(buf) { + #(#match_arms)* + v => ::uniffi::deps::anyhow::bail!("Invalid #ident enum value: {}", v), + }) + } - fn write(obj: Self, buf: &mut ::std::vec::Vec) { - #write_impl - } + fn try_lift(v: ::uniffi::RustBuffer) -> ::uniffi::deps::anyhow::Result { + >::try_lift_from_rust_buffer(v) + } - fn try_read(buf: &mut &[::std::primitive::u8]) -> ::uniffi::deps::anyhow::Result { - #try_read_impl + const TYPE_ID_META: ::uniffi::MetadataBuffer = >::TYPE_ID_META; } - #handle_callback_unexpected_error - - const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::TYPE_ENUM) - .concat_str(#mod_path) - .concat_str(#name); } + }); - #lift_ref_impl_spec { - type LiftType = Self; - } + quote! { + #lower_impl + #lift_impl + #derive_ffi_traits } } @@ -211,8 +204,6 @@ pub fn flat_error_variant_metadata(enum_: &DataEnum) -> syn::Result, with_try_read: Option, - /// Can this error be used in a callback interface? - handle_unknown_callback_error: Option, } impl UniffiAttributeArgs for ErrorAttr { @@ -229,10 +220,8 @@ impl UniffiAttributeArgs for ErrorAttr { ..Self::default() }) } else if lookahead.peek(kw::handle_unknown_callback_error) { - Ok(Self { - handle_unknown_callback_error: input.parse()?, - ..Self::default() - }) + // Not used anymore, but still lallowed + Ok(Self::default()) } else { Err(lookahead.error()) } @@ -242,10 +231,6 @@ impl UniffiAttributeArgs for ErrorAttr { Ok(Self { flat: either_attribute_arg(self.flat, other.flat)?, with_try_read: either_attribute_arg(self.with_try_read, other.with_try_read)?, - handle_unknown_callback_error: either_attribute_arg( - self.handle_unknown_callback_error, - other.handle_unknown_callback_error, - )?, }) } } diff --git a/uniffi_macros/src/export.rs b/uniffi_macros/src/export.rs index 9a77beb700..7d3f54da93 100644 --- a/uniffi_macros/src/export.rs +++ b/uniffi_macros/src/export.rs @@ -2,7 +2,7 @@ * 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 proc_macro2::{Ident, Span, TokenStream}; +use proc_macro2::TokenStream; use quote::{quote, quote_spanned}; use syn::{visit_mut::VisitMut, Item, Type}; @@ -10,18 +10,18 @@ mod attributes; mod callback_interface; mod item; mod scaffolding; +mod trait_interface; +mod utrait; use self::{ item::{ExportItem, ImplItem}, - scaffolding::{gen_constructor_scaffolding, gen_fn_scaffolding, gen_method_scaffolding}, -}; -use crate::{ - object::interface_meta_static_var, - util::{ident_to_string, mod_path, tagged_impl_header}, + scaffolding::{ + gen_constructor_scaffolding, gen_ffi_function, gen_fn_scaffolding, gen_method_scaffolding, + }, }; +use crate::util::{ident_to_string, mod_path}; pub use attributes::ExportAttributeArguments; pub use callback_interface::ffi_converter_callback_interface_impl; -use uniffi_meta::free_fn_symbol_name; // TODO(jplatte): Ensure no generics, … // TODO(jplatte): Aggregate errors instead of short-circuiting, wherever possible @@ -68,177 +68,35 @@ pub(crate) fn expand_export( items, self_ident, callback_interface: false, - } => { - if let Some(rt) = args.async_runtime { - return Err(syn::Error::new_spanned(rt, "not supported for traits")); - } - - let name = ident_to_string(&self_ident); - let free_fn_ident = - Ident::new(&free_fn_symbol_name(&mod_path, &name), Span::call_site()); - - let free_tokens = quote! { - #[doc(hidden)] - #[no_mangle] - pub extern "C" fn #free_fn_ident( - ptr: *const ::std::ffi::c_void, - call_status: &mut ::uniffi::RustCallStatus - ) { - uniffi::rust_call(call_status, || { - assert!(!ptr.is_null()); - drop(unsafe { ::std::boxed::Box::from_raw(ptr as *mut std::sync::Arc) }); - Ok(()) - }); - } - }; - - let impl_tokens: TokenStream = items - .into_iter() - .map(|item| match item { - ImplItem::Method(sig) => { - if sig.is_async { - return Err(syn::Error::new( - sig.span, - "async trait methods are not supported", - )); - } - gen_method_scaffolding(sig, &args, udl_mode) - } - _ => unreachable!("traits have no constructors"), - }) - .collect::>()?; - - let meta_static_var = (!udl_mode).then(|| { - interface_meta_static_var(&self_ident, true, &mod_path) - .unwrap_or_else(syn::Error::into_compile_error) - }); - let ffi_converter_tokens = ffi_converter_trait_impl(&self_ident, false); - - Ok(quote_spanned! { self_ident.span() => - #meta_static_var - #free_tokens - #ffi_converter_tokens - #impl_tokens - }) - } + } => trait_interface::gen_trait_scaffolding(&mod_path, args, self_ident, items, udl_mode), ExportItem::Trait { items, self_ident, callback_interface: true, } => { let trait_name = ident_to_string(&self_ident); - let trait_impl_ident = Ident::new( - &format!("UniFFICallbackHandler{trait_name}"), - Span::call_site(), - ); - let internals_ident = Ident::new( - &format!( - "UNIFFI_FOREIGN_CALLBACK_INTERNALS_{}", - trait_name.to_ascii_uppercase() - ), - Span::call_site(), - ); - - let trait_impl = callback_interface::trait_impl( - &trait_impl_ident, - &self_ident, - &internals_ident, - &items, - ) - .unwrap_or_else(|e| e.into_compile_error()); + let trait_impl_ident = callback_interface::trait_impl_ident(&trait_name); + let trait_impl = callback_interface::trait_impl(&mod_path, &self_ident, &items) + .unwrap_or_else(|e| e.into_compile_error()); let metadata_items = callback_interface::metadata_items(&self_ident, &items, &mod_path) .unwrap_or_else(|e| vec![e.into_compile_error()]); - - let init_ident = Ident::new( - &uniffi_meta::init_callback_fn_symbol_name(&mod_path, &trait_name), - Span::call_site(), - ); + let ffi_converter_tokens = + ffi_converter_callback_interface_impl(&self_ident, &trait_impl_ident, udl_mode); Ok(quote! { - #[doc(hidden)] - static #internals_ident: ::uniffi::ForeignCallbackInternals = ::uniffi::ForeignCallbackInternals::new(); - - #[doc(hidden)] - #[no_mangle] - pub extern "C" fn #init_ident(callback: ::uniffi::ForeignCallback, _: &mut ::uniffi::RustCallStatus) { - #internals_ident.set_callback(callback); - } - #trait_impl + #ffi_converter_tokens + #(#metadata_items)* }) } - } -} - -pub(crate) fn ffi_converter_trait_impl(trait_ident: &Ident, udl_mode: bool) -> TokenStream { - let impl_spec = tagged_impl_header("FfiConverterArc", "e! { dyn #trait_ident }, udl_mode); - let lift_ref_impl_spec = tagged_impl_header("LiftRef", "e! { dyn #trait_ident }, udl_mode); - let name = ident_to_string(trait_ident); - let mod_path = match mod_path() { - Ok(p) => p, - Err(e) => return e.into_compile_error(), - }; - - quote! { - // All traits must be `Sync + Send`. The generated scaffolding will fail to compile - // if they are not, but unfortunately it fails with an unactionably obscure error message. - // By asserting the requirement explicitly, we help Rust produce a more scrutable error message - // and thus help the user debug why the requirement isn't being met. - uniffi::deps::static_assertions::assert_impl_all!(dyn #trait_ident: Sync, Send); - - unsafe #impl_spec { - type FfiType = *const ::std::os::raw::c_void; - type ReturnType = Self::FfiType; - type FutureCallback = ::uniffi::FutureCallback; - - fn lower(obj: ::std::sync::Arc) -> Self::FfiType { - ::std::boxed::Box::into_raw(::std::boxed::Box::new(obj)) as *const ::std::os::raw::c_void - } - - fn try_lift(v: Self::FfiType) -> ::uniffi::Result<::std::sync::Arc> { - let foreign_arc = ::std::boxed::Box::leak(unsafe { Box::from_raw(v as *mut ::std::sync::Arc) }); - // Take a clone for our own use. - Ok(::std::sync::Arc::clone(foreign_arc)) - } - - fn write(obj: ::std::sync::Arc, buf: &mut Vec) { - ::uniffi::deps::static_assertions::const_assert!(::std::mem::size_of::<*const ::std::ffi::c_void>() <= 8); - ::uniffi::deps::bytes::BufMut::put_u64( - buf, - >::lower(obj) as u64, - ); - } - - fn try_read(buf: &mut &[u8]) -> ::uniffi::Result<::std::sync::Arc> { - ::uniffi::deps::static_assertions::const_assert!(::std::mem::size_of::<*const ::std::ffi::c_void>() <= 8); - ::uniffi::check_remaining(buf, 8)?; - >::try_lift( - ::uniffi::deps::bytes::Buf::get_u64(buf) as Self::FfiType) - } - - fn lower_return(v: ::std::sync::Arc) -> ::std::result::Result { - Ok(>::lower(v)) - } - - fn invoke_future_callback( - callback: Self::FutureCallback, - callback_data: *const (), - return_value: Self::ReturnType, - call_status: ::uniffi::RustCallStatus, - ) { - callback(callback_data, return_value, call_status); - } - - const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::TYPE_INTERFACE) - .concat_str(#mod_path) - .concat_str(#name) - .concat_bool(true); - } - - #lift_ref_impl_spec { - type LiftType = ::std::sync::Arc; + ExportItem::Struct { + self_ident, + uniffi_traits, + } => { + assert!(!udl_mode); + utrait::expand_uniffi_trait_export(self_ident, uniffi_traits) } } } diff --git a/uniffi_macros/src/export/attributes.rs b/uniffi_macros/src/export/attributes.rs index 92b79cfe3b..c3edcd5920 100644 --- a/uniffi_macros/src/export/attributes.rs +++ b/uniffi_macros/src/export/attributes.rs @@ -12,6 +12,11 @@ pub struct ExportAttributeArguments { pub(crate) async_runtime: Option, pub(crate) callback_interface: Option, pub(crate) constructor: Option, + // tried to make this a vec but that got messy quickly... + pub(crate) trait_debug: Option, + pub(crate) trait_display: Option, + pub(crate) trait_hash: Option, + pub(crate) trait_eq: Option, } impl Parse for ExportAttributeArguments { @@ -40,6 +45,26 @@ impl UniffiAttributeArgs for ExportAttributeArguments { constructor: input.parse()?, ..Self::default() }) + } else if lookahead.peek(kw::Debug) { + Ok(Self { + trait_debug: input.parse()?, + ..Self::default() + }) + } else if lookahead.peek(kw::Display) { + Ok(Self { + trait_display: input.parse()?, + ..Self::default() + }) + } else if lookahead.peek(kw::Hash) { + Ok(Self { + trait_hash: input.parse()?, + ..Self::default() + }) + } else if lookahead.peek(kw::Eq) { + Ok(Self { + trait_eq: input.parse()?, + ..Self::default() + }) } else { Ok(Self::default()) } @@ -53,6 +78,10 @@ impl UniffiAttributeArgs for ExportAttributeArguments { other.callback_interface, )?, constructor: either_attribute_arg(self.constructor, other.constructor)?, + trait_debug: either_attribute_arg(self.trait_debug, other.trait_debug)?, + trait_display: either_attribute_arg(self.trait_display, other.trait_display)?, + trait_hash: either_attribute_arg(self.trait_hash, other.trait_hash)?, + trait_eq: either_attribute_arg(self.trait_eq, other.trait_eq)?, }) } } diff --git a/uniffi_macros/src/export/callback_interface.rs b/uniffi_macros/src/export/callback_interface.rs index 4c8fc360f3..81258f0bb7 100644 --- a/uniffi_macros/src/export/callback_interface.rs +++ b/uniffi_macros/src/export/callback_interface.rs @@ -13,34 +13,48 @@ use std::iter; use syn::Ident; pub(super) fn trait_impl( - ident: &Ident, + mod_path: &str, trait_ident: &Ident, - internals_ident: &Ident, items: &[ImplItem], ) -> syn::Result { + let trait_name = ident_to_string(trait_ident); + let trait_impl_ident = trait_impl_ident(&trait_name); + let internals_ident = internals_ident(&trait_name); + let init_ident = Ident::new( + &uniffi_meta::init_callback_fn_symbol_name(mod_path, &trait_name), + Span::call_site(), + ); + let trait_impl_methods = items .iter() .map(|item| match item { - ImplItem::Method(sig) => gen_method_impl(sig, internals_ident), + ImplItem::Method(sig) => gen_method_impl(sig, &internals_ident), _ => unreachable!("traits have no constructors"), }) .collect::>()?; - let ffi_converter_tokens = ffi_converter_callback_interface_impl(trait_ident, ident, false); - Ok(quote! { + #[doc(hidden)] + static #internals_ident: ::uniffi::ForeignCallbackInternals = ::uniffi::ForeignCallbackInternals::new(); + + #[doc(hidden)] + #[no_mangle] + pub extern "C" fn #init_ident(callback: ::uniffi::ForeignCallback) { + #internals_ident.set_callback(callback); + } + #[doc(hidden)] #[derive(Debug)] - struct #ident { + struct #trait_impl_ident { handle: u64, } - impl #ident { + impl #trait_impl_ident { fn new(handle: u64) -> Self { Self { handle } } } - impl ::std::ops::Drop for #ident { + impl ::std::ops::Drop for #trait_impl_ident { fn drop(&mut self) { #internals_ident.invoke_callback::<(), crate::UniFfiTag>( self.handle, uniffi::IDX_CALLBACK_FREE, Default::default() @@ -48,31 +62,41 @@ pub(super) fn trait_impl( } } - ::uniffi::deps::static_assertions::assert_impl_all!(#ident: Send); + ::uniffi::deps::static_assertions::assert_impl_all!(#trait_impl_ident: Send); - impl #trait_ident for #ident { + impl #trait_ident for #trait_impl_ident { #trait_impl_methods } - - #ffi_converter_tokens }) } +pub fn trait_impl_ident(trait_name: &str) -> Ident { + Ident::new( + &format!("UniFFICallbackHandler{trait_name}"), + Span::call_site(), + ) +} + +pub fn internals_ident(trait_name: &str) -> Ident { + Ident::new( + &format!( + "UNIFFI_FOREIGN_CALLBACK_INTERNALS_{}", + trait_name.to_ascii_uppercase() + ), + Span::call_site(), + ) +} + pub fn ffi_converter_callback_interface_impl( trait_ident: &Ident, trait_impl_ident: &Ident, udl_mode: bool, ) -> TokenStream { - let name = ident_to_string(trait_ident); + let trait_name = ident_to_string(trait_ident); let dyn_trait = quote! { dyn #trait_ident }; let box_dyn_trait = quote! { ::std::boxed::Box<#dyn_trait> }; - let impl_spec = tagged_impl_header("FfiConverter", &box_dyn_trait, udl_mode); + let lift_impl_spec = tagged_impl_header("Lift", &box_dyn_trait, udl_mode); let lift_ref_impl_spec = tagged_impl_header("LiftRef", &dyn_trait, udl_mode); - let tag = if udl_mode { - quote! { crate::UniFfiTag } - } else { - quote! { T } - }; let mod_path = match mod_path() { Ok(p) => p, Err(e) => return e.into_compile_error(), @@ -81,46 +105,27 @@ pub fn ffi_converter_callback_interface_impl( quote! { #[doc(hidden)] #[automatically_derived] - unsafe #impl_spec { + unsafe #lift_impl_spec { type FfiType = u64; - // Lower and write are tricky to implement because we have a dyn trait as our type. There's - // probably a way to, but this carries lots of thread safety risks, down to impedance - // mismatches between Rust and foreign languages, and our uncertainty around implementations of - // concurrent handlemaps. - // - // The use case for them is also quite exotic: it's passing a foreign callback back to the foreign - // language. - // - // Until we have some certainty, and use cases, we shouldn't use them. - fn lower(_obj: Self) -> Self::FfiType { - panic!("Lowering CallbackInterface not supported") - } - - fn write(_obj: Self, _buf: &mut std::vec::Vec) { - panic!("Writing CallbackInterface not supported") - } - - fn try_lift(v: Self::FfiType) -> uniffi::deps::anyhow::Result { + fn try_lift(v: Self::FfiType) -> ::uniffi::deps::anyhow::Result { Ok(::std::boxed::Box::new(<#trait_impl_ident>::new(v))) } - fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result { + fn try_read(buf: &mut &[u8]) -> ::uniffi::deps::anyhow::Result { use uniffi::deps::bytes::Buf; - uniffi::check_remaining(buf, 8)?; - >::try_lift(buf.get_u64()) + ::uniffi::check_remaining(buf, 8)?; + >::try_lift(buf.get_u64()) } - ::uniffi::ffi_converter_default_return!(#tag); - const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code( ::uniffi::metadata::codes::TYPE_CALLBACK_INTERFACE, ) .concat_str(#mod_path) - .concat_str(#name); + .concat_str(#trait_name); } - #lift_ref_impl_spec { + unsafe #lift_ref_impl_spec { type LiftType = #box_dyn_trait; } } diff --git a/uniffi_macros/src/export/item.rs b/uniffi_macros/src/export/item.rs index f0d68048a2..98c7d0ebe2 100644 --- a/uniffi_macros/src/export/item.rs +++ b/uniffi_macros/src/export/item.rs @@ -7,6 +7,7 @@ use proc_macro2::{Ident, Span}; use quote::ToTokens; use super::attributes::{ExportAttributeArguments, ExportedImplFnAttributes}; +use uniffi_meta::UniffiTraitDiscriminants; pub(super) enum ExportItem { Function { @@ -21,6 +22,10 @@ pub(super) enum ExportItem { items: Vec, callback_interface: bool, }, + Struct { + self_ident: Ident, + uniffi_traits: Vec, + }, } impl ExportItem { @@ -32,6 +37,7 @@ impl ExportItem { } syn::Item::Impl(item) => Self::from_impl(item, args.constructor.is_some()), syn::Item::Trait(item) => Self::from_trait(item, args.callback_interface.is_some()), + syn::Item::Struct(item) => Self::from_struct(item, args), // FIXME: Support const / static? _ => Err(syn::Error::new( Span::call_site(), @@ -150,6 +156,26 @@ impl ExportItem { callback_interface, }) } + + fn from_struct(item: syn::ItemStruct, args: &ExportAttributeArguments) -> syn::Result { + let mut uniffi_traits = Vec::new(); + if args.trait_debug.is_some() { + uniffi_traits.push(UniffiTraitDiscriminants::Debug); + } + if args.trait_display.is_some() { + uniffi_traits.push(UniffiTraitDiscriminants::Display); + } + if args.trait_hash.is_some() { + uniffi_traits.push(UniffiTraitDiscriminants::Hash); + } + if args.trait_eq.is_some() { + uniffi_traits.push(UniffiTraitDiscriminants::Eq); + } + Ok(Self::Struct { + self_ident: item.ident, + uniffi_traits, + }) + } } pub(super) enum ImplItem { diff --git a/uniffi_macros/src/export/scaffolding.rs b/uniffi_macros/src/export/scaffolding.rs index 3f25d4ed0d..d00d8403bd 100644 --- a/uniffi_macros/src/export/scaffolding.rs +++ b/uniffi_macros/src/export/scaffolding.rs @@ -92,9 +92,9 @@ pub(super) fn gen_method_scaffolding( struct ScaffoldingBits { /// Parameters for the scaffolding function params: Vec, - /// Statements to execute before `rust_fn_call` - pre_fn_call: TokenStream, - /// Tokenstream for the call to the actual Rust function + /// Lift closure. See `FnSignature::lift_closure` for an explanation of this. + lift_closure: TokenStream, + /// Expression to call the Rust function after a successful lift. rust_fn_call: TokenStream, } @@ -102,17 +102,18 @@ impl ScaffoldingBits { fn new_for_function(sig: &FnSignature, udl_mode: bool) -> Self { let ident = &sig.ident; let params: Vec<_> = sig.args.iter().map(NamedArg::scaffolding_param).collect(); - let param_lifts = sig.lift_exprs(); - let simple_rust_fn_call = quote! { #ident(#(#param_lifts,)*) }; + let call_params = sig.rust_call_params(false); + let rust_fn_call = quote! { #ident(#call_params) }; + // UDL mode adds an extra conversion (#1749) let rust_fn_call = if udl_mode && sig.looks_like_result { - quote! { #simple_rust_fn_call.map_err(::std::convert::Into::into) } + quote! { #rust_fn_call.map_err(::std::convert::Into::into) } } else { - simple_rust_fn_call + rust_fn_call }; Self { params, - pre_fn_call: quote! {}, + lift_closure: sig.lift_closure(None), rust_fn_call, } } @@ -124,35 +125,51 @@ impl ScaffoldingBits { udl_mode: bool, ) -> Self { let ident = &sig.ident; - let ffi_converter = if is_trait { + let lift_impl = if is_trait { quote! { - <::std::sync::Arc as ::uniffi::FfiConverter> + <::std::sync::Arc as ::uniffi::Lift> } } else { quote! { - <::std::sync::Arc<#self_ident> as ::uniffi::FfiConverter> + <::std::sync::Arc<#self_ident> as ::uniffi::Lift> } }; - let params: Vec<_> = iter::once(quote! { uniffi_self_lowered: #ffi_converter::FfiType }) + let params: Vec<_> = iter::once(quote! { uniffi_self_lowered: #lift_impl::FfiType }) .chain(sig.scaffolding_params()) .collect(); - let param_lifts = sig.lift_exprs(); - let simple_rust_fn_call = quote! { uniffi_self.#ident(#(#param_lifts,)*) }; + let try_lift_self = if is_trait { + // For trait interfaces we need to special case this. Trait interfaces normally lift + // foreign trait impl pointers. However, for a method call, we want to lift a Rust + // pointer. + quote! { + { + let foreign_arc = ::std::boxed::Box::leak(unsafe { Box::from_raw(uniffi_self_lowered as *mut ::std::sync::Arc) }); + // Take a clone for our own use. + Ok(::std::sync::Arc::clone(foreign_arc)) + } + } + } else { + quote! { #lift_impl::try_lift(uniffi_self_lowered) } + }; + + let lift_closure = sig.lift_closure(Some(quote! { + match #try_lift_self { + Ok(v) => v, + Err(e) => return Err(("self", e)) + } + })); + let call_params = sig.rust_call_params(true); + let rust_fn_call = quote! { uniffi_args.0.#ident(#call_params) }; + // UDL mode adds an extra conversion (#1749) let rust_fn_call = if udl_mode && sig.looks_like_result { - quote! { #simple_rust_fn_call.map_err(::std::convert::Into::into) } + quote! { #rust_fn_call.map_err(::std::convert::Into::into) } } else { - simple_rust_fn_call + rust_fn_call }; - let return_ffi_converter = sig.return_ffi_converter(); Self { params, - pre_fn_call: quote! { - let uniffi_self = match #ffi_converter::try_lift(uniffi_self_lowered) { - Ok(v) => v, - Err(e) => return Err(#return_ffi_converter::handle_failed_lift("self", e)), - }; - }, + lift_closure, rust_fn_call, } } @@ -160,20 +177,21 @@ impl ScaffoldingBits { fn new_for_constructor(sig: &FnSignature, self_ident: &Ident, udl_mode: bool) -> Self { let ident = &sig.ident; let params: Vec<_> = sig.args.iter().map(NamedArg::scaffolding_param).collect(); - let param_lifts = sig.lift_exprs(); - let simple_rust_fn_call = quote! { #self_ident::#ident(#(#param_lifts,)*) }; + let call_params = sig.rust_call_params(false); + let rust_fn_call = quote! { #self_ident::#ident(#call_params) }; + // UDL mode adds extra conversions (#1749) let rust_fn_call = match (udl_mode, sig.looks_like_result) { // For UDL - (true, false) => quote! { ::std::sync::Arc::new(#simple_rust_fn_call) }, + (true, false) => quote! { ::std::sync::Arc::new(#rust_fn_call) }, (true, true) => { - quote! { #simple_rust_fn_call.map(::std::sync::Arc::new).map_err(::std::convert::Into::into) } + quote! { #rust_fn_call.map(::std::sync::Arc::new).map_err(::std::convert::Into::into) } } - (false, _) => simple_rust_fn_call, + (false, _) => rust_fn_call, }; Self { params, - pre_fn_call: quote! {}, + lift_closure: sig.lift_closure(None), rust_fn_call, } } @@ -183,14 +201,14 @@ impl ScaffoldingBits { /// /// `pre_fn_call` is the statements that we should execute before the rust call /// `rust_fn` is the Rust function to call. -fn gen_ffi_function( +pub(super) fn gen_ffi_function( sig: &FnSignature, arguments: &ExportAttributeArguments, udl_mode: bool, ) -> syn::Result { let ScaffoldingBits { params, - pre_fn_call, + lift_closure, rust_fn_call, } = match &sig.kind { FnKind::Function => ScaffoldingBits::new_for_function(sig, udl_mode), @@ -213,7 +231,7 @@ fn gen_ffi_function( let ffi_ident = sig.scaffolding_fn_ident()?; let name = &sig.name; - let return_ty = &sig.return_ty; + let return_impl = &sig.return_impl(); Ok(if !sig.is_async { quote! { @@ -222,12 +240,17 @@ fn gen_ffi_function( #vis extern "C" fn #ffi_ident( #(#params,)* call_status: &mut ::uniffi::RustCallStatus, - ) -> <#return_ty as ::uniffi::FfiConverter>::ReturnType { + ) -> #return_impl::ReturnType { ::uniffi::deps::log::debug!(#name); + let uniffi_lift_args = #lift_closure; ::uniffi::rust_call(call_status, || { - #pre_fn_call - <#return_ty as ::uniffi::FfiConverter>::lower_return( - #rust_fn_call + #return_impl::lower_return( + match uniffi_lift_args() { + Ok(uniffi_args) => #rust_fn_call, + Err((arg_name, anyhow_error)) => { + #return_impl::handle_failed_lift(arg_name, anyhow_error) + }, + } ) }) } @@ -241,25 +264,25 @@ fn gen_ffi_function( quote! { #[doc(hidden)] #[no_mangle] - #vis extern "C" fn #ffi_ident( - #(#params,)* - uniffi_executor_handle: ::uniffi::ForeignExecutorHandle, - uniffi_callback: <#return_ty as ::uniffi::FfiConverter>::FutureCallback, - uniffi_callback_data: *const (), - uniffi_call_status: &mut ::uniffi::RustCallStatus, - ) { + pub extern "C" fn #ffi_ident(#(#params,)*) -> ::uniffi::RustFutureHandle { ::uniffi::deps::log::debug!(#name); - ::uniffi::rust_call(uniffi_call_status, || { - #pre_fn_call; - let uniffi_rust_future = ::uniffi::RustFuture::<_, #return_ty, crate::UniFfiTag>::new( - #future_expr, - uniffi_executor_handle, - uniffi_callback, - uniffi_callback_data - ); - uniffi_rust_future.wake(); - Ok(()) - }); + let uniffi_lift_args = #lift_closure; + match uniffi_lift_args() { + Ok(uniffi_args) => { + ::uniffi::rust_future_new( + async move { #future_expr.await }, + crate::UniFfiTag + ) + }, + Err((arg_name, anyhow_error)) => { + ::uniffi::rust_future_new( + async move { + #return_impl::handle_failed_lift(arg_name, anyhow_error) + }, + crate::UniFfiTag, + ) + }, + } } } }) diff --git a/uniffi_macros/src/export/trait_interface.rs b/uniffi_macros/src/export/trait_interface.rs new file mode 100644 index 0000000000..c4755015dc --- /dev/null +++ b/uniffi_macros/src/export/trait_interface.rs @@ -0,0 +1,132 @@ +/* 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 proc_macro2::{Ident, Span, TokenStream}; +use quote::{quote, quote_spanned}; + +use crate::{ + export::{ + attributes::ExportAttributeArguments, callback_interface, gen_method_scaffolding, + item::ImplItem, + }, + object::interface_meta_static_var, + util::{ident_to_string, tagged_impl_header}, +}; +use uniffi_meta::free_fn_symbol_name; + +pub(super) fn gen_trait_scaffolding( + mod_path: &str, + args: ExportAttributeArguments, + self_ident: Ident, + items: Vec, + udl_mode: bool, +) -> syn::Result { + if let Some(rt) = args.async_runtime { + return Err(syn::Error::new_spanned(rt, "not supported for traits")); + } + let trait_name = ident_to_string(&self_ident); + let trait_impl = callback_interface::trait_impl(mod_path, &self_ident, &items) + .unwrap_or_else(|e| e.into_compile_error()); + + let free_fn_ident = Ident::new( + &free_fn_symbol_name(mod_path, &trait_name), + Span::call_site(), + ); + + let free_tokens = quote! { + #[doc(hidden)] + #[no_mangle] + pub extern "C" fn #free_fn_ident( + ptr: *const ::std::ffi::c_void, + call_status: &mut ::uniffi::RustCallStatus + ) { + uniffi::rust_call(call_status, || { + assert!(!ptr.is_null()); + drop(unsafe { ::std::boxed::Box::from_raw(ptr as *mut std::sync::Arc) }); + Ok(()) + }); + } + }; + + let impl_tokens: TokenStream = items + .into_iter() + .map(|item| match item { + ImplItem::Method(sig) => { + if sig.is_async { + return Err(syn::Error::new( + sig.span, + "async trait methods are not supported", + )); + } + gen_method_scaffolding(sig, &args, udl_mode) + } + _ => unreachable!("traits have no constructors"), + }) + .collect::>()?; + + let meta_static_var = (!udl_mode).then(|| { + interface_meta_static_var(&self_ident, true, mod_path) + .unwrap_or_else(syn::Error::into_compile_error) + }); + let ffi_converter_tokens = ffi_converter(mod_path, &self_ident, false); + + Ok(quote_spanned! { self_ident.span() => + #meta_static_var + #free_tokens + #trait_impl + #impl_tokens + #ffi_converter_tokens + }) +} + +pub(crate) fn ffi_converter(mod_path: &str, trait_ident: &Ident, udl_mode: bool) -> TokenStream { + let impl_spec = tagged_impl_header("FfiConverterArc", "e! { dyn #trait_ident }, udl_mode); + let lift_ref_impl_spec = tagged_impl_header("LiftRef", "e! { dyn #trait_ident }, udl_mode); + let trait_name = ident_to_string(trait_ident); + let trait_impl_ident = callback_interface::trait_impl_ident(&trait_name); + + quote! { + // All traits must be `Sync + Send`. The generated scaffolding will fail to compile + // if they are not, but unfortunately it fails with an unactionably obscure error message. + // By asserting the requirement explicitly, we help Rust produce a more scrutable error message + // and thus help the user debug why the requirement isn't being met. + uniffi::deps::static_assertions::assert_impl_all!(dyn #trait_ident: Sync, Send); + + unsafe #impl_spec { + type FfiType = *const ::std::os::raw::c_void; + + fn lower(obj: ::std::sync::Arc) -> Self::FfiType { + ::std::boxed::Box::into_raw(::std::boxed::Box::new(obj)) as *const ::std::os::raw::c_void + } + + fn try_lift(v: Self::FfiType) -> ::uniffi::deps::anyhow::Result<::std::sync::Arc> { + Ok(::std::sync::Arc::new(<#trait_impl_ident>::new(v as u64))) + } + + fn write(obj: ::std::sync::Arc, buf: &mut Vec) { + ::uniffi::deps::static_assertions::const_assert!(::std::mem::size_of::<*const ::std::ffi::c_void>() <= 8); + ::uniffi::deps::bytes::BufMut::put_u64( + buf, + >::lower(obj) as u64, + ); + } + + fn try_read(buf: &mut &[u8]) -> ::uniffi::Result<::std::sync::Arc> { + ::uniffi::deps::static_assertions::const_assert!(::std::mem::size_of::<*const ::std::ffi::c_void>() <= 8); + ::uniffi::check_remaining(buf, 8)?; + >::try_lift( + ::uniffi::deps::bytes::Buf::get_u64(buf) as Self::FfiType) + } + + const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::TYPE_INTERFACE) + .concat_str(#mod_path) + .concat_str(#trait_name) + .concat_bool(true); + } + + unsafe #lift_ref_impl_spec { + type LiftType = ::std::sync::Arc; + } + } +} diff --git a/uniffi_macros/src/export/utrait.rs b/uniffi_macros/src/export/utrait.rs new file mode 100644 index 0000000000..3db09ea2b7 --- /dev/null +++ b/uniffi_macros/src/export/utrait.rs @@ -0,0 +1,168 @@ +/* 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 proc_macro2::{Ident, TokenStream}; +use quote::quote; +use syn::ext::IdentExt; + +use super::{attributes::ExportAttributeArguments, gen_ffi_function}; +use crate::fnsig::FnSignature; +use uniffi_meta::UniffiTraitDiscriminants; + +pub(crate) fn expand_uniffi_trait_export( + self_ident: Ident, + uniffi_traits: Vec, +) -> syn::Result { + let udl_mode = false; + let mut impl_items = Vec::new(); + let mut global_items = Vec::new(); + for trait_id in uniffi_traits { + match trait_id { + UniffiTraitDiscriminants::Debug => { + let method = quote! { + fn uniffi_trait_debug(&self) -> String { + ::uniffi::deps::static_assertions::assert_impl_all!(#self_ident: ::std::fmt::Debug); + format!("{:?}", self) + } + }; + let (ffi_func, method_meta) = + process_uniffi_trait_method(&method, &self_ident, udl_mode)?; + // metadata for the trait - which includes metadata for the method. + let discr = UniffiTraitDiscriminants::Debug as u8; + let trait_meta = crate::util::create_metadata_items( + "uniffi_trait", + &format!("{}_Debug", self_ident.unraw()), + quote! { + ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::UNIFFI_TRAIT) + .concat_value(#discr) + .concat(#method_meta) + }, + None, + ); + impl_items.push(method); + global_items.push(ffi_func); + global_items.push(trait_meta); + } + UniffiTraitDiscriminants::Display => { + let method = quote! { + fn uniffi_trait_display(&self) -> String { + ::uniffi::deps::static_assertions::assert_impl_all!(#self_ident: ::std::fmt::Display); + format!("{}", self) + } + }; + let (ffi_func, method_meta) = + process_uniffi_trait_method(&method, &self_ident, udl_mode)?; + // metadata for the trait - which includes metadata for the method. + let discr = UniffiTraitDiscriminants::Display as u8; + let trait_meta = crate::util::create_metadata_items( + "uniffi_trait", + &format!("{}_Display", self_ident.unraw()), + quote! { + ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::UNIFFI_TRAIT) + .concat_value(#discr) + .concat(#method_meta) + }, + None, + ); + impl_items.push(method); + global_items.push(ffi_func); + global_items.push(trait_meta); + } + UniffiTraitDiscriminants::Hash => { + let method = quote! { + fn uniffi_trait_hash(&self) -> u64 { + use ::std::hash::{Hash, Hasher}; + ::uniffi::deps::static_assertions::assert_impl_all!(#self_ident: Hash); + let mut s = ::std::collections::hash_map::DefaultHasher::new(); + Hash::hash(self, &mut s); + s.finish() + } + }; + let (ffi_func, method_meta) = + process_uniffi_trait_method(&method, &self_ident, udl_mode)?; + // metadata for the trait - which includes metadata for the hash method. + let discr = UniffiTraitDiscriminants::Hash as u8; + let trait_meta = crate::util::create_metadata_items( + "uniffi_trait", + &format!("{}_Hash", self_ident.unraw()), + quote! { + ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::UNIFFI_TRAIT) + .concat_value(#discr) + .concat(#method_meta) + }, + None, + ); + impl_items.push(method); + global_items.push(ffi_func); + global_items.push(trait_meta); + } + UniffiTraitDiscriminants::Eq => { + let method_eq = quote! { + fn uniffi_trait_eq_eq(&self, other: &#self_ident) -> bool { + use ::std::cmp::PartialEq; + uniffi::deps::static_assertions::assert_impl_all!(#self_ident: PartialEq); // This object has a trait method which requires `PartialEq` be implemented. + PartialEq::eq(self, other) + } + }; + let method_ne = quote! { + fn uniffi_trait_eq_ne(&self, other: &#self_ident) -> bool { + use ::std::cmp::PartialEq; + uniffi::deps::static_assertions::assert_impl_all!(#self_ident: PartialEq); // This object has a trait method which requires `PartialEq` be implemented. + PartialEq::ne(self, other) + } + }; + let (ffi_func_eq, method_meta_eq) = + process_uniffi_trait_method(&method_eq, &self_ident, udl_mode)?; + let (ffi_func_ne, method_meta_ne) = + process_uniffi_trait_method(&method_ne, &self_ident, udl_mode)?; + // metadata for the trait itself. + let discr = UniffiTraitDiscriminants::Eq as u8; + let trait_meta = crate::util::create_metadata_items( + "uniffi_trait", + &format!("{}_Eq", self_ident.unraw()), + quote! { + ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::UNIFFI_TRAIT) + .concat_value(#discr) + .concat(#method_meta_eq) + .concat(#method_meta_ne) + }, + None, + ); + impl_items.push(method_eq); + impl_items.push(method_ne); + global_items.push(ffi_func_eq); + global_items.push(ffi_func_ne); + global_items.push(trait_meta); + } + } + } + Ok(quote! { + #[doc(hidden)] + impl #self_ident { + #(#impl_items)* + } + #(#global_items)* + }) +} + +fn process_uniffi_trait_method( + method: &TokenStream, + self_ident: &Ident, + udl_mode: bool, +) -> syn::Result<(TokenStream, TokenStream)> { + let item = syn::parse(method.clone().into())?; + + let syn::Item::Fn(item) = item else { + unreachable!() + }; + + let ffi_func = gen_ffi_function( + &FnSignature::new_method(self_ident.clone(), item.sig.clone())?, + &ExportAttributeArguments::default(), + udl_mode, + )?; + // metadata for the method, which will be packed inside metadata for the trait. + let method_meta = FnSignature::new_method(self_ident.clone(), item.sig)?.metadata_expr()?; + Ok((ffi_func, method_meta)) +} diff --git a/uniffi_macros/src/fnsig.rs b/uniffi_macros/src/fnsig.rs index 5ee86866ab..69b6529d1e 100644 --- a/uniffi_macros/src/fnsig.rs +++ b/uniffi_macros/src/fnsig.rs @@ -100,18 +100,55 @@ impl FnSignature { }) } - pub fn return_ffi_converter(&self) -> TokenStream { + pub fn return_impl(&self) -> TokenStream { let return_ty = &self.return_ty; quote! { - <#return_ty as ::uniffi::FfiConverter> + <#return_ty as ::uniffi::LowerReturn> } } - /// Lift expressions for each of our arguments - pub fn lift_exprs(&self) -> impl Iterator + '_ { - self.args - .iter() - .map(|a| a.lift_expr(&self.return_ffi_converter())) + /// Generate a closure that tries to lift all arguments into a tuple. + /// + /// The closure moves all scaffolding arguments into itself and returns: + /// - The lifted argument tuple on success + /// - The field name and error on failure (`Err(&'static str, anyhow::Error>`) + pub fn lift_closure(&self, self_lift: Option) -> TokenStream { + let arg_lifts = self.args.iter().map(|arg| { + let ident = &arg.ident; + let lift_impl = arg.lift_impl(); + let name = &arg.name; + quote! { + match #lift_impl::try_lift(#ident) { + Ok(v) => v, + Err(e) => return Err((#name, e)), + } + } + }); + let all_lifts = self_lift.into_iter().chain(arg_lifts); + quote! { + move || Ok(( + #(#all_lifts,)* + )) + } + } + + /// Call a Rust function from a [Self::lift_closure] success. + /// + /// This takes an Ok value returned by `lift_closure` with the name `uniffi_args` and generates + /// a series of parameters to pass to the Rust function. + pub fn rust_call_params(&self, self_lift: bool) -> TokenStream { + let start_idx = if self_lift { 1 } else { 0 }; + let args = self.args.iter().enumerate().map(|(i, arg)| { + let idx = syn::Index::from(i + start_idx); + let ty = &arg.ty; + match &arg.ref_type { + None => quote! { uniffi_args.#idx }, + Some(ref_type) => quote! { + <#ty as ::std::borrow::Borrow<#ref_type>>::borrow(&uniffi_args.#idx) + }, + } + }); + quote! { #(#args),* } } /// Write expressions for each of our arguments @@ -150,7 +187,7 @@ impl FnSignature { } /// Generate metadata items for this function - pub(crate) fn metadata_items(&self) -> syn::Result { + pub(crate) fn metadata_expr(&self) -> syn::Result { let Self { name, return_ty, @@ -164,21 +201,70 @@ impl FnSignature { self.args.len(), "UniFFI limits functions to 256 arguments", )?; - let arg_metadata_calls = self.args.iter().map(NamedArg::metadata_calls); + let arg_metadata_calls = self.args.iter().map(NamedArg::arg_metadata); match &self.kind { - FnKind::Function => Ok(create_metadata_items( - "func", - name, - quote! { - ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::FUNC) + FnKind::Function => Ok(quote! { + ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::FUNC) + .concat_str(#mod_path) + .concat_str(#name) + .concat_bool(#is_async) + .concat_value(#args_len) + #(#arg_metadata_calls)* + .concat(<#return_ty as ::uniffi::LowerReturn>::TYPE_ID_META) + }), + + FnKind::Method { self_ident } => { + let object_name = ident_to_string(self_ident); + Ok(quote! { + ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::METHOD) .concat_str(#mod_path) + .concat_str(#object_name) .concat_str(#name) .concat_bool(#is_async) .concat_value(#args_len) #(#arg_metadata_calls)* - .concat(<#return_ty as ::uniffi::FfiConverter>::TYPE_ID_META) - }, + .concat(<#return_ty as ::uniffi::LowerReturn>::TYPE_ID_META) + }) + } + + FnKind::TraitMethod { self_ident, index } => { + let object_name = ident_to_string(self_ident); + Ok(quote! { + ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::TRAIT_METHOD) + .concat_str(#mod_path) + .concat_str(#object_name) + .concat_u32(#index) + .concat_str(#name) + .concat_bool(#is_async) + .concat_value(#args_len) + #(#arg_metadata_calls)* + .concat(<#return_ty as ::uniffi::LowerReturn>::TYPE_ID_META) + }) + } + + FnKind::Constructor { self_ident } => { + let object_name = ident_to_string(self_ident); + Ok(quote! { + ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::CONSTRUCTOR) + .concat_str(#mod_path) + .concat_str(#object_name) + .concat_str(#name) + .concat_value(#args_len) + #(#arg_metadata_calls)* + .concat(<#return_ty as ::uniffi::LowerReturn>::TYPE_ID_META) + }) + } + } + } + + pub(crate) fn metadata_items(&self) -> syn::Result { + let Self { name, .. } = &self; + match &self.kind { + FnKind::Function => Ok(create_metadata_items( + "func", + name, + self.metadata_expr()?, Some(self.checksum_symbol_name()), )), @@ -187,36 +273,17 @@ impl FnSignature { Ok(create_metadata_items( "method", &format!("{object_name}_{name}"), - quote! { - ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::METHOD) - .concat_str(#mod_path) - .concat_str(#object_name) - .concat_str(#name) - .concat_bool(#is_async) - .concat_value(#args_len) - #(#arg_metadata_calls)* - .concat(<#return_ty as ::uniffi::FfiConverter>::TYPE_ID_META) - }, + self.metadata_expr()?, Some(self.checksum_symbol_name()), )) } - FnKind::TraitMethod { self_ident, index } => { + FnKind::TraitMethod { self_ident, .. } => { let object_name = ident_to_string(self_ident); Ok(create_metadata_items( "method", &format!("{object_name}_{name}"), - quote! { - ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::TRAIT_METHOD) - .concat_str(#mod_path) - .concat_str(#object_name) - .concat_u32(#index) - .concat_str(#name) - .concat_bool(#is_async) - .concat_value(#args_len) - #(#arg_metadata_calls)* - .concat(<#return_ty as ::uniffi::FfiConverter>::TYPE_ID_META) - }, + self.metadata_expr()?, Some(self.checksum_symbol_name()), )) } @@ -226,15 +293,7 @@ impl FnSignature { Ok(create_metadata_items( "constructor", &format!("{object_name}_{name}"), - quote! { - ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::CONSTRUCTOR) - .concat_str(#mod_path) - .concat_str(#object_name) - .concat_str(#name) - .concat_value(#args_len) - #(#arg_metadata_calls)* - .concat(<#return_ty as ::uniffi::FfiConverter>::TYPE_ID_META) - }, + self.metadata_expr()?, Some(self.checksum_symbol_name()), )) } @@ -343,16 +402,14 @@ impl NamedArg { } } - /// Generate the expression for this argument's FfiConverter - pub(crate) fn ffi_converter(&self) -> TokenStream { + pub(crate) fn lift_impl(&self) -> TokenStream { let ty = &self.ty; - quote! { <#ty as ::uniffi::FfiConverter> } + quote! { <#ty as ::uniffi::Lift> } } - /// Generate the expression for this argument's FfiType - pub(crate) fn ffi_type(&self) -> TokenStream { - let ffi_converter = self.ffi_converter(); - quote! { #ffi_converter::FfiType } + pub(crate) fn lower_impl(&self) -> TokenStream { + let ty = &self.ty; + quote! { <#ty as ::uniffi::Lower> } } /// Generate the parameter for this Arg @@ -365,43 +422,23 @@ impl NamedArg { /// Generate the scaffolding parameter for this Arg pub(crate) fn scaffolding_param(&self) -> TokenStream { let ident = &self.ident; - let ffi_type = self.ffi_type(); - quote! { #ident: #ffi_type } - } - - /// Generate the expression to lift the scaffolding parameter for this arg - pub(crate) fn lift_expr(&self, return_ffi_converter: &TokenStream) -> TokenStream { - let ident = &self.ident; - let ty = &self.ty; - let ffi_converter = self.ffi_converter(); - let name = &self.name; - let lift = quote! { - match #ffi_converter::try_lift(#ident) { - Ok(v) => v, - Err(e) => return Err(#return_ffi_converter::handle_failed_lift(#name, e)) - } - }; - match &self.ref_type { - None => lift, - Some(ref_type) => quote! { - <#ty as ::std::borrow::Borrow<#ref_type>>::borrow(&#lift) - }, - } + let lift_impl = self.lift_impl(); + quote! { #ident: #lift_impl::FfiType } } /// Generate the expression to write the scaffolding parameter for this arg pub(crate) fn write_expr(&self, buf_ident: &Ident) -> TokenStream { let ident = &self.ident; - let ffi_converter = self.ffi_converter(); - quote! { #ffi_converter::write(#ident, &mut #buf_ident) } + let lower_impl = self.lower_impl(); + quote! { #lower_impl::write(#ident, &mut #buf_ident) } } - pub(crate) fn metadata_calls(&self) -> TokenStream { + pub(crate) fn arg_metadata(&self) -> TokenStream { let name = &self.name; - let ffi_converter = self.ffi_converter(); + let lift_impl = self.lift_impl(); quote! { .concat_str(#name) - .concat(#ffi_converter::TYPE_ID_META) + .concat(#lift_impl::TYPE_ID_META) } } } diff --git a/uniffi_macros/src/lib.rs b/uniffi_macros/src/lib.rs index 4cffddfa0e..b7ba86ddc1 100644 --- a/uniffi_macros/src/lib.rs +++ b/uniffi_macros/src/lib.rs @@ -257,14 +257,6 @@ pub fn export_for_udl(attrs: TokenStream, input: TokenStream) -> TokenStream { do_export(attrs, input, true) } -/// Generate various support elements, including the FfiConverter implementation, -/// for a trait interface for the scaffolding code -#[doc(hidden)] -#[proc_macro] -pub fn expand_trait_interface_support(tokens: TokenStream) -> TokenStream { - export::ffi_converter_trait_impl(&syn::parse_macro_input!(tokens), true).into() -} - /// Generate the FfiConverter implementation for an trait interface for the scaffolding code #[doc(hidden)] #[proc_macro] diff --git a/uniffi_macros/src/object.rs b/uniffi_macros/src/object.rs index ee992eb074..573a1eaadd 100644 --- a/uniffi_macros/src/object.rs +++ b/uniffi_macros/src/object.rs @@ -64,8 +64,6 @@ pub(crate) fn interface_impl(ident: &Ident, udl_mode: bool) -> TokenStream { unsafe #impl_spec { // Don't use a pointer to as that requires a `pub ` type FfiType = *const ::std::os::raw::c_void; - type ReturnType = *const ::std::os::raw::c_void; - type FutureCallback = ::uniffi::FutureCallback; /// When lowering, we have an owned `Arc` and we transfer that ownership /// to the foreign-language code, "leaking" it out of Rust's ownership system @@ -117,26 +115,13 @@ pub(crate) fn interface_impl(ident: &Ident, udl_mode: bool) -> TokenStream { >::try_lift(::uniffi::deps::bytes::Buf::get_u64(buf) as Self::FfiType) } - fn lower_return(v: ::std::sync::Arc) -> ::std::result::Result { - Ok(>::lower(v)) - } - - fn invoke_future_callback( - callback: Self::FutureCallback, - callback_data: *const (), - return_value: Self::ReturnType, - call_status: ::uniffi::RustCallStatus, - ) { - callback(callback_data, return_value, call_status); - } - const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::TYPE_INTERFACE) .concat_str(#mod_path) .concat_str(#name) .concat_bool(false); } - #lift_ref_impl_spec { + unsafe #lift_ref_impl_spec { type LiftType = ::std::sync::Arc; } } diff --git a/uniffi_macros/src/record.rs b/uniffi_macros/src/record.rs index aa7fa1eebc..abf2743ec6 100644 --- a/uniffi_macros/src/record.rs +++ b/uniffi_macros/src/record.rs @@ -6,8 +6,9 @@ use syn::{ }; use crate::util::{ - create_metadata_items, either_attribute_arg, ident_to_string, kw, mod_path, tagged_impl_header, - try_metadata_value_from_usize, try_read_field, AttributeSliceExt, UniffiAttributeArgs, + create_metadata_items, derive_all_ffi_traits, either_attribute_arg, ident_to_string, kw, + mod_path, tagged_impl_header, try_metadata_value_from_usize, try_read_field, AttributeSliceExt, + UniffiAttributeArgs, }; pub fn expand_record(input: DeriveInput, udl_mode: bool) -> syn::Result { @@ -40,7 +41,7 @@ pub(crate) fn record_ffi_converter_impl( udl_mode: bool, ) -> syn::Result { let impl_spec = tagged_impl_header("FfiConverter", ident, udl_mode); - let lift_ref_impl_spec = tagged_impl_header("LiftRef", ident, udl_mode); + let derive_ffi_traits = derive_all_ffi_traits(ident, udl_mode); let name = ident_to_string(ident); let mod_path = mod_path()?; let write_impl: TokenStream = record.fields.iter().map(write_field).collect(); @@ -50,7 +51,6 @@ pub(crate) fn record_ffi_converter_impl( #[automatically_derived] unsafe #impl_spec { ::uniffi::ffi_converter_rust_buffer_lift_and_lower!(crate::UniFfiTag); - ::uniffi::ffi_converter_default_return!(crate::UniFfiTag); fn write(obj: Self, buf: &mut ::std::vec::Vec) { #write_impl @@ -65,9 +65,7 @@ pub(crate) fn record_ffi_converter_impl( .concat_str(#name); } - #lift_ref_impl_spec { - type LiftType = Self; - } + #derive_ffi_traits }) } @@ -76,7 +74,7 @@ fn write_field(f: &Field) -> TokenStream { let ty = &f.ty; quote! { - <#ty as ::uniffi::FfiConverter>::write(obj.#ident, buf); + <#ty as ::uniffi::Lower>::write(obj.#ident, buf); } } @@ -158,9 +156,11 @@ pub(crate) fn record_meta_static_var( None => quote! { .concat_bool(false) }, }; + // Note: fields need to implement both `Lower` and `Lift` to be used in a record. The + // TYPE_ID_META should be the same for both traits. Ok(quote! { .concat_str(#name) - .concat(<#ty as ::uniffi::FfiConverter>::TYPE_ID_META) + .concat(<#ty as ::uniffi::Lower>::TYPE_ID_META) #default }) }) diff --git a/uniffi_macros/src/setup_scaffolding.rs b/uniffi_macros/src/setup_scaffolding.rs index 7b5a46f10f..d8596c77dd 100644 --- a/uniffi_macros/src/setup_scaffolding.rs +++ b/uniffi_macros/src/setup_scaffolding.rs @@ -22,6 +22,9 @@ pub fn setup_scaffolding(namespace: String) -> Result { let reexport_hack_ident = format_ident!("{module_path}_uniffi_reexport_hack"); let ffi_foreign_executor_callback_set_ident = format_ident!("ffi_{module_path}_foreign_executor_callback_set"); + let ffi_rust_future_continuation_callback_set = + format_ident!("ffi_{module_path}_rust_future_continuation_callback_set"); + let ffi_rust_future_scaffolding_fns = rust_future_scaffolding_fns(&module_path); Ok(quote! { // Unit struct to parameterize the FfiConverter trait. @@ -93,6 +96,15 @@ pub fn setup_scaffolding(namespace: String) -> Result { uniffi::ffi::foreign_executor_callback_set(callback) } + #[allow(clippy::missing_safety_doc, missing_docs)] + #[doc(hidden)] + #[no_mangle] + pub unsafe extern "C" fn #ffi_rust_future_continuation_callback_set(callback: ::uniffi::RustFutureContinuationCallback) { + ::uniffi::ffi::rust_future_continuation_callback_set(callback); + } + + #ffi_rust_future_scaffolding_fns + // Code to re-export the UniFFI scaffolding functions. // // Rust won't always re-export the functions from dependencies @@ -132,3 +144,74 @@ pub fn setup_scaffolding(namespace: String) -> Result { } }) } + +/// Generates the rust_future_* functions +/// +/// The foreign side uses a type-erased `RustFutureHandle` to interact with futures, which presents +/// a problem when creating scaffolding functions. What is the `ReturnType` parameter of `RustFutureFfi`? +/// +/// Handle this by using some brute-force monomorphization. For each possible ffi type, we +/// generate a set of scaffolding functions. The bindings code is responsible for calling the one +/// corresponds the scaffolding function that created the `RustFutureHandle`. +/// +/// This introduces safety issues, but we do get some type checking. If the bindings code calls +/// the wrong rust_future_complete function, they should get an unexpected return type, which +/// hopefully will result in a compile-time error. +fn rust_future_scaffolding_fns(module_path: &str) -> TokenStream { + let fn_info = [ + (quote! { u8 }, "u8"), + (quote! { i8 }, "i8"), + (quote! { u16 }, "u16"), + (quote! { i16 }, "i16"), + (quote! { u32 }, "u32"), + (quote! { i32 }, "i32"), + (quote! { u64 }, "u64"), + (quote! { i64 }, "i64"), + (quote! { f32 }, "f32"), + (quote! { f64 }, "f64"), + (quote! { *const ::std::ffi::c_void }, "pointer"), + (quote! { ::uniffi::RustBuffer }, "rust_buffer"), + (quote! { () }, "void"), + ]; + fn_info.iter() + .map(|(return_type, fn_suffix)| { + let ffi_rust_future_poll = format_ident!("ffi_{module_path}_rust_future_poll_{fn_suffix}"); + let ffi_rust_future_cancel = format_ident!("ffi_{module_path}_rust_future_cancel_{fn_suffix}"); + let ffi_rust_future_complete = format_ident!("ffi_{module_path}_rust_future_complete_{fn_suffix}"); + let ffi_rust_future_free = format_ident!("ffi_{module_path}_rust_future_free_{fn_suffix}"); + + quote! { + #[allow(clippy::missing_safety_doc, missing_docs)] + #[doc(hidden)] + #[no_mangle] + pub unsafe extern "C" fn #ffi_rust_future_poll(handle: ::uniffi::RustFutureHandle, data: *const ()) { + ::uniffi::ffi::rust_future_poll::<#return_type>(handle, data); + } + + #[allow(clippy::missing_safety_doc, missing_docs)] + #[doc(hidden)] + #[no_mangle] + pub unsafe extern "C" fn #ffi_rust_future_cancel(handle: ::uniffi::RustFutureHandle) { + ::uniffi::ffi::rust_future_cancel::<#return_type>(handle) + } + + #[allow(clippy::missing_safety_doc, missing_docs)] + #[doc(hidden)] + #[no_mangle] + pub unsafe extern "C" fn #ffi_rust_future_complete( + handle: ::uniffi::RustFutureHandle, + out_status: &mut ::uniffi::RustCallStatus + ) -> #return_type { + ::uniffi::ffi::rust_future_complete::<#return_type>(handle, out_status) + } + + #[allow(clippy::missing_safety_doc, missing_docs)] + #[doc(hidden)] + #[no_mangle] + pub unsafe extern "C" fn #ffi_rust_future_free(handle: ::uniffi::RustFutureHandle) { + ::uniffi::ffi::rust_future_free::<#return_type>(handle) + } + } + }) + .collect() +} diff --git a/uniffi_macros/src/util.rs b/uniffi_macros/src/util.rs index b4a5a96108..9f213ea1d7 100644 --- a/uniffi_macros/src/util.rs +++ b/uniffi_macros/src/util.rs @@ -80,7 +80,7 @@ pub fn try_read_field(f: &syn::Field) -> TokenStream { let ty = &f.ty; quote! { - #ident: <#ty as ::uniffi::FfiConverter>::try_read(buf)?, + #ident: <#ty as ::uniffi::Lift>::try_read(buf)?, } } @@ -216,6 +216,33 @@ pub(crate) fn tagged_impl_header( } } +pub(crate) fn derive_all_ffi_traits(ty: &Ident, udl_mode: bool) -> TokenStream { + if udl_mode { + quote! { ::uniffi::derive_ffi_traits!(local #ty); } + } else { + quote! { ::uniffi::derive_ffi_traits!(blanket #ty); } + } +} + +pub(crate) fn derive_ffi_traits(ty: &Ident, udl_mode: bool, trait_names: &[&str]) -> TokenStream { + let trait_idents = trait_names + .iter() + .map(|name| Ident::new(name, Span::call_site())); + if udl_mode { + quote! { + #( + ::uniffi::derive_ffi_traits!(impl #trait_idents for #ty); + )* + } + } else { + quote! { + #( + ::uniffi::derive_ffi_traits!(impl #trait_idents for #ty); + )* + } + } +} + /// Custom keywords pub mod kw { syn::custom_keyword!(async_runtime); @@ -223,9 +250,14 @@ pub mod kw { syn::custom_keyword!(constructor); syn::custom_keyword!(default); syn::custom_keyword!(flat_error); - syn::custom_keyword!(handle_unknown_callback_error); syn::custom_keyword!(None); syn::custom_keyword!(with_try_read); + syn::custom_keyword!(Debug); + syn::custom_keyword!(Display); + syn::custom_keyword!(Eq); + syn::custom_keyword!(Hash); + // Not used anymore + syn::custom_keyword!(handle_unknown_callback_error); } /// Specifies a type from a dependent crate diff --git a/uniffi_meta/src/group.rs b/uniffi_meta/src/group.rs index 4f39466579..f0be2e5a98 100644 --- a/uniffi_meta/src/group.rs +++ b/uniffi_meta/src/group.rs @@ -7,10 +7,12 @@ use std::collections::{BTreeSet, HashMap}; use crate::*; use anyhow::{bail, Result}; -/// Group metadata by namespace -pub fn group_metadata(items: Vec) -> Result> { +type MetadataGroupMap = HashMap; + +// Create empty metadata groups based on the metadata items. +pub fn create_metadata_groups(items: &[Metadata]) -> MetadataGroupMap { // Map crate names to MetadataGroup instances - let mut group_map = items + items .iter() .filter_map(|i| match i { Metadata::Namespace(namespace) => { @@ -33,48 +35,29 @@ pub fn group_metadata(items: Vec) -> Result> { } _ => None, }) - .collect::>(); + .collect::>() +} +/// Consume the items into the previously created metadata groups. +pub fn group_metadata(group_map: &mut MetadataGroupMap, items: Vec) -> Result<()> { for item in items { - let (item_desc, module_path) = match &item { - Metadata::Namespace(_) => continue, - Metadata::UdlFile(meta) => { - (format!("udl_file `{}`", meta.namespace), &meta.module_path) - } - Metadata::Func(meta) => (format!("function `{}`", meta.name), &meta.module_path), - Metadata::Constructor(meta) => ( - format!("constructor `{}.{}`", meta.self_name, meta.name), - &meta.module_path, - ), - Metadata::Method(meta) => ( - format!("method `{}.{}`", meta.self_name, meta.name), - &meta.module_path, - ), - Metadata::Record(meta) => (format!("record `{}`", meta.name), &meta.module_path), - Metadata::Enum(meta) => (format!("enum `{}`", meta.name), &meta.module_path), - Metadata::Object(meta) => (format!("object `{}`", meta.name), &meta.module_path), - Metadata::CallbackInterface(meta) => ( - format!("callback interface `{}`", meta.name), - &meta.module_path, - ), - Metadata::TraitMethod(meta) => { - (format!("trait method`{}`", meta.name), &meta.module_path) - } - Metadata::Error(meta) => (format!("error `{}`", meta.name()), meta.module_path()), - Metadata::CustomType(meta) => (format!("custom `{}`", meta.name), &meta.module_path), - }; + if matches!(&item, Metadata::Namespace(_)) { + continue; + } + + let crate_name = calc_crate_name(item.module_path()).to_owned(); // XXX - kill clone? - let crate_name = calc_crate_name(module_path); - let group = match group_map.get_mut(crate_name) { + let item = fixup_external_type(item, group_map); + let group = match group_map.get_mut(&crate_name) { Some(ns) => ns, - None => bail!("Unknown namespace for {item_desc} ({crate_name})"), + None => bail!("Unknown namespace for {item:?} ({crate_name})"), }; if group.items.contains(&item) { bail!("Duplicate metadata item: {item:?}"); } group.add_item(item); } - Ok(group_map.into_values().collect()) + Ok(()) } #[derive(Debug)] @@ -85,19 +68,35 @@ pub struct MetadataGroup { impl MetadataGroup { pub fn add_item(&mut self, item: Metadata) { - let converter = ExternalTypeConverter { - crate_name: &self.namespace.crate_name, - }; - self.items.insert(converter.convert_item(item)); + self.items.insert(item); } } +pub fn fixup_external_type(item: Metadata, group_map: &MetadataGroupMap) -> Metadata { + let crate_name = calc_crate_name(item.module_path()).to_owned(); + let converter = ExternalTypeConverter { + crate_name: &crate_name, + crate_to_namespace: group_map, + }; + converter.convert_item(item) +} + /// Convert metadata items by replacing types from external crates with Type::External struct ExternalTypeConverter<'a> { crate_name: &'a str, + crate_to_namespace: &'a MetadataGroupMap, } impl<'a> ExternalTypeConverter<'a> { + fn crate_to_namespace(&self, crate_name: &str) -> String { + self.crate_to_namespace + .get(crate_name) + .unwrap_or_else(|| panic!("Can't find namespace for module {crate_name}")) + .namespace + .name + .clone() + } + fn convert_item(&self, item: Metadata) -> Metadata { match item { Metadata::Func(meta) => Metadata::Func(FnMetadata { @@ -183,9 +182,11 @@ impl<'a> ExternalTypeConverter<'a> { if self.is_module_path_external(&module_path) => { Type::External { + namespace: self.crate_to_namespace(&module_path), module_path, name, kind: ExternalKind::DataClass, + tagged: false, } } Type::Custom { @@ -194,17 +195,21 @@ impl<'a> ExternalTypeConverter<'a> { // For now, it's safe to assume that all custom types are data classes. // There's no reason to use a custom type with an interface. Type::External { + namespace: self.crate_to_namespace(&module_path), module_path, name, kind: ExternalKind::DataClass, + tagged: false, } } Type::Object { module_path, name, .. } if self.is_module_path_external(&module_path) => Type::External { + namespace: self.crate_to_namespace(&module_path), module_path, name, kind: ExternalKind::Interface, + tagged: false, }, Type::CallbackInterface { module_path, name } if self.is_module_path_external(&module_path) => @@ -235,6 +240,24 @@ impl<'a> ExternalTypeConverter<'a> { key_type: Box::new(self.convert_type(*key_type)), value_type: Box::new(self.convert_type(*value_type)), }, + // Existing External types probably need namespace fixed. + Type::External { + namespace, + module_path, + name, + kind, + tagged, + } => { + assert!(namespace.is_empty()); + Type::External { + namespace: self.crate_to_namespace(&module_path), + module_path, + name, + kind, + tagged, + } + } + // Otherwise, just return the type unchanged _ => ty, } diff --git a/uniffi_meta/src/lib.rs b/uniffi_meta/src/lib.rs index 58f31e3887..6625718d19 100644 --- a/uniffi_meta/src/lib.rs +++ b/uniffi_meta/src/lib.rs @@ -12,7 +12,7 @@ mod ffi_names; pub use ffi_names::*; mod group; -pub use group::{group_metadata, MetadataGroup}; +pub use group::{create_metadata_groups, fixup_external_type, group_metadata, MetadataGroup}; mod reader; pub use reader::{read_metadata, read_metadata_type}; @@ -26,7 +26,7 @@ mod metadata; // `docs/uniffi-versioning.md` for details. // // Once we get to 1.0, then we'll need to update the scheme to something like 100 + major_version -pub const UNIFFI_CONTRACT_VERSION: u32 = 23; +pub const UNIFFI_CONTRACT_VERSION: u32 = 24; /// Similar to std::hash::Hash. /// @@ -323,7 +323,6 @@ pub struct ObjectMetadata { pub module_path: String, pub name: String, pub imp: types::ObjectImpl, - pub uniffi_traits: Vec, } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -359,6 +358,48 @@ pub enum UniffiTraitMetadata { }, } +impl UniffiTraitMetadata { + fn module_path(&self) -> &String { + &match self { + UniffiTraitMetadata::Debug { fmt } => fmt, + UniffiTraitMetadata::Display { fmt } => fmt, + UniffiTraitMetadata::Eq { eq, .. } => eq, + UniffiTraitMetadata::Hash { hash } => hash, + } + .module_path + } + + pub fn self_name(&self) -> &String { + &match self { + UniffiTraitMetadata::Debug { fmt } => fmt, + UniffiTraitMetadata::Display { fmt } => fmt, + UniffiTraitMetadata::Eq { eq, .. } => eq, + UniffiTraitMetadata::Hash { hash } => hash, + } + .self_name + } +} + +#[repr(u8)] +pub enum UniffiTraitDiscriminants { + Debug, + Display, + Eq, + Hash, +} + +impl UniffiTraitDiscriminants { + pub fn from(v: u8) -> anyhow::Result { + Ok(match v { + 0 => UniffiTraitDiscriminants::Debug, + 1 => UniffiTraitDiscriminants::Display, + 2 => UniffiTraitDiscriminants::Eq, + 3 => UniffiTraitDiscriminants::Hash, + _ => anyhow::bail!("invalid trait discriminant {v}"), + }) + } +} + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum ErrorMetadata { Enum { enum_: EnumMetadata, is_flat: bool }, @@ -410,12 +451,31 @@ pub enum Metadata { Method(MethodMetadata), TraitMethod(TraitMethodMetadata), CustomType(CustomTypeMetadata), + UniffiTrait(UniffiTraitMetadata), } impl Metadata { pub fn read(data: &[u8]) -> anyhow::Result { read_metadata(data) } + + pub(crate) fn module_path(&self) -> &String { + match self { + Metadata::Namespace(meta) => &meta.crate_name, + Metadata::UdlFile(meta) => &meta.module_path, + Metadata::Func(meta) => &meta.module_path, + Metadata::Constructor(meta) => &meta.module_path, + Metadata::Method(meta) => &meta.module_path, + Metadata::Record(meta) => &meta.module_path, + Metadata::Enum(meta) => &meta.module_path, + Metadata::Object(meta) => &meta.module_path, + Metadata::CallbackInterface(meta) => &meta.module_path, + Metadata::TraitMethod(meta) => &meta.module_path, + Metadata::Error(meta) => meta.module_path(), + Metadata::CustomType(meta) => &meta.module_path, + Metadata::UniffiTrait(meta) => meta.module_path(), + } + } } impl From for Metadata { @@ -489,3 +549,9 @@ impl From for Metadata { Self::CustomType(v) } } + +impl From for Metadata { + fn from(v: UniffiTraitMetadata) -> Self { + Self::UniffiTrait(v) + } +} diff --git a/uniffi_meta/src/metadata.rs b/uniffi_meta/src/metadata.rs index 5b0bcaae13..6e490a4866 100644 --- a/uniffi_meta/src/metadata.rs +++ b/uniffi_meta/src/metadata.rs @@ -21,6 +21,7 @@ pub mod codes { pub const UDL_FILE: u8 = 8; pub const CALLBACK_INTERFACE: u8 = 9; pub const TRAIT_METHOD: u8 = 10; + pub const UNIFFI_TRAIT: u8 = 11; //pub const UNKNOWN: u8 = 255; // Type codes diff --git a/uniffi_meta/src/reader.rs b/uniffi_meta/src/reader.rs index 8cbe8d021e..bf6525f2b5 100644 --- a/uniffi_meta/src/reader.rs +++ b/uniffi_meta/src/reader.rs @@ -57,6 +57,7 @@ impl<'a> MetadataReader<'a> { codes::INTERFACE => self.read_object()?.into(), codes::CALLBACK_INTERFACE => self.read_callback_interface()?.into(), codes::TRAIT_METHOD => self.read_trait_method()?.into(), + codes::UNIFFI_TRAIT => self.read_uniffi_trait()?.into(), _ => bail!("Unexpected metadata code: {value:?}"), }) } @@ -289,7 +290,31 @@ impl<'a> MetadataReader<'a> { module_path: self.read_string()?, name: self.read_string()?, imp: ObjectImpl::from_is_trait(self.read_bool()?), - uniffi_traits: vec![], // TODO: not yet emitted + }) + } + + fn read_uniffi_trait(&mut self) -> Result { + let code = self.read_u8()?; + let mut read_metadata_method = || -> Result { + let code = self.read_u8()?; + ensure!(code == codes::METHOD, "expected METHOD but read {code}"); + self.read_method() + }; + + Ok(match UniffiTraitDiscriminants::from(code)? { + UniffiTraitDiscriminants::Debug => UniffiTraitMetadata::Debug { + fmt: read_metadata_method()?, + }, + UniffiTraitDiscriminants::Display => UniffiTraitMetadata::Display { + fmt: read_metadata_method()?, + }, + UniffiTraitDiscriminants::Eq => UniffiTraitMetadata::Eq { + eq: read_metadata_method()?, + ne: read_metadata_method()?, + }, + UniffiTraitDiscriminants::Hash => UniffiTraitMetadata::Hash { + hash: read_metadata_method()?, + }, }) } diff --git a/uniffi_meta/src/types.rs b/uniffi_meta/src/types.rs index 4129a0735c..24f8a6f2a8 100644 --- a/uniffi_meta/src/types.rs +++ b/uniffi_meta/src/types.rs @@ -114,7 +114,10 @@ pub enum Type { External { module_path: String, name: String, + #[checksum_ignore] // The namespace is not known generating scaffolding. + namespace: String, kind: ExternalKind, + tagged: bool, // does its FfiConverter use ? }, // Custom type on the scaffolding side Custom { diff --git a/uniffi_udl/src/attributes.rs b/uniffi_udl/src/attributes.rs index 2f9c372390..f06b4f29c1 100644 --- a/uniffi_udl/src/attributes.rs +++ b/uniffi_udl/src/attributes.rs @@ -35,6 +35,7 @@ pub(super) enum Attribute { External { crate_name: String, kind: ExternalKind, + export: bool, }, // Custom type on the scaffolding side Custom, @@ -77,10 +78,22 @@ impl TryFrom<&weedle::attribute::ExtendedAttribute<'_>> for Attribute { "External" => Ok(Attribute::External { crate_name: name_from_id_or_string(&identity.rhs), kind: ExternalKind::DataClass, + export: false, + }), + "ExternalExport" => Ok(Attribute::External { + crate_name: name_from_id_or_string(&identity.rhs), + kind: ExternalKind::DataClass, + export: true, }), "ExternalInterface" => Ok(Attribute::External { crate_name: name_from_id_or_string(&identity.rhs), kind: ExternalKind::Interface, + export: false, + }), + "ExternalInterfaceExport" => Ok(Attribute::External { + crate_name: name_from_id_or_string(&identity.rhs), + kind: ExternalKind::Interface, + export: true, }), _ => anyhow::bail!( "Attribute identity Identifier not supported: {:?}", @@ -484,6 +497,14 @@ impl TypedefAttributes { _ => None, }) } + + pub(super) fn external_tagged(&self) -> Option { + // If it was "exported" via a proc-macro the FfiConverter was not tagged. + self.0.iter().find_map(|attr| match attr { + Attribute::External { export, .. } => Some(!*export), + _ => None, + }) + } } impl TryFrom<&weedle::attribute::ExtendedAttributeList<'_>> for TypedefAttributes { @@ -627,7 +648,7 @@ mod test { let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[]").unwrap(); let attrs = FunctionAttributes::try_from(&node).unwrap(); - assert!(matches!(attrs.get_throws_err(), None)); + assert!(attrs.get_throws_err().is_none()); } #[test] @@ -675,12 +696,12 @@ mod test { let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Throws=Error]").unwrap(); let attrs = ConstructorAttributes::try_from(&node).unwrap(); assert!(matches!(attrs.get_throws_err(), Some("Error"))); - assert!(matches!(attrs.get_name(), None)); + assert!(attrs.get_name().is_none()); let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Name=MyFactory]").unwrap(); let attrs = ConstructorAttributes::try_from(&node).unwrap(); - assert!(matches!(attrs.get_throws_err(), None)); + assert!(attrs.get_throws_err().is_none()); assert!(matches!(attrs.get_name(), Some("MyFactory"))); let (_, node) = diff --git a/uniffi_udl/src/converters/interface.rs b/uniffi_udl/src/converters/interface.rs index 5c77168f87..58e6a9c8a0 100644 --- a/uniffi_udl/src/converters/interface.rs +++ b/uniffi_udl/src/converters/interface.rs @@ -123,11 +123,13 @@ impl APIConverter for weedle::InterfaceDefinition<'_> { }) }) .collect::>>()?; + for ut in uniffi_traits { + ci.items.insert(ut.into()); + } Ok(ObjectMetadata { module_path: ci.module_path(), name: object_name.to_string(), imp: object_impl, - uniffi_traits, }) } } diff --git a/uniffi_udl/src/finder.rs b/uniffi_udl/src/finder.rs index f0f76547ad..0c4c187dc0 100644 --- a/uniffi_udl/src/finder.rs +++ b/uniffi_udl/src/finder.rs @@ -130,7 +130,8 @@ impl TypeFinder for weedle::TypedefDefinition<'_> { }, ) } else { - let kind = attrs.external_kind().expect("ExternalKind missing"); + let kind = attrs.external_kind().expect("External missing"); + let tagged = attrs.external_tagged().expect("External missing"); // A crate which can supply an `FfiConverter`. // We don't reference `self._type`, so ideally we could insist on it being // the literal 'extern' but that's tricky @@ -138,8 +139,10 @@ impl TypeFinder for weedle::TypedefDefinition<'_> { name, Type::External { name: name.to_string(), + namespace: "".to_string(), // we don't know this yet module_path: attrs.get_crate_name(), kind, + tagged, }, ) } @@ -249,16 +252,16 @@ mod test { "#, |types| { assert!( - matches!(types.get_type_definition("ExternalType").unwrap(), Type::External { name, module_path, kind: ExternalKind::DataClass } + matches!(types.get_type_definition("ExternalType").unwrap(), Type::External { name, module_path, kind: ExternalKind::DataClass, .. } if name == "ExternalType" && module_path == "crate-name") ); assert!( - matches!(types.get_type_definition("ExternalInterfaceType").unwrap(), Type::External { name, module_path, kind: ExternalKind::Interface } + matches!(types.get_type_definition("ExternalInterfaceType").unwrap(), Type::External { name, module_path, kind: ExternalKind::Interface, .. } if name == "ExternalInterfaceType" && module_path == "crate-name") ); assert!( matches!(types.get_type_definition("CustomType").unwrap(), Type::Custom { name, builtin, ..} - if name == "CustomType" && builtin == Box::new(Type::String)) + if name == "CustomType" && *builtin == Type::String) ); }, );