Skip to content

Commit bb06cb3

Browse files
authored
Swift: Use root object protocol conformance (#2671)
* Use root object protocol conformance In Swift, extension protocol conformance cannot be made public – it’s tied to the original module (and conformance can’t be re-declared later). This has implications for cases where types from this library are part of the public API of another library. For example, in the following setup: ``` +-----------------------------------------------------------+ | Application | | (calls public API of Wrapper Library) | | | | +---------------------------------------------+ | | | FooLibrary (Library A) | | | | - Exports select Rust types | | | | - Associated some free-floating functions | | | | with types using `public extension` | | | | +-------------------------------------+ | | | | | UniFFIWrapper | | | | +-------------------------------------+ | | | +---------------------------------------------+ | | | +-----------------------------------------------------------+ ``` Extension-based Protocol Conformances in `UniFFIWrapper` cannot be made `public` (Swift will say `'public' modifier cannot be used with extensions that declare protocol conformances`). To avoid breaking encapsulation (and thus needing `Application` to import both `FooLibrary` and `UniFFIWrapper` the protocol conformance must be part of the type declaration which _is_ public. * Add Equatable, Hashable note * Move `Sendable` conformance back to an extension Because [`Sendable` is a marker protocol](https://www.swift.org/migration/documentation/swift-6-concurrency-migration-guide/commonproblems/#Retroactive-Sendable-Conformance), the compiler doesn’t need access to any implementation details – that means it can be defined in an extension and still works outside that module. You can trivially test this by making an `Application`-level object (see the top-level PR description for details) with a signature like:: ```swift struct TestObject: Sendable { let string: String let member: MyObject } ``` If the `Sendable` conformance wasn’t exported correctly, you’d get a compiler warning that “TestObject is not Sendable” but this doesn’t happen. * Allow objects to use Rust debug_fmt + display_fmt * Fix bindings
1 parent 3659f06 commit bb06cb3

File tree

6 files changed

+247
-110
lines changed

6 files changed

+247
-110
lines changed

uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,192 @@ impl Config {
284284
pub fn link_frameworks(&self) -> Vec<String> {
285285
self.link_frameworks.clone()
286286
}
287+
288+
/// Does the given Record have protocol conformances to list?
289+
///
290+
/// This isn't the most efficient way to do this, but it should be fast enough.
291+
pub fn record_has_conformances(&self, rec: &Record, contains_object_references: &bool) -> bool {
292+
!self
293+
.conformance_list_for_record(rec, contains_object_references)
294+
.is_empty()
295+
}
296+
297+
/// Programmatically generate the conformances for Record
298+
pub fn conformance_list_for_record(
299+
&self,
300+
rec: &Record,
301+
contains_object_references: &bool,
302+
) -> String {
303+
let mut conformances = vec![];
304+
305+
let uniffi_trait_methods = rec.uniffi_trait_methods();
306+
307+
// We auto-generate `Equatable, Hashable`, but only if we have no objects. We could do better - see #2409
308+
if !contains_object_references || uniffi_trait_methods.eq_eq.is_some() {
309+
conformances.push("Equatable");
310+
}
311+
312+
if !contains_object_references || uniffi_trait_methods.hash_hash.is_some() {
313+
conformances.push("Hashable");
314+
}
315+
316+
if uniffi_trait_methods.ord_cmp.is_some() {
317+
conformances.push("Comparable");
318+
}
319+
320+
if uniffi_trait_methods.debug_fmt.is_some() {
321+
conformances.push("CustomDebugStringConvertible");
322+
}
323+
324+
if uniffi_trait_methods.display_fmt.is_some() {
325+
conformances.push("CustomStringConvertible");
326+
}
327+
328+
// Objects can't be Codable at the moment, so we can't derive `Codable` conformance if this Record references one
329+
if !contains_object_references && self.generate_codable_conformance() {
330+
conformances.push("Codable");
331+
}
332+
333+
conformances.join(", ")
334+
}
335+
336+
/// Does the given Enum have protocol conformances to list?
337+
///
338+
/// This isn't the most efficient way to do this, but it should be fast enough.
339+
pub fn enum_has_conformances(&self, e: &Enum, contains_object_references: &bool) -> bool {
340+
!self
341+
.conformance_list_for_enum(e, contains_object_references)
342+
.is_empty()
343+
}
344+
345+
/// Programmatically generate the conformances for an Enum
346+
pub fn conformance_list_for_enum(&self, e: &Enum, contains_object_references: &bool) -> String {
347+
let uniffi_trait_methods = e.uniffi_trait_methods();
348+
349+
let mut conformances = vec![];
350+
351+
// We auto-generate `Equatable, Hashable`, but only if we have no objects. We could do better - see #2409
352+
if !contains_object_references || uniffi_trait_methods.eq_eq.is_some() {
353+
conformances.push("Equatable");
354+
}
355+
356+
if !contains_object_references || uniffi_trait_methods.hash_hash.is_some() {
357+
conformances.push("Hashable");
358+
}
359+
360+
if uniffi_trait_methods.ord_cmp.is_some() {
361+
conformances.push("Comparable");
362+
}
363+
364+
if uniffi_trait_methods.debug_fmt.is_some() {
365+
conformances.push("CustomDebugStringConvertible");
366+
}
367+
368+
if uniffi_trait_methods.display_fmt.is_some() {
369+
conformances.push("CustomStringConvertible");
370+
}
371+
372+
// Objects can't be Codable at the moment, so we can't derive `Codable` conformance if this Enum references one
373+
if !contains_object_references && self.generate_codable_conformance() {
374+
conformances.push("Codable");
375+
}
376+
377+
if self.generate_case_iterable_conformance() && !e.contains_variant_fields() {
378+
conformances.push("CaseIterable");
379+
}
380+
381+
conformances.join(", ")
382+
}
383+
384+
/// Does the given Error have protocol conformances to list? (aside from the default `Swift.Error`)
385+
///
386+
/// This isn't the most efficient way to do this, but it should be fast enough.
387+
pub fn error_has_additional_conformances(
388+
&self,
389+
e: &Enum,
390+
contains_object_references: &bool,
391+
) -> bool {
392+
!self
393+
.additional_conformance_list_for_error(e, contains_object_references)
394+
.is_empty()
395+
}
396+
397+
/// Programmatically generate the additional conformances for an Error (aside from the default `Swift.Error`)
398+
pub fn additional_conformance_list_for_error(
399+
&self,
400+
e: &Enum,
401+
contains_object_references: &bool,
402+
) -> String {
403+
let uniffi_trait_methods = e.uniffi_trait_methods();
404+
405+
let mut conformances = vec![];
406+
407+
// We auto-generate `Equatable, Hashable`, but only if we have no objects. We could do better - see #2409
408+
if !contains_object_references || uniffi_trait_methods.eq_eq.is_some() {
409+
conformances.push("Equatable");
410+
}
411+
412+
if !contains_object_references || uniffi_trait_methods.hash_hash.is_some() {
413+
conformances.push("Hashable");
414+
}
415+
416+
if uniffi_trait_methods.ord_cmp.is_some() {
417+
conformances.push("Comparable");
418+
}
419+
420+
// Objects can't be Codable at the moment, so we can't derive `Codable` conformance if this Error references one
421+
if !contains_object_references && self.generate_codable_conformance() {
422+
conformances.push("Codable");
423+
}
424+
425+
if !self.omit_localized_error_conformance() {
426+
conformances.push("Foundation.LocalizedError");
427+
}
428+
429+
if self.generate_case_iterable_conformance() && !e.is_flat() && !e.contains_variant_fields()
430+
{
431+
conformances.push("CaseIterable");
432+
}
433+
434+
conformances.join(", ")
435+
}
436+
437+
/// Programmatically generate the conformances for an Object
438+
pub fn conformance_list_for_object(&self, o: &Object, is_error: &bool) -> String {
439+
let uniffi_trait_methods = o.uniffi_trait_methods();
440+
441+
let mut conformances = vec!["@unchecked Sendable"];
442+
443+
if *is_error {
444+
conformances.push("Swift.Error");
445+
446+
if !self.omit_localized_error_conformance() {
447+
conformances.push("Foundation.LocalizedError");
448+
}
449+
}
450+
451+
if uniffi_trait_methods.eq_eq.is_some() {
452+
conformances.push("Equatable");
453+
}
454+
455+
if uniffi_trait_methods.hash_hash.is_some() {
456+
conformances.push("Hashable");
457+
}
458+
459+
if uniffi_trait_methods.ord_cmp.is_some() {
460+
conformances.push("Comparable");
461+
}
462+
463+
if uniffi_trait_methods.debug_fmt.is_some() {
464+
conformances.push("CustomDebugStringConvertible");
465+
}
466+
467+
if uniffi_trait_methods.display_fmt.is_some() {
468+
conformances.push("CustomStringConvertible");
469+
}
470+
471+
conformances.join(", ")
472+
}
287473
}
288474

289475
// Given a trait, work out what the protocol name we generate for it.

uniffi_bindgen/src/bindings/swift/templates/EnumTemplate.swift

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,30 @@
44
{%- let uniffi_trait_methods = e.uniffi_trait_methods() %}
55
{% match e.variant_discr_type() %}
66
{% when None %}
7+
{%- if config.enum_has_conformances(e, contains_object_references) %}
8+
public enum {{ type_name }}: {{ config.conformance_list_for_enum(e, contains_object_references) }} {
9+
{%- else %}
710
public enum {{ type_name }} {
11+
{%- endif %}
812
{% for variant in e.variants() %}
913
{%- call swift::docstring(variant, 4) %}
1014
case {{ variant.name()|enum_variant_swift_quoted }}{% if variant.fields().len() > 0 %}(
1115
{%- call swift::field_list_decl(variant, variant.has_nameless_fields()) %}
1216
){% endif -%}
1317
{% endfor %}
1418
{% when Some(variant_discr_type) %}
15-
public enum {{ type_name }} : {{ variant_discr_type|type_name }} {
19+
public enum {{ type_name }}: {{ variant_discr_type|type_name }}, {{ config.conformance_list_for_enum(e, contains_object_references) }} {
1620
{% for variant in e.variants() %}
1721
{%- call swift::docstring(variant, 4) %}
1822
case {{ variant.name()|enum_variant_swift_quoted }} = {{ e|variant_discr_literal(loop.index0) }}{% if variant.fields().len() > 0 %}(
1923
{%- call swift::field_list_decl(variant, variant.has_nameless_fields()) %}
2024
){% endif -%}
2125
{% endfor %}
2226
{% endmatch %}
27+
28+
{% call swift::uniffi_trait_impls(uniffi_trait_methods) %}
2329
}
30+
2431
#if compiler(>=6)
2532
extension {{ type_name }}: Sendable {}
2633
#endif
@@ -85,21 +92,3 @@ public func {{ ffi_converter_name }}_lift(_ buf: RustBuffer) throws -> {{ type_n
8592
public func {{ ffi_converter_name }}_lower(_ value: {{ type_name }}) -> RustBuffer {
8693
return {{ ffi_converter_name }}.lower(value)
8794
}
88-
89-
{% call swift::uniffi_trait_impls(uniffi_trait_methods) %}
90-
91-
{%- if !contains_object_references %}
92-
{# We auto-generate `Equatable, Hashable`, but only if we have no objects. We could do better - see #2409 #}
93-
{% if !(contains_object_references || uniffi_trait_methods.eq_eq.is_some() || uniffi_trait_methods.hash_hash.is_some()) %}
94-
extension {{ type_name }}: Equatable, Hashable {}
95-
{% endif %}
96-
97-
{# Not clear if `generate_codable_conformance` actually needs to avoid objects too? #}
98-
{%- if config.generate_codable_conformance() %}
99-
extension {{ type_name }}: Codable {}
100-
{%- endif %}
101-
{% endif %}
102-
103-
{% if config.generate_case_iterable_conformance() && !e.contains_variant_fields() %}
104-
extension {{ type_name }}: CaseIterable {}
105-
{% endif %}

uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
{%- call swift::docstring(e, 0) %}
2+
{%- if config.error_has_additional_conformances(e, contains_object_references) %}
3+
public enum {{ type_name }}: Swift.Error, {{ config.additional_conformance_list_for_error(e, contains_object_references) }} {
4+
{%- else %}
25
public enum {{ type_name }}: Swift.Error {
6+
{%- endif %}
37

48
{% if e.is_flat() %}
59
{% for variant in e.variants() %}
@@ -16,8 +20,19 @@ public enum {{ type_name }}: Swift.Error {
1620
{% endfor %}
1721

1822
{%- endif %}
23+
24+
{% call swift::uniffi_trait_impls(e.uniffi_trait_methods()) %}
25+
26+
{% if !config.omit_localized_error_conformance() %}
27+
public var errorDescription: String? {
28+
String(reflecting: self)
29+
}
30+
{% endif %}
1931
}
2032

33+
#if compiler(>=6)
34+
extension {{ type_name }}: Sendable {}
35+
#endif
2136

2237
#if swift(>=5.8)
2338
@_documentation(visibility: private)
@@ -104,22 +119,3 @@ public func {{ ffi_converter_name }}_lift(_ buf: RustBuffer) throws -> {{ type_n
104119
public func {{ ffi_converter_name }}_lower(_ value: {{ type_name }}) -> RustBuffer {
105120
return {{ ffi_converter_name }}.lower(value)
106121
}
107-
108-
{% if !contains_object_references %}
109-
extension {{ type_name }}: Equatable, Hashable {}
110-
{% if config.generate_codable_conformance() %}
111-
extension {{ type_name }}: Codable {}
112-
{% endif %}
113-
{% endif %}
114-
115-
{% if !config.omit_localized_error_conformance() %}
116-
extension {{ type_name }}: Foundation.LocalizedError {
117-
public var errorDescription: String? {
118-
String(reflecting: self)
119-
}
120-
}
121-
{% endif %}
122-
123-
{% if config.generate_case_iterable_conformance() && !e.is_flat() && !e.contains_variant_fields() %}
124-
extension {{ type_name }}: CaseIterable {}
125-
{% endif %}

uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
{% include "Protocol.swift" %}
99

1010
{%- call swift::docstring(obj, 0) %}
11-
open class {{ impl_class_name }}: {{ protocol_name }}, @unchecked Sendable {
11+
open class {{ impl_class_name }}: {{ protocol_name }}, {{ config.conformance_list_for_object(obj, is_error) }} {
1212
fileprivate let handle: UInt64
1313

1414
/// Used to instantiate a [FFIObject] without an actual handle, for fakes in tests, mostly.
@@ -66,6 +66,16 @@ open class {{ impl_class_name }}: {{ protocol_name }}, @unchecked Sendable {
6666
{% for meth in obj.methods() -%}
6767
{%- call swift::func_decl("open func", meth, 4) %}
6868
{% endfor %}
69+
70+
{% call swift::uniffi_trait_impls(obj.uniffi_trait_methods()) %}
71+
72+
{%- if is_error %}
73+
{% if !config.omit_localized_error_conformance() %}
74+
public var errorDescription: String? {
75+
String(reflecting: self)
76+
}
77+
{% endif %}
78+
{% endif %}
6979
}
7080

7181
{%- if !obj.has_callback_interface() %}
@@ -150,17 +160,10 @@ public struct {{ ffi_converter_name }}: FfiConverter {
150160

151161
{%- endif %}
152162

153-
{% call swift::uniffi_trait_impls(obj.uniffi_trait_methods()) %}
154-
155-
{%- if is_error %}
156-
extension {{ impl_class_name }}: Swift.Error {}
157-
{% endif %}
158-
159163
{%- for t in obj.trait_impls() %}
160164
extension {{impl_class_name}}: {{ self::trait_protocol_name(ci, t.trait_ty)? }} {}
161165
{% endfor %}
162166

163-
164167
{#
165168
We always write these public functions just in case the object is used as
166169
an external type by another crate.
@@ -182,14 +185,6 @@ public func {{ ffi_converter_name }}_lower(_ value: {{ type_name }}) -> UInt64 {
182185
{# Objects as error #}
183186
{%- if is_error %}
184187

185-
{% if !config.omit_localized_error_conformance() %}
186-
extension {{ type_name }}: Foundation.LocalizedError {
187-
public var errorDescription: String? {
188-
String(reflecting: self)
189-
}
190-
}
191-
{% endif %}
192-
193188
{# Due to some mismatches in the ffi converter mechanisms, errors are a RustBuffer storing a handle #}
194189
#if swift(>=5.8)
195190
@_documentation(visibility: private)

0 commit comments

Comments
 (0)