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

Support Rust raw identifiers in function names #299

Open
boozook opened this issue Nov 2, 2024 · 1 comment
Open

Support Rust raw identifiers in function names #299

boozook opened this issue Nov 2, 2024 · 1 comment
Labels
good first issue Good for newcomers

Comments

@boozook
Copy link

boozook commented Nov 2, 2024

#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        fn r#enum();
    }
}
@chinedufn
Copy link
Owner

Notes for anyone that may wish to implement support for this.

Implementation Guide

Background Reading

Read the adding-support-for-a-signature chapter in the swift-bridge book:

# Adding support for a signature
Bridge modules expose Rust and Swift functions by declaring their function signatures.
For example, in the following bridge module we declare one Swift and one Rust function signature.
```rust
#[swift_bridge::bridge]
mod ffi {
extern "Rust" {
async fn add(a: u8, b: u16) -> u32;
}
extern "Swift" {
type Counter;
fn increment(&mut self);
}
}
```
Not all signatures are supported. For example, the following would not compile:
```rust
// This does not compile
#[swift_bridge::bridge]
mod ffi {
extern "Rust" {
async fn print_cow(cow: Cow<'static, str>);
}
}
```
`swift-bridge` does not currently support the `Cow<'static, str>`, so the `print_cow` function signature is unsupported.
This chapter shows how to add support for an unsupported signature.
## Implementing Support for a Signature
To support a new signature, we first write automated tests for the signature and then implement just enough code to get
those
tests passing.
Add the time of writing, the `Swift` programming language does not have support for 128 bit integers.
Let's pretend that `Swift` gained support for them and we were tasked with supporting `u128` argument and return types
in `swift-bridge` function signatures.
```rust
#[swift_bridge::bridge]
mod ffi {
extern "Rust" {
fn reflect_u128(num: u128);
}
}
fn reflect_u128(num: u128) -> num {
num
}
```
#### Integration Tests
Our first step would be to add an integration test where we declared this signature in
a bridge module and called the function from Rust.
We would first find a good place in `crates/swift-integration-tests` to declare the signature.
[`crates/swift-integration-tests/src/primitive.rs`](https://github.com/chinedufn/swift-bridge/blob/master/crates/swift-integration-tests/src/primitive.rs)
would be a good choice.
Before adding our `u128` support, the file looks like this:
```rust
#[swift_bridge::bridge]
mod ffi {
extern "Rust" {
fn test_rust_calls_swift_primitives();
fn rust_double_u8(arg: u8) -> u8;
fn rust_double_i8(arg: i8) -> i8;
fn rust_double_u16(arg: u16) -> u16;
fn rust_double_i16(arg: i16) -> i16;
fn rust_double_u32(arg: u32) -> u32;
// ... snip ...
```
Next we would add our `reflect_u128` function to the bridge module.
We would then modify the `SwiftRustIntegrationTestRunner` to call our function.
In this case we would want to
modify [`SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/PrimitiveTests.swift`](https://github.com/chinedufn/swift-bridge/blob/master/SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/PrimitiveTests.swift),
which before our updates looks something like:
```swift
import XCTest
@testable import SwiftRustIntegrationTestRunner
/// Tests for generic types such as `type SomeType<u32>`
class PrimitiveTests: XCTestCase {
/// Run tests where Rust calls Swift functions that take primitive args.
func testRustCallsSwiftPrimitives() throws {
test_rust_calls_swift_primitives()
}
/// Run tests where Swift calls Rust functions that take primitive args.
func testSwiftCallsRustPrimitives() throws {
XCTAssertEqual(rust_double_u8(10), 20);
XCTAssertEqual(rust_double_i8(10), 20);
XCTAssertEqual(rust_double_u16(10), 20);
XCTAssertEqual(rust_double_i16(10), 20);
XCTAssertEqual(rust_double_u32(10), 20);
XCTAssertEqual(rust_double_i32(10), 20);
XCTAssertEqual(rust_double_u64(10), 20);
XCTAssertEqual(rust_double_i64(10), 20);
XCTAssertEqual(rust_double_f32(10.0), 20.0);
XCTAssertEqual(rust_double_f64(10.0), 20.0);
XCTAssertEqual(rust_negate_bool(true), false);
XCTAssertEqual(rust_negate_bool(false), true);
}
}
```
#### Codegen Tests
After adding one or more integration tests, we would then add one or more codegen tests.
Codegen tests live in `crates/swift-bridge-ir/src/codegen/codegen_tests`.
In codegen tests we write out the exact code that we expect `swift-bridge` to generate.
For example, here is the codegen test for supporting `Option<u8>` in Rust function arguments.
```rust
// Copied from: crates/swift-bridge-ir/src/codegen/codegen_tests/option_codegen_tests.rs
/// Test code generation for Rust function that accepts and returns an Option<T> where T is a
/// primitive.
mod extern_rust_fn_option_primitive {
use super::*;
fn bridge_module_tokens() -> TokenStream {
quote! {
mod ffi {
extern "Rust" {
fn some_function (arg: Option<u8>) -> Option<f32>;
}
}
}
}
fn expected_rust_tokens() -> ExpectedRustTokens {
ExpectedRustTokens::Contains(quote! {
#[export_name = "__swift_bridge__$some_function"]
pub extern "C" fn __swift_bridge__some_function(
arg: swift_bridge::option::OptionU8
) -> swift_bridge::option::OptionF32 {
if let Some(val) = super::some_function(
if arg.is_some {
Some(arg.val)
} else {
None
}
) {
swift_bridge::option::OptionF32 { val, is_some: true}
} else {
swift_bridge::option::OptionF32 { val: 123.4, is_some: false}
}
}
})
}
fn expected_swift_code() -> ExpectedSwiftCode {
ExpectedSwiftCode::ContainsAfterTrim(
r#"
func some_function(_ arg: Optional<UInt8>) -> Optional<Float> {
{ let val = __swift_bridge__$some_function({ let val = arg; return __private__OptionU8(val: val ?? 123, is_some: val != nil); }()); if val.is_some { return val.val } else { return nil } }()
}
"#,
)
}
const EXPECTED_C_HEADER: ExpectedCHeader = ExpectedCHeader::ExactAfterTrim(
r#"
struct __private__OptionF32 __swift_bridge__$some_function(struct __private__OptionU8 arg);
"#,
);
#[test]
fn extern_rust_fn_option_primitive() {
CodegenTest {
bridge_module: bridge_module_tokens().into(),
expected_rust_tokens: expected_rust_tokens(),
expected_swift_code: expected_swift_code(),
expected_c_header: EXPECTED_C_HEADER,
}
.test();
}
}
```
#### Passing Tests
After writing our integration and codegen tests we would add just enough code to make them pass.
This would involve modifying `crates/swift-bridge-ir/src/bridged_type.rs` until all of our tests passed.

Integration Test

In crates/swift-bridge-integration-tests creates a mod raw_identifier:

mod expose_opaque_rust_type;
mod import_opaque_swift_class;
mod async_function;
mod boxed_functions;
mod conditional_compilation;
mod generics;
mod option;
mod pointer;
mod primitive;
mod result;
mod rust_function_uses_opaque_swift_type;
mod shared_types;
mod single_representation_type_elision;
mod slice;
mod string;
mod swift_function_uses_opaque_rust_type;
mod swift_function_uses_opaque_swift_type;
mod tuple;
mod vec;
mod enum_attributes;
mod function_attributes;
mod opaque_type_attributes;
mod struct_attributes;
mod futures_experiment;
mod argument_attributes;

Add a bridge module that uses a raw identifier function name:

#[swift_bridge::bridge]
mod ffi {
    fn r#enum();
}

Add a RawIdentifierTests.swift to the Swift integration tests:

https://github.com/chinedufn/swift-bridge/tree/27246447b4ac741b8bf086d3dfca15b47558e44f/SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests

Add an integration test that calls the r#enum function`.

Codegen Tests

Add a mod raw_identifier to the codegen tests:

mod already_declared_attribute_codegen_tests;
mod argument_label_codegen_tests;
mod async_function_codegen_tests;
mod boxed_fnonce_codegen_tests;
mod built_in_tuple_codegen_tests;
mod c_header_declaration_order_codegen_tests;
mod conditional_compilation_codegen_tests;
mod derive_attribute_codegen_tests;
mod derive_struct_attribute_codegen_tests;
mod extern_rust_function_opaque_rust_type_argument_codegen_tests;
mod extern_rust_function_opaque_rust_type_return_codegen_tests;
mod extern_rust_method_swift_class_placement_codegen_tests;
mod function_attribute_codegen_tests;
mod generic_opaque_rust_type_codegen_tests;
mod opaque_rust_type_codegen_tests;
mod opaque_swift_type_codegen_tests;
mod option_codegen_tests;
mod result_codegen_tests;
mod return_into_attribute_codegen_tests;
mod single_representation_type_elision_codegen_tests;
mod string_codegen_tests;
mod transparent_enum_codegen_tests;
mod transparent_struct_codegen_tests;
mod vec_codegen_tests;

Add a test case that has a fn r#enum

#[swift_bridge::bridge]
mod ffi {
    fn r#enum();
}

Assert that the generated Swift code uses backticks to escape the name enum:

func `enum`() {
}

Passing Tests

When generating the Swift function check if the function name is a Swift keyword (i.e. enum is a Swift keyword).

If so, surround the name with backticks since Swift uses backticks to escape.

Instead of inlining this name generation code in generate_function_swift_calls_rust can add a method to the ParsedExternFn that handles selecting the name and then replace this code with a call to the method:

Here is where the ParsedExternFn is defined:

/// A method or associated function associated with a type.
///
/// fn bar (&self);
/// fn buzz (self: &Foo) -> u8;
///
/// #\[swift_bridge(init)\]
/// fn new () -> Foo;
///
/// ... etc
pub(crate) struct ParsedExternFn {
pub func: ForeignItemFn,
/// The type that this function is associated to.
///
/// ```
/// # const _: &str = stringify!(
/// #[swift_bridge::bridge]
/// mod ffi {
/// extern "Rust" {
/// type SomeType;
///
/// // This function is associated to `SomeType` since it has a receiver `&self`.
/// fn some_function(&self);
/// }
/// }
/// # );
/// ```
pub associated_type: Option<TypeDeclaration>,
pub host_lang: HostLang,
/// Whether or not this function is a Swift initializer.
pub is_swift_initializer: bool,
/// Whether or not this function is a Swift failable initializer.
/// For more details, see:
/// [Swift Documentation - Failable Initializers](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/initialization/#Failable-Initializers)
pub swift_failable_initializer: Option<FailableInitializerType>,
/// Whether or not this function should be used for the associated type's Swift
/// `Identifiable` protocol implementation.
pub is_swift_identifiable: bool,
pub rust_name_override: Option<syn::LitStr>,
pub swift_name_override: Option<syn::LitStr>,
/// If true, we call `.into()` on the expression that the function returns before returning it.
///
/// ```no_run,ignore
/// // Declaration
/// fn some_function() -> SomeType;
///
/// // Approximate generated Code
/// extern "C" fn some_function() -> SomeType {
/// super::some_function().into()
/// }
/// ```
pub return_into: bool,
pub return_with: Option<Path>,
/// Call `.into()` before passing this argument to the function that handles it.
///
/// ```no_run,ignore
/// // Declaration
/// #[swift_bridge(args_into = (some_arg, another_arg)]
/// fn some_function(some_arg: u8, another_arg: MyStruct);
///
/// // Approximate generated code
/// extern "C" fn some_function(some_arg: u8, another_arg: MyStruct) {
/// super::some_function(some_arg, another_arg.into())
/// }
/// ```
pub args_into: Option<Vec<Ident>>,
/// Get one of the associated type's fields
pub get_field: Option<GetField>,
pub argument_labels: HashMap<Ident, LitStr>,
}

@chinedufn chinedufn added the good first issue Good for newcomers label Nov 2, 2024
@chinedufn chinedufn changed the title Support raw idents Support Rust raw identifiers in function names Nov 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
good first issue Good for newcomers
Projects
None yet
Development

No branches or pull requests

2 participants