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

[Swift] Generate open classes, methods and mocks #1918

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
26 changes: 26 additions & 0 deletions uniffi_bindgen/src/bindings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,29 @@ pub fn write_bindings(
}
Ok(())
}

pub fn write_mocks(
config: &Config,
ci: &ComponentInterface,
out_dir: &Utf8Path,
language: TargetLanguage,
try_format_code: bool,
) -> Result<()> {
match language {
TargetLanguage::Kotlin => {
return Err(anyhow::anyhow!(
"Mock generation is not supported for Kotlin"
))
}
TargetLanguage::Swift => swift::write_mocks(&config.swift, ci, out_dir, try_format_code)?,
TargetLanguage::Python => {
return Err(anyhow::anyhow!(
"Mock generation is not supported for Python"
))
}
TargetLanguage::Ruby => {
return Err(anyhow::anyhow!("Mock generation is not supported for Ruby"))
}
}
Ok(())
}
29 changes: 29 additions & 0 deletions uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,31 @@ impl<'a> TypeRenderer<'a> {
}
}

/// Generate UniFFI mocks for Swift, as strings in memory.
///
pub fn generate_mocks(config: &Config, ci: &ComponentInterface) -> Result<String> {
let mocks_renderer = MocksRenderer::new(config, ci);
let mocks = mocks_renderer
.render()
.context("failed to render Swift mocks")?;

Ok(mocks)
}

/// Renders Swift mocks helper code for all types
#[derive(Template)]
#[template(syntax = "swift", escape = "none", path = "wrapper_mocks.swift")]
pub struct MocksRenderer<'a> {
config: &'a Config,
ci: &'a ComponentInterface,
}

impl<'a> MocksRenderer<'a> {
fn new(config: &'a Config, ci: &'a ComponentInterface) -> Self {
Self { config, ci }
}
}

/// Template for generating the `.h` file that defines the low-level C FFI.
///
/// This file defines only the low-level structs and functions that are exposed
Expand Down Expand Up @@ -578,6 +603,10 @@ pub mod filters {
Ok(oracle().find(&as_type.as_type()).type_label())
}

pub fn type_is_optional(as_type: &impl AsType) -> Result<bool, askama::Error> {
Ok(matches!(&as_type.as_type(), Type::Optional { .. }))
}

pub fn canonical_name(as_type: &impl AsType) -> Result<String, askama::Error> {
Ok(oracle().find(&as_type.as_type()).canonical_name())
}
Expand Down
43 changes: 33 additions & 10 deletions uniffi_bindgen/src/bindings/swift/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use camino::Utf8Path;
use fs_err as fs;

pub mod gen_swift;
pub use gen_swift::{generate_bindings, Config};
pub use gen_swift::{generate_bindings, generate_mocks, Config};
mod test;

use super::super::interface::ComponentInterface;
Expand Down Expand Up @@ -82,16 +82,39 @@ pub fn write_bindings(
}

if try_format_code {
if let Err(e) = Command::new("swiftformat")
.arg(source_file.as_str())
.output()
{
println!(
"Warning: Unable to auto-format {} using swiftformat: {e:?}",
source_file.file_name().unwrap(),
);
}
format_code(&source_file)
}

Ok(())
}

/// Write UniFFI mocks for Swift as files on disk.
pub fn write_mocks(
config: &Config,
ci: &ComponentInterface,
out_dir: &Utf8Path,
try_format_code: bool,
) -> Result<()> {
let mocks = generate_mocks(config, ci)?;

let source_file = out_dir.join(format!("{}_mocks.swift", config.module_name()));
fs::write(&source_file, mocks)?;

if try_format_code {
format_code(&source_file)
}

Ok(())
}

