From 296bb6ffa303b7983c5af25d517c5e99a2fdae6e Mon Sep 17 00:00:00 2001 From: Jonathan Tanner <> Date: Wed, 8 Oct 2025 23:37:59 +0100 Subject: [PATCH 1/2] FIXES #335 --- crates/swift-bridge-ir/src/codegen/generate_swift.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/swift-bridge-ir/src/codegen/generate_swift.rs b/crates/swift-bridge-ir/src/codegen/generate_swift.rs index 48e67988..39c206c7 100644 --- a/crates/swift-bridge-ir/src/codegen/generate_swift.rs +++ b/crates/swift-bridge-ir/src/codegen/generate_swift.rs @@ -141,7 +141,7 @@ impl SwiftBridgeModule { if ty.attributes.sendable { let ty_name = ty.ty_name_string(); - swift += &format!("extension {ty_name}: @unchecked Sendable {{}}") + swift += &format!("extension {ty_name}: @unchecked Sendable {{}}\n") } } HostLang::Swift => { From 755d0a4a993dc49d51c418691b1464a91d958b3c Mon Sep 17 00:00:00 2001 From: Jonathan Tanner <> Date: Fri, 10 Oct 2025 10:00:53 +0100 Subject: [PATCH 2/2] FIXES #334 --- .../OpaqueRustStructTests.swift | 43 ++++++++++++++++++- .../codegen/codegen_tests/opaque_rust_type.rs | 20 ++++++++- .../src/codegen/generate_c_header.rs | 19 +++++--- .../generate_swift/opaque_copy_type.rs | 26 +++++++++-- crates/swift-integration-tests/build.rs | 10 ++--- .../src/opaque_type_attributes/hashable.rs | 15 +++++++ 6 files changed, 115 insertions(+), 18 deletions(-) diff --git a/SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/OpaqueRustStructTests.swift b/SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/OpaqueRustStructTests.swift index db09142f..244a83de 100644 --- a/SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/OpaqueRustStructTests.swift +++ b/SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/OpaqueRustStructTests.swift @@ -136,7 +136,48 @@ class OpaqueRustStructTests: XCTestCase { var table: [RustHashableType: String] = [:] table[val1] = "hello" table[val2] = "world" - + + //Should not be overwritten + if let element = table[val1] { + XCTAssertEqual(element, "hello") + }else { + XCTFail() + } + if let element = table[val2] { + XCTAssertEqual(element, "world") + }else { + XCTFail() + } + } + } + + func testOpaqueRustCopyTypeImplHashable() throws { + XCTContext.runActivity(named: "Same hash value"){ + _ in + let val1 = RustCopyHashableType(10) + let val2 = RustCopyHashableType(10) + + var table: [RustCopyHashableType: String] = [:] + table[val1] = "hello" + table[val2] = "world" + + //Should be overwritten. + if let element = table[val1] { + XCTAssertEqual(element, "world") + }else { + XCTFail() + } + } + + XCTContext.runActivity(named: "Not same hash value"){ + _ in + let val1 = RustCopyHashableType(10) + let val2 = RustCopyHashableType(100) + + var table: [RustCopyHashableType: String] = [:] + table[val1] = "hello" + table[val2] = "world" + //Should not be overwritten if let element = table[val1] { XCTAssertEqual(element, "hello") diff --git a/crates/swift-bridge-ir/src/codegen/codegen_tests/opaque_rust_type.rs b/crates/swift-bridge-ir/src/codegen/codegen_tests/opaque_rust_type.rs index 7b24656e..fb2460ad 100644 --- a/crates/swift-bridge-ir/src/codegen/codegen_tests/opaque_rust_type.rs +++ b/crates/swift-bridge-ir/src/codegen/codegen_tests/opaque_rust_type.rs @@ -395,10 +395,12 @@ void __swift_bridge__$SomeType$some_method_ref(struct __swift_bridge__$SomeType } } -/// Verify that we properly generate an Equatable extension for a Copy opaque Rust type. +/// Verify that we properly generate Equatable and Hashable extensions for a Copy opaque Rust type. /// /// In May 2025 it was discovered that the Swift `Equatable` protocol was not being implemented for /// Rust `Copy` types. This test case confirms tht we emit an `Equatable` protocol implementation. +/// Furthermore, in October 2025 it was discovered that the Swift `Hashable` protocol was not being implemented for +/// Rust `Copy` types. This test case confirms tht we emit an `Hashable` protocol implementation. mod extern_rust_copy_type_equatable { use super::*; @@ -406,7 +408,7 @@ mod extern_rust_copy_type_equatable { quote! { mod ffi { extern "Rust" { - #[swift_bridge(Copy(16), Equatable)] + #[swift_bridge(Copy(16), Equatable, Hashable)] type SomeType; } } @@ -434,6 +436,17 @@ extension SomeType: Equatable { }) } } + +extension SomeType: Hashable { + public func hash(into hasher: inout Hasher){ + var this = self + return withUnsafePointer(to: &this.bytes, {(ptr: UnsafePointer<__swift_bridge__$SomeType>) in + hasher.combine(__swift_bridge__$SomeType$_hash( + UnsafeMutablePointer(mutating: ptr) + )) + }) + } +} "#, ) } @@ -446,6 +459,9 @@ extension SomeType: Equatable { "#, r#" bool __swift_bridge__$SomeType$_partial_eq(__swift_bridge__$SomeType* lhs, __swift_bridge__$SomeType* rhs); +"#, + r#" +uint64_t __swift_bridge__$SomeType$_hash(__swift_bridge__$SomeType* self); "#, ]) } diff --git a/crates/swift-bridge-ir/src/codegen/generate_c_header.rs b/crates/swift-bridge-ir/src/codegen/generate_c_header.rs index ab0cab59..c327827a 100644 --- a/crates/swift-bridge-ir/src/codegen/generate_c_header.rs +++ b/crates/swift-bridge-ir/src/codegen/generate_c_header.rs @@ -270,12 +270,6 @@ typedef struct {option_ffi_name} {{ bool is_some; {ffi_name} val; }} {option_ffi if ty.attributes.declare_generic { continue; } - if ty.attributes.hashable { - let ty_name = ty.ty_name_ident(); - let hash_ty = - format!("uint64_t __swift_bridge__${}$_hash(void* self);", ty_name); - header += &hash_ty; - } let ty_name = ty.to_string(); if let Some(copy) = ty.attributes.copy { @@ -333,6 +327,19 @@ typedef struct {option_ffi_name} {{ bool is_some; {ffi_name} val; }} {option_ffi header += &equal_ty; header += "\n"; } + if ty.attributes.hashable { + let ty_name = ty.ty_name_ident(); + let hash_ty = format!( + "uint64_t __swift_bridge__${ty_name}$_hash({c_ffi_type}* self);", + ty_name = ty_name, + c_ffi_type = if ty.attributes.copy.is_some() { + &ty.ffi_repr_name_string() + } else { + "void" + }, + ); + header += &hash_ty; + } // TODO: Support Vec. Add codegen tests and then // make them pass. diff --git a/crates/swift-bridge-ir/src/codegen/generate_swift/opaque_copy_type.rs b/crates/swift-bridge-ir/src/codegen/generate_swift/opaque_copy_type.rs index 2d3136e1..b8ce38e6 100644 --- a/crates/swift-bridge-ir/src/codegen/generate_swift/opaque_copy_type.rs +++ b/crates/swift-bridge-ir/src/codegen/generate_swift/opaque_copy_type.rs @@ -137,13 +137,31 @@ extension {type_name}: Equatable {{ String::new() }; + let ext_hashable = if ty.attributes.hashable { + format!( + r#" +extension {type_name}: Hashable {{ + public func hash(into hasher: inout Hasher){{ + var this = self + return withUnsafePointer(to: &this.bytes, {{(ptr: UnsafePointer<{ffi_repr_name}>) in + hasher.combine(__swift_bridge__${type_name}$_hash( + UnsafeMutablePointer(mutating: ptr) + )) + }}) + }} +}} +"#, + type_name = type_name, + ffi_repr_name = ty.ffi_repr_name_string() + ) + } else { + String::new() + }; + format!( r#"{declare_struct} {ffi_repr_conversion} -{ext_equatable}"#, - declare_struct = declare_struct, - ffi_repr_conversion = ffi_repr_conversion, - ext_equatable = ext_equatable, +{ext_equatable}{ext_hashable}"#, ) } diff --git a/crates/swift-integration-tests/build.rs b/crates/swift-integration-tests/build.rs index cc519062..e8e0fa62 100644 --- a/crates/swift-integration-tests/build.rs +++ b/crates/swift-integration-tests/build.rs @@ -1,11 +1,11 @@ -use std::path::PathBuf; +use std::{ffi::OsStr, path::PathBuf}; fn main() { let out_dir = "../../SwiftRustIntegrationTestRunner/Generated"; let out_dir = PathBuf::from(out_dir); let mut bridges = vec![]; - read_files_recursive(PathBuf::from("src"), &mut bridges); + read_files_recursive(PathBuf::from("src"), &mut bridges, OsStr::new("rs")); for path in &bridges { println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); @@ -15,12 +15,12 @@ fn main() { .write_all_concatenated(out_dir, env!("CARGO_PKG_NAME")); } -fn read_files_recursive(dir: PathBuf, files: &mut Vec) { +fn read_files_recursive(dir: PathBuf, files: &mut Vec, extension: &OsStr) { for entry in std::fs::read_dir(dir).unwrap() { let path = entry.unwrap().path(); if path.is_dir() { - read_files_recursive(path, files); - } else { + read_files_recursive(path, files, extension); + } else if path.extension() == Some(extension) { files.push(path) } } diff --git a/crates/swift-integration-tests/src/opaque_type_attributes/hashable.rs b/crates/swift-integration-tests/src/opaque_type_attributes/hashable.rs index fc44d144..067892d2 100644 --- a/crates/swift-integration-tests/src/opaque_type_attributes/hashable.rs +++ b/crates/swift-integration-tests/src/opaque_type_attributes/hashable.rs @@ -6,6 +6,12 @@ mod ffi { #[swift_bridge(init)] fn new(num: isize) -> RustHashableType; + + #[swift_bridge(Copy(4), Hashable, Equatable)] + type RustCopyHashableType; + + #[swift_bridge(init)] + fn new(num: i32) -> RustCopyHashableType; } } @@ -17,3 +23,12 @@ impl RustHashableType { RustHashableType(num) } } + +#[derive(Clone, Copy, Hash, PartialEq)] +pub struct RustCopyHashableType(i32); + +impl RustCopyHashableType { + fn new(num: i32) -> Self { + Self(num) + } +}