Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python: Fix custom types generating invalid code when there are forward references #2075

Merged
merged 1 commit into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
did not already start with a capital letter, but this changes makes all type naming consistent.
([#2073](https://github.com/mozilla/uniffi-rs/issues/2073))

- Python: Fix custom types generating invalid code when there are forward references.
([#2067](https://github.com/mozilla/uniffi-rs/issues/2067))

### What's changed?
- The async runtime can be specified for constructors/methods, this will override the runtime specified at the impl block level.

Expand Down
19 changes: 15 additions & 4 deletions docs/manual/src/udl/custom_types.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Custom types

Custom types allow you to extend the UniFFI type system to support types from your Rust crate or 3rd
party libraries. This relies on a [builtin](./builtin_types.md) UDL type move data across the
FFI, followed by a conversion to your custom type.
party libraries. This works by converting to and from some other UniFFI type to move data across the
FFI. You must provide a `UniffiCustomTypeConverter` implementation to convert the types.

## Custom types in the scaffolding code

Expand All @@ -12,7 +12,13 @@ Consider the following trivial Rust abstraction for a "handle" which wraps an in
pub struct Handle(i64);
```

You can use this type in your udl by declaring it via a `typedef` with a `Custom` attribute,
In this trivial example, the simplest way to expose this is with a macro.

```
uniffi::custom_newtype!(Handle, i64);
```

Or you can define this in UDL via a `typedef` with a `Custom` attribute,
defining the builtin type that it's based on.

```idl
Expand All @@ -21,7 +27,12 @@ typedef i64 Handle;
```

For this to work, your Rust code must also implement a special trait named
`UniffiCustomTypeConverter`. This trait is generated by UniFFI and can be found in the generated
`UniffiCustomTypeConverter`.

An implementation is provided if you used the `uniffi::custom_newtype!()` macro.
But if you use UDL or otherwise need to implement your own:

This trait is generated by UniFFI and can be found in the generated
Rust scaffolding - it is defined as:

```Rust
Expand Down
4 changes: 4 additions & 0 deletions fixtures/ext-types/custom-types/src/custom_types.udl
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
[Custom]
typedef string Guid;

// Wrapping another custom type.
[Custom]
typedef Guid ANestedGuid;

[Error]
enum GuidError {
"TooShort"
Expand Down
64 changes: 64 additions & 0 deletions fixtures/ext-types/custom-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,68 @@ impl UniffiCustomTypeConverter for Guid {
}
}

pub struct ANestedGuid(pub Guid);

impl UniffiCustomTypeConverter for ANestedGuid {
type Builtin = Guid;

// This is a "fixture" rather than an "example", so we are free to do things that don't really
// make sense for real apps.
fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> {
Ok(ANestedGuid(val))
}

fn from_custom(obj: Self) -> Self::Builtin {
obj.0
}
}

#[uniffi::export]
fn get_nested_guid(nguid: Option<ANestedGuid>) -> ANestedGuid {
nguid.unwrap_or_else(|| ANestedGuid(Guid("ANestedGuid".to_string())))
}

// Dependent types might come in the "wrong" order - #2067 triggered
// a bug here because of the alphabetical order of these types.
pub struct ANestedOuid(pub Ouid);
uniffi::custom_newtype!(ANestedOuid, Ouid);

#[uniffi::export]
fn get_nested_ouid(nouid: Option<ANestedOuid>) -> ANestedOuid {
nouid.unwrap_or_else(|| ANestedOuid(Ouid("ANestedOuid".to_string())))
}

// And custom types around other objects.
#[derive(uniffi::Object)]
pub struct InnerObject;

#[uniffi::export]
impl InnerObject {
#[uniffi::constructor]
fn new() -> Self {
Self
}
}

pub struct NestedObject(pub std::sync::Arc<InnerObject>);
uniffi::custom_newtype!(NestedObject, std::sync::Arc<InnerObject>);

#[uniffi::export]
pub fn get_nested_object(n: NestedObject) -> NestedObject {
n
}

#[derive(uniffi::Record)]
pub struct InnerRecord {
i: i32,
}

pub struct NestedRecord(pub InnerRecord);
uniffi::custom_newtype!(NestedRecord, InnerRecord);

#[uniffi::export]
pub fn get_nested_record(n: NestedRecord) -> NestedRecord {
n
}

uniffi::include_scaffolding!("custom_types");
3 changes: 3 additions & 0 deletions fixtures/ext-types/custom-types/tests/bindings/test_guid.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,8 @@ def test_guid_callback(self):
self.assertEqual(guid, "callback-test-payload")
self.assertEqual(test_callback.saw_guid, "callback-test-payload")

def test_custom(self):
get_nested_object(InnerObject())

if __name__=='__main__':
unittest.main()
48 changes: 47 additions & 1 deletion fixtures/ext-types/lib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use custom_types::Handle;
use ext_types_custom::Guid;
use ext_types_custom::{ANestedGuid, Guid, Ouid};
use ext_types_external_crate::{
ExternalCrateDictionary, ExternalCrateInterface, ExternalCrateNonExhaustiveEnum,
};
Expand All @@ -11,6 +11,18 @@ use uniffi_one::{
use uniffi_sublib::SubLibType;
use url::Url;

// #1988
uniffi::ffi_converter_forward!(
ext_types_custom::Ouid,
ext_types_custom::UniFfiTag,
crate::UniFfiTag
);
uniffi::ffi_converter_forward!(
ext_types_custom::ANestedGuid,
ext_types_custom::UniFfiTag,
crate::UniFfiTag
);

pub struct CombinedType {
pub uoe: UniffiOneEnum,
pub uot: UniffiOneType,
Expand Down Expand Up @@ -100,6 +112,40 @@ fn get_maybe_urls(urls: Vec<Option<Url>>) -> Vec<Option<Url>> {
urls
}

// XXX - #1854
// fn get_imported_guid(guid: Guid) -> Guid {

#[uniffi::export]
fn get_imported_ouid(ouid: Ouid) -> Ouid {
ouid
}

// external custom types wrapping external custom types.
#[uniffi::export]
fn get_imported_nested_guid(guid: Option<ANestedGuid>) -> ANestedGuid {
guid.unwrap_or_else(|| ANestedGuid(Guid("nested".to_string())))
}

#[uniffi::export]
fn get_imported_nested_ouid(guid: Option<ANestedGuid>) -> ANestedGuid {
guid.unwrap_or_else(|| ANestedGuid(Guid("nested".to_string())))
}

// A local custom type wrapping an external imported UDL type
// XXX - #1854
// pub struct NestedExternalGuid(pub Guid);
// ...
// fn get_nested_external_guid(nguid: Option<NestedExternalGuid>) -> NestedExternalGuid {

// A local custom type wrapping an external imported procmacro type
pub struct NestedExternalOuid(pub Ouid);
uniffi::custom_newtype!(NestedExternalOuid, Ouid);

#[uniffi::export]
fn get_nested_external_ouid(ouid: Option<NestedExternalOuid>) -> NestedExternalOuid {
ouid.unwrap_or_else(|| NestedExternalOuid(Ouid("nested-external-ouid".to_string())))
}

// A struct
fn get_uniffi_one_type(t: UniffiOneType) -> UniffiOneType {
t
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import uniffi.imported_types_lib.*
import uniffi.imported_types_sublib.*
import uniffi.uniffi_one_ns.*
import uniffi.ext_types_custom.*

val ct = getCombinedType(null)
assert(ct.uot.sval == "hello")
Expand All @@ -28,6 +29,11 @@ assert(getMaybeUrl(null) == null)
assert(getUrls(listOf(url)) == listOf(url))
assert(getMaybeUrls(listOf(url, null)) == listOf(url, null))

assert(getGuid("guid") == "guid")
assert(getOuid("ouid") == "ouid")
//assert(getImportedGuid("guid") == "guid")
assert(getImportedOuid("ouid") == "ouid")

val uot = UniffiOneType("hello")
assert(getUniffiOneType(uot) == uot)
assert(getMaybeUniffiOneType(uot)!! == uot)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from imported_types_lib import *
from imported_types_sublib import *
from uniffi_one_ns import *
from ext_types_custom import *

class TestIt(unittest.TestCase):
def test_it(self):
Expand Down Expand Up @@ -40,6 +41,12 @@ def test_get_url(self):
self.assertEqual(get_maybe_url(None), None)
self.assertEqual(get_maybe_urls([url, None]), [url, None])

def test_custom_types(self):
self.assertEqual(get_guid("guid"), "guid")
self.assertEqual(get_ouid("ouid"), "ouid")
self.assertEqual(get_ouid("uuid"), "uuid")
self.assertEqual(get_nested_guid("uuid"), "uuid")

def test_get_uniffi_one_type(self):
t1 = UniffiOneType(sval="hello")
self.assertEqual(t1, get_uniffi_one_type(t1))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ assert(getMaybeUrl(url: nil) == nil)
assert(getUrls(urls: [url]) == [url])
assert(getMaybeUrls(urls: [url, nil]) == [url, nil])

assert(getGuid(value: "guid") == "guid")
assert(getOuid(ouid: "ouid") == "ouid")
assert(getImportedOuid(ouid: "ouid") == "ouid")
assert(getNestedOuid(nouid: "ouid") == "ouid")
assert(getImportedNestedGuid(guid: nil) == "nested")
assert(getNestedExternalOuid(ouid: nil) == "nested-external-ouid")

assert(getUniffiOneType(t: UniffiOneType(sval: "hello")).sval == "hello")
assert(getMaybeUniffiOneType(t: UniffiOneType(sval: "hello"))!.sval == "hello")
assert(getMaybeUniffiOneType(t: nil) == nil)
Expand Down
27 changes: 27 additions & 0 deletions uniffi_bindgen/src/bindings/python/gen_python/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,33 @@ impl<'a> TypeRenderer<'a> {
});
""
}

// An inefficient algo to return type aliases needed for custom types
// in an order such that dependencies are in the correct order.
// Eg, if there's a custom type `Guid` -> `str` and another `GuidWrapper` -> `Guid`,
// it's important the type alias for `Guid` appears first. Fails to handle
// another level of indirection (eg, `A { builtin: C}, B { }, C { builtin: B })`)
// but that's pathological :)
fn get_custom_type_aliases(&self) -> Vec<(String, &Type)> {
let mut ordered = vec![];
for type_ in self.ci.iter_types() {
if let Type::Custom { name, builtin, .. } = type_ {
match ordered
.iter()
.position(|x: &(&str, &Type)| *name == x.1.as_codetype().type_label())
{
// This 'name' appears as a builtin, so we must insert our type first.
Some(pos) => ordered.insert(pos, (name, builtin)),
// Otherwise at the end.
None => ordered.push((name, builtin)),
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could maybe fail if there were multiple levels of builtins, eg:

A { builtin: C},
B { }
C { builtin: B }

would get sorted to

C { builtin: B }
A { builtin: C},
B { }

That seems pretty pathological for custom types and I think it's probably fine to ignore. How about adding a note mentioning this? That way we don't repurpose the algo for a situation where it would matter.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, thanks!

ordered
.into_iter()
.map(|(n, t)| (PythonCodeOracle.class_name(n), t))
.collect()
}
}

#[derive(Template)]
Expand Down
6 changes: 0 additions & 6 deletions uniffi_bindgen/src/bindings/python/templates/CustomType.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
{%- match python_config.custom_types.get(name.as_str()) %}
{% when None %}
{#- No custom type config, just forward all methods to our builtin type #}
# Type alias
{{ type_name }} = {{ builtin|type_name }}

class _UniffiConverterType{{ name }}:
@staticmethod
def write(value, buf):
Expand Down Expand Up @@ -35,9 +32,6 @@ def lower(value):
{%- else %}
{%- endmatch %}

# Type alias
{{ type_name }} = {{ builtin|type_name }}

{#- Custom type config supplied, use it to convert the builtin type #}
class _UniffiConverterType{{ name }}:
@staticmethod
Expand Down
8 changes: 8 additions & 0 deletions uniffi_bindgen/src/bindings/python/templates/Types.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,11 @@
{%- else %}
{%- endmatch %}
{%- endfor %}

{#-
Setup type aliases for our custom types, has complications due to
forward type references, #2067
-#}
{%- for (name, ty) in self.get_custom_type_aliases() %}
{{ name }} = {{ ty|type_name }}
{%- endfor %}