fn format_code(source_file: &Utf8Path) {
if let Err(e) = Command::new("swiftformat")
.arg(source_file.as_str())
.output()
{
println!(
"Warning: Unable to auto-format {} using swiftformat: {e:?}",
source_file.file_name().unwrap(),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
{#
// Template used to generate mock function.
// if is_alternate_constructor is true, then it generates a class function.
#}

// MARK: - {{ meth.name()|fn_name }}

{# // CallsCount -#}
public {% if is_alternate_constructor %}static {% endif -%} var {% call swift::mock_var_prefix(meth) %}CallsCount = 0
public {% if is_alternate_constructor %}static {% endif -%} var {% call swift::mock_var_prefix(meth) %}Called: Bool {
return {% if is_alternate_constructor %}Self.{% endif %}{% call swift::mock_var_prefix(meth) %}CallsCount > 0
}
{% if meth.arguments().len() > 0 -%}
{# // ReceivedInvocations -#}
public {% if is_alternate_constructor %}static {% endif -%} var {% call swift::mock_var_prefix(meth) %}ReceivedInvocations: [
{%- if meth.arguments().len() > 1 -%}
({% call swift::arg_list_decl(meth) %})
{%- else -%}
{{ meth.arguments()[0]|type_name }}
{%- endif -%}
] = []
{# // ReceivedArguments -#}
public {% if is_alternate_constructor %}static {% endif -%} var {% call swift::mock_var_prefix(meth) %}
{%- if meth.arguments().len() > 1 -%}
ReceivedArguments: ({% call swift::arg_list_decl(meth) %})?
{%- else -%}
ReceivedArgument: {{ meth.arguments()[0]|type_name }}{% if meth.arguments()[0]|type_is_optional == false %}?{% endif %}
{% endif %}
{# // ThrowableError -#}
{%- endif %}
{%- if meth.throws() -%}
public {% if is_alternate_constructor %}static {% endif -%} var {% call swift::mock_var_prefix(meth) %}ThrowableError: Error?
{%- endif %}
{# // Closure -#}
public {% if is_alternate_constructor %}static {% endif -%} var {% call swift::mock_var_prefix(meth) -%}Closure: ((
{%- for arg in meth.arguments() -%}
{{ arg|type_name }}
{%- if !loop.last %}, {% endif -%}
{%- endfor -%}
) {% call swift::throws(meth) -%} -> {% match meth.return_type() %}{% when Some with (return_type) %}{{ return_type|type_name }}{% when None %}Void{% endmatch %})?
{# // ReturnValue -#}
{%- match meth.return_type() -%}
{%- when Some with (return_type) %}
public {% if is_alternate_constructor %}static {% endif -%} var {% call swift::mock_var_prefix(meth) %}ReturnValue: {{ return_type|type_name }}{% if return_type|type_is_optional == false %}!{% endif %}
{%- when None -%}
{%- endmatch %}
{# // Documentation -#}
{% call swift::docstring(meth, 4) %}
{# // Implementation -#}
{%- if is_alternate_constructor -%}
public override class func {{ meth.name()|fn_name }}({% call swift::arg_list_decl(meth) %}) {% call swift::throws(meth) %} -> {{ impl_class_name }} {
{%- else -%}
public override func {{ meth.name()|fn_name }}({%- call swift::arg_list_decl(meth) -%})
{%- if meth.is_async() %} async{% endif %} {% call swift::throws(meth) %}
{%- match meth.return_type() %}{% when Some with (return_type) -%} -> {{ return_type|type_name }}{% when None %}{% endmatch %} {
{%- endif %}
{# // Check for throwable error -#}
{% if meth.throws() -%}
if let error = {% if is_alternate_constructor %}Self.{% endif %}{% call swift::mock_var_prefix(meth) -%}ThrowableError {
throw error
}
{%- endif %}
{% if meth.arguments().len() > 0 -%}
{# // Update received arguments -#}
{%- if meth.arguments().len() > 1 -%}
{% if is_alternate_constructor %}Self.{% endif %}{% call swift::mock_var_prefix(meth) %}ReceivedArguments = (
{%- for arg in meth.arguments() -%}
{{ arg.name()|var_name }}: {{ arg.name()|var_name }}
{%- if !loop.last %}, {% endif -%}
{%- endfor -%}
)
{% else %}
{% if is_alternate_constructor %}Self.{% endif %}{% call swift::mock_var_prefix(meth) %}ReceivedArgument = {{ meth.arguments()[0].name()|var_name }}
{% endif %}
{# // Update received invocations -#}
{% if is_alternate_constructor %}Self.{% endif %}{% call swift::mock_var_prefix(meth) %}ReceivedInvocations.append(
{%- if meth.arguments().len() > 1 -%}
(
{%- for arg in meth.arguments() -%}
{{ arg.name()|var_name }}: {{ arg.name()|var_name }}
{%- if !loop.last %}, {% endif -%}
{%- endfor -%}
)
{%- else -%}
{{ meth.arguments()[0].name()|var_name }}
{%- endif -%}
)
{% endif %}
{# // Update calls count -#}
{% if is_alternate_constructor %}Self.{% endif %}{% call swift::mock_var_prefix(meth) %}CallsCount += 1
{% match meth.return_type() -%}
{%- when Some with (return_type) %}
{# // Check for closure -#}
if let closure = {% if is_alternate_constructor %}Self.{% endif %}{% call swift::mock_var_prefix(meth) %}Closure {
return {% if meth.throws() %}try {% endif -%} closure(
{%- for arg in meth.arguments() -%}
{{ arg.name()|var_name }}
{%- if !loop.last %}, {% endif -%}
{%- endfor -%}
)
}
{# // Returns the return value -#}
return {% if is_alternate_constructor %}Self.{% endif %}{% call swift::mock_var_prefix(meth) -%}ReturnValue
{%- when None %}
{# // Check for closure -#}
if let closure = {% if is_alternate_constructor %}Self.{% endif %}{% call swift::mock_var_prefix(meth) %}Closure {
{% if meth.throws() %}try {% endif -%}
closure(
{%- for arg in meth.arguments() -%}
{{ arg.name()|var_name }}
{%- if !loop.last %}, {% endif -%}
{%- endfor -%}
)
}
{%- endmatch %}
}
26 changes: 26 additions & 0 deletions uniffi_bindgen/src/bindings/swift/templates/MockTemplate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{%- let obj = ci|get_object_definition(name) %}
{%- let (protocol_name, impl_class_name) = obj|object_names %}
{%- let methods = obj.methods() %}
{%- let protocol_docstring = obj.docstring() %}

{% call swift::docstring(obj, 0) %}
public class {{ impl_class_name }}Mock: {{ impl_class_name }} {

required public init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
fatalError("Not supported")
}

public init() {
super.init(noPointer: NoPointer())
}

{%- let is_alternate_constructor = true -%}
{%- for meth in obj.alternate_constructors() -%}
{% include "MockFunctionTemplate.swift" %}
{%- endfor -%}

{%- let is_alternate_constructor = false -%}
{%- for meth in obj.methods() -%}
{% include "MockFunctionTemplate.swift" %}
{%- endfor %}
}
37 changes: 27 additions & 10 deletions uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
{% include "Protocol.swift" %}

{%- call swift::docstring(obj, 0) %}
public class {{ impl_class_name }}:
open class {{ impl_class_name }}:
{%- for tm in obj.uniffi_traits() %}
{%- match tm %}
{%- when UniffiTrait::Display { fmt } %}
Expand All @@ -21,15 +21,29 @@ public class {{ impl_class_name }}:
{%- endmatch %}
{%- endfor %}
{{ protocol_name }} {
fileprivate let pointer: UnsafeMutableRawPointer
fileprivate let pointer: UnsafeMutableRawPointer!

/// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly.
public struct NoPointer {
public init() {}
}

// TODO: We'd like this to be `private` but for Swifty reasons,
// we can't implement `FfiConverter` without making this `required` and we can't
// make it `required` without making it `public`.
required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
required public init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
self.pointer = pointer
}

/// This constructor can be used to instantiate a fake object.
/// - Parameter noPointer: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject].
///
/// - Warning:
/// Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing [Pointer] the FFI lower functions will crash.
public init(noPointer: NoPointer) {
self.pointer = nil
}

public func uniffiClonePointer() -> UnsafeMutableRawPointer {
return try! rustCall { {{ obj.ffi_object_clone().name() }}(self.pointer, $0) }
}
Expand All @@ -44,12 +58,15 @@ public class {{ impl_class_name }}:
{%- endmatch %}

deinit {
guard let pointer = self.pointer else {
return
}
try! rustCall { {{ obj.ffi_object_free().name() }}(pointer, $0) }
}

{% for cons in obj.alternate_constructors() %}
{%- call swift::docstring(cons, 4) %}
public static func {{ cons.name()|fn_name }}({% call swift::arg_list_decl(cons) %}) {% call swift::throws(cons) %} -> {{ impl_class_name }} {
open class 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) %})
}

Expand All @@ -59,7 +76,7 @@ public class {{ impl_class_name }}:
{% for meth in obj.methods() -%}
{%- if meth.is_async() %}
{%- call swift::docstring(meth, 4) %}
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 %} {
open 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 %} {
return {% call swift::try(meth) %} await uniffiRustCallAsync(
rustFutureFunc: {
{{ meth.ffi_func().name() }}(
Expand Down Expand Up @@ -94,15 +111,15 @@ public class {{ impl_class_name }}:

{%- when Some with (return_type) %}
{%- call swift::docstring(meth, 4) %}
public func {{ meth.name()|fn_name }}({% call swift::arg_list_decl(meth) %}) {% call swift::throws(meth) %} -> {{ return_type|type_name }} {
open func {{ meth.name()|fn_name }}({% call swift::arg_list_decl(meth) %}) {% call swift::throws(meth) %} -> {{ return_type|type_name }} {
return {% call swift::try(meth) %} {{ return_type|lift_fn }}(
{% call swift::to_ffi_call_with_prefix("self.uniffiClonePointer()", meth) %}
)
}

{%- when None %}
{%- call swift::docstring(meth, 4) %}
public func {{ meth.name()|fn_name }}({% call swift::arg_list_decl(meth) %}) {% call swift::throws(meth) %} {
open func {{ meth.name()|fn_name }}({% call swift::arg_list_decl(meth) %}) {% call swift::throws(meth) %} {
{% call swift::to_ffi_call_with_prefix("self.uniffiClonePointer()", meth) %}
}

Expand All @@ -113,13 +130,13 @@ public class {{ impl_class_name }}:
{%- for tm in obj.uniffi_traits() %}
{%- match tm %}
{%- when UniffiTrait::Display { fmt } %}
public var description: String {
open var description: String {
return {% call swift::try(fmt) %} {{ fmt.return_type().unwrap()|lift_fn }}(
{% call swift::to_ffi_call_with_prefix("self.uniffiClonePointer()", fmt) %}
)
}
{%- when UniffiTrait::Debug { fmt } %}
public var debugDescription: String {
open var debugDescription: String {
return {% call swift::try(fmt) %} {{ fmt.return_type().unwrap()|lift_fn }}(
{% call swift::to_ffi_call_with_prefix("self.uniffiClonePointer()", fmt) %}
)
Expand All @@ -131,7 +148,7 @@ public class {{ impl_class_name }}:
)
}
{%- when UniffiTrait::Hash { hash } %}
public func hash(into hasher: inout Hasher) {
open func hash(into hasher: inout Hasher) {
let val = {% call swift::try(hash) %} {{ hash.return_type().unwrap()|lift_fn }}(
{% call swift::to_ffi_call_with_prefix("self.uniffiClonePointer()", hash) %}
)
Expand Down
7 changes: 7 additions & 0 deletions uniffi_bindgen/src/bindings/swift/templates/macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@
{%- endfor %}
{%- endmacro %}

{% macro mock_var_prefix(func) -%}
{{ func.name()|fn_name }}
{%- for arg in func.arguments() -%}
{{ arg.name()|var_name|capitalize }}
{%- endfor %}
{%- endmacro -%}

{#-
// Field lists as used in Swift declarations of Records and Enums.
// Note the var_name and type_name filters.
Expand Down
Loading