diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..0a53bcf --- /dev/null +++ b/build.rs @@ -0,0 +1,11 @@ +#[cfg(feature = "secure-enclave")] +use swift_rs::SwiftLinker; + +fn main() { + // Ensure this matches the versions set in your `Package.swift` file. + #[cfg(feature = "secure-enclave")] + SwiftLinker::new("11") + .with_ios("11") + .with_package("swift-lib", "./swift-lib/") + .link(); +} \ No newline at end of file diff --git a/swift-lib/Package.swift b/swift-lib/Package.swift new file mode 100644 index 0000000..997dad6 --- /dev/null +++ b/swift-lib/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "swift-lib", + platforms: [ + .macOS(.v11), // macOS Catalina. Earliest version that is officially supported by Apple. + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "swift-lib", + type: .static, + targets: ["swift-lib"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(name: "SwiftRs", path: "../swift-rs") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "swift-lib", + dependencies: [.product(name: "SwiftRs", package: "SwiftRs")], + path: "src") + ] +) \ No newline at end of file diff --git a/swift-lib/src/lib.swift b/swift-lib/src/lib.swift new file mode 100644 index 0000000..f23ed3b --- /dev/null +++ b/swift-lib/src/lib.swift @@ -0,0 +1,64 @@ +import SwiftRs +import CryptoKit +import LocalAuthentication + +// reference: +// https://zenn.dev/iceman/scraps/380f69137c7ea2 +// https://www.andyibanez.com/posts/cryptokit-secure-enclave/ +@_cdecl("is_support_secure_enclave") +func isSupportSecureEnclave() -> Bool { + return SecureEnclave.isAvailable +} + +@_cdecl("generate_secure_enclave_p256_keypair") +func generateSecureEnclaveP256KeyPair() -> SRString { + var error: Unmanaged? = nil; + guard let accessCtrl = SecAccessControlCreateWithFlags( + nil, + kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + [.privateKeyUsage, .biometryCurrentSet], + &error + ) else { + return SRString("err:\(error.debugDescription)") + } + do { + let privateKeyReference = try SecureEnclave.P256.KeyAgreement.PrivateKey.init( + accessControl: accessCtrl + ); + let publicKeyBase64 = privateKeyReference.publicKey.x963Representation.base64EncodedString() + let dataRepresentationBase64 = privateKeyReference.dataRepresentation.base64EncodedString() + return SRString("ok:\(publicKeyBase64),\(dataRepresentationBase64)") + } catch { + return SRString("err:\(error)") + } +} + +@_cdecl("compute_secure_enclave_p256_ecdh") +func computeSecureEnclaveP256Ecdh(privateKeyDataRepresentation: SRString, ephemeraPublicKey: SRString) -> SRString { + guard let privateKeyDataRepresentation = Data( + base64Encoded: privateKeyDataRepresentation.toString() + ) else { + return SRString("err:private key base64 decode failed") + } + guard let ephemeralPublicKeyRepresentation = Data( + base64Encoded: ephemeraPublicKey.toString() + ) else { + return SRString("err:ephemeral public key base64 decode failed") + } + do { + let context = LAContext(); + let p = try SecureEnclave.P256.KeyAgreement.PrivateKey( + dataRepresentation: privateKeyDataRepresentation, + authenticationContext: context + ) + + let ephemeralPublicKey = try P256.KeyAgreement.PublicKey.init(derRepresentation: ephemeralPublicKeyRepresentation) + + let sharedSecret = try p.sharedSecretFromKeyAgreement( + with: ephemeralPublicKey) + + return SRString("ok:\(sharedSecret.description)") + } catch { + return SRString("err:\(error)") + } +} diff --git a/swift-rs/Cargo.toml b/swift-rs/Cargo.toml new file mode 100644 index 0000000..2d0172b --- /dev/null +++ b/swift-rs/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "swift-rs" +version = "1.0.6" +description = "Call Swift from Rust with ease!" +authors = ["The swift-rs contributors"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/Brendonovich/swift-rs" +edition = "2021" +exclude=["/src-swift", "*.swift"] +build = "src-rs/test-build.rs" + +# /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features +[package.metadata."docs.rs"] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lib] +path = "src-rs/lib.rs" + +[dependencies] +base64 = "0.21.0" +serde = { version = "1.0", features = ["derive"], optional = true} +serde_json = { version = "1.0", optional = true } + +[build-dependencies] +serde = { version = "1.0", features = ["derive"]} +serde_json = { version = "1.0" } + +[dev-dependencies] +serial_test = "0.10" + +[features] +default = [] +build = ["serde", "serde_json"] diff --git a/swift-rs/LICENSE-APACHE b/swift-rs/LICENSE-APACHE new file mode 100644 index 0000000..6e7334b --- /dev/null +++ b/swift-rs/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 The swift-rs developers + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/swift-rs/LICENSE-MIT b/swift-rs/LICENSE-MIT new file mode 100644 index 0000000..dd89749 --- /dev/null +++ b/swift-rs/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) 2023 The swift-rs Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/swift-rs/Package.swift b/swift-rs/Package.swift new file mode 100644 index 0000000..eb1c07d --- /dev/null +++ b/swift-rs/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "SwiftRs", + platforms: [ + .macOS(.v10_13), + .iOS(.v11), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "SwiftRs", + targets: ["SwiftRs"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "SwiftRs", + dependencies: [], + path: "src-swift") + ] +) diff --git a/swift-rs/README.md b/swift-rs/README.md new file mode 100644 index 0000000..2e83a2d --- /dev/null +++ b/swift-rs/README.md @@ -0,0 +1,483 @@ +# swift-rs + +![Crates.io](https://img.shields.io/crates/v/swift-rs?color=blue&style=flat-square) +![docs.rs](https://img.shields.io/docsrs/swift-rs?color=blue&style=flat-square) + +Call Swift functions from Rust with ease! + +## Setup + +Add `swift-rs` to your project's `dependencies` and `build-dependencies`: + +```toml +[dependencies] +swift-rs = "1.0.5" + +[build-dependencies] +swift-rs = { version = "1.0.5", features = ["build"] } +``` + +Next, some setup work must be done: + +1. Ensure your swift code is organized into a Swift Package. +This can be done in XCode by selecting File -> New -> Project -> Multiplatform -> Swift Package and importing your existing code. +2. Add `SwiftRs` as a dependency to your Swift package and make the build type `.static`. +```swift +let package = Package( + dependencies: [ + .package(url: "https://github.com/Brendonovich/swift-rs", from: "1.0.5") + ], + products: [ + .library( + type: .static, + ), + ], + targets: [ + .target( + // Must specify swift-rs as a dependency of your target + dependencies: [ + .product( + name: "SwiftRs", + package: "swift-rs" + ) + ], + ) + ] +) +``` +3. Create a `build.rs` file in your project's root folder, if you don't have one already. +4. Use `SwiftLinker` in your `build.rs` file to link both the Swift runtime and your Swift package. +The package name should be the same as is specified in your `Package.swift` file, +and the path should point to your Swift project's root folder relative to your crate's root folder. + +```rust +use swift_rs::SwiftLinker; + +fn build() { + // swift-rs has a minimum of macOS 10.13 + // Ensure the same minimum supported macOS version is specified as in your `Package.swift` file. + SwiftLinker::new("10.13") + // Only if you are also targetting iOS + // Ensure the same minimum supported iOS version is specified as in your `Package.swift` file + .with_ios("11") + .with_package(PACKAGE_NAME, PACKAGE_PATH) + .link(); + + // Other build steps +} +``` + +With those steps completed, you should be ready to start using Swift code from Rust! + +If you experience the error `dyld[16008]: Library not loaded: @rpath/libswiftCore.dylib` +when using `swift-rs` with [Tauri](https://tauri.app) ensure you have set your +[Tauri minimum system version](https://tauri.app/v1/guides/building/macos#setting-a-minimum-system-version) +to `10.15` or higher in your `tauri.config.json`. + +## Calling basic functions + +To allow calling a Swift function from Rust, it must follow some rules: + +1. It must be global +2. It must be annotated with `@_cdecl`, so that it is callable from C +3. It must only use types that can be represented in Objective-C, +so only classes that derive `NSObject`, as well as scalars such as Int and Bool. +This excludes strings, arrays, generics (though all of these can be sent with workarounds) +and structs (which are strictly forbidden). + +For this example we will use a function that simply squares a number: + +```swift +public func squareNumber(number: Int) -> Int { + return number * number +} +``` + +So far, this function meets requirements 1 and 3: it is global and public, and only uses the Int type, which is Objective-C compatible. +However, it is not annotated with `@_cdecl`. +To fix this, we must call `@_cdecl` before the function's declaration and specify the name that the function is exposed to Rust with as its only argument. +To keep with Rust's naming conventions, we will export this function in snake case as `square_number`. + +```swift +@_cdecl("square_number") +public func squareNumber(number: Int) -> Int { + return number * number +} +``` + +Now that `squareNumber` is properly exposed to Rust, we can start interfacing with it. +This can be done using the `swift!` macro, with the `Int` type helping to provide a similar function signature: + +```rust +use swift_rs::swift; + +swift!(fn square_number(number: Int) -> Int); +``` + +Lastly, you can call the function from regular Rust functions. +Note that all calls to a Swift function are unsafe, +and require wrapping in an `unsafe {}` block or `unsafe fn`. + +```rust +fn main() { + let input: Int = 4; + let output = unsafe { square_number(input) }; + + println!("Input: {}, Squared: {}", input, output); + // Prints "Input: 4, Squared: 16" +} +``` + +Check [the documentation](TODO) for all available helper types. + +## Returning objects from Swift + +Let's say that we want our `squareNumber` function to return not only the result, but also the original input. +A standard way to do this in Swift would be with a struct: + +```swift +struct SquareNumberResult { + var input: Int + var output: Int +} +``` + +We are not allowed to do this, though, since structs cannot be represented in Objective-C. +Instead, we must use a class that extends `NSObject`: + +```swift +class SquareNumberResult: NSObject { + var input: Int + var output: Int + + init(_ input: Int, _ output: Int) { + self.input = input; + self.output = output + } +} +``` + +Yes, this class could contain the squaring logic too, but that is irrelevant for this example + +An instance of this class can then be returned from `squareNumber`: + +```swift +@_cdecl("square_number") +public func squareNumber(input: Int) -> SquareNumberResult { + let output = input * input + return SquareNumberResult(input, output) +} +``` + +As you can see, returning an `NSObject` from Swift isn't too difficult. +The same can't be said for the Rust implementation, though. +`squareNumber` doesn't actually return a struct containing `input` and `output`, +but instead a pointer to a `SquareNumberResult` stored somewhere in memory. +Additionally, this value contains more data than just `input` and `output`: +Since it is an `NSObject`, it contains extra data that must be accounted for when using it in Rust. + +This may sound daunting, but it's not actually a problem thanks to `SRObject`. +This type manages the pointer internally, and takes a generic argument for a struct that we can access the data through. +Let's see how we'd implement `SquareNumberResult` in Rust: + +```rust +use swift_rs::{swift, Int, SRObject}; + +// Any struct that is used in a C function must be annotated +// with this, and since our Swift function is exposed as a +// C function with @_cdecl, this is necessary here +#[repr(C)] +// Struct matches the class declaration in Swift +struct SquareNumberResult { + input: Int, + output: Int +} + +// SRObject abstracts away the underlying pointer and will automatically deref to +// &SquareNumberResult through the Deref trait +swift!(fn square_number(input: Int) -> SRObject); +``` + +Then, using the new return value is just like using `SquareNumberResult` directly: + +```rust +fn main() { + let input = 4; + let result = unsafe { square_number(input) }; + + let result_input = result.input; // 4 + let result_output = result.output; // 16 +} +``` + +Creating objects in Rust and then passing them to Swift is not supported. + +## Optionals + +`swift-rs` also supports Swift's `nil` type, but only for functions that return optional `NSObject`s. +Functions returning optional primitives cannot be represented in Objective C, and thus are not supported. + +Let's say we have a function returning an optional `SRString`: + +```swift +@_cdecl("optional_string") +func optionalString(returnNil: Bool) -> SRString? { + if (returnNil) return nil + else return SRString("lorem ipsum") +} +``` + +Thanks to Rust's [null pointer optimisation](https://doc.rust-lang.org/std/option/index.html#representation), +the optional nature of `SRString?` can be represented by wrapping `SRString` in Rust's `Option` type! + +```rust +use swift_rs::{swift, Bool, SRString}; + +swift!(optional_string(return_nil: Bool) -> Option) +``` + +Null pointers are actually the reason why a function that returns an optional primitive cannot be represented in C. +If this were to be supported, how could a `nil` be differentiated from a number? It can't! + +## Complex types + +So far we have only looked at using primitive types and structs/classes, +but this leaves out some of the most important data structures: arrays (`SRArray`) and strings (`SRString`). +These types must be treated with caution, however, and are not as flexible as their native Swift & Rust counterparts. + +### Strings + +Strings can be passed between Rust and Swift through `SRString`, which can be created from native strings in either language. + +**As an argument** + +```swift +import SwiftRs + +@_cdecl("swift_print") +public func swiftPrint(value: SRString) { + // .to_string() converts the SRString to a Swift String + print(value.to_string()) +} +``` + +```rust +use swift_rs::{swift, SRString, SwiftRef}; + +swift!(fn swift_print(value: &SRString)); + +fn main() { + // SRString can be created by simply calling .into() on any string reference. + // This will allocate memory in Swift and copy the string + let value: SRString = "lorem ipsum".into(); + + unsafe { swift_print(&value) }; // Will print "lorem ipsum" to the console +} +``` + +**As a return value** + +```swift +import SwiftRs + +@_cdecl("get_string") +public func getString() -> SRString { + let value = "lorem ipsum" + + // SRString can be created from a regular String + return SRString(value) +} +``` + +```rust +use swift_rs::{swift, SRString}; + +swift!(fn get_string() -> SRString); + +fn main() { + let value_srstring = unsafe { get_string() }; + + // SRString can be converted to an &str using as_str()... + let value_str: &str = value_srstring.as_str(); + // or though the Deref trait + let value_str: &str = &*value_srstring; + + // SRString also implements Display + println!("{}", value_srstring); // Will print "lorem ipsum" to the console +} +``` + +### Arrays + +**Primitive Arrays** + +Representing arrays properly is tricky, since we cannot use generics as Swift arguments or return values according to rule 3. +Instead, `swift-rs` provides a generic `SRArray` that can be embedded inside another class that extends `NSObject` that is not generic, +but is restricted to a single element type. + +```swift +import SwiftRs + +// Argument/Return values can contain generic types, but cannot be generic themselves. +// This includes extending generic types. +class IntArray: NSObject { + var data: SRArray + + init(_ data: [Int]) { + self.data = SRArray(data) + } +} + +@_cdecl("get_numbers") +public func getNumbers() -> IntArray { + let numbers = [1, 2, 3, 4] + + return IntArray(numbers) +} +``` + +```rust +use swift_rs::{Int, SRArray, SRObject}; + +#[repr(C)] +struct IntArray { + data: SRArray +} + +// Since IntArray extends NSObject in its Swift implementation, +// it must be wrapped in SRObject on the Rust side +swift!(fn get_numbers() -> SRObject); + +fn main() { + let numbers = unsafe { get_numbers() }; + + // SRArray can be accessed as a slice via as_slice + let numbers_slice: &[Int] = numbers.data.as_slice(); + + assert_eq!(numbers_slice, &[1, 2, 3, 4]); +} +``` + +To simplify things on the rust side, we can actually do away with the `IntArray` struct. +Since `IntArray` only has one field, its memory layout is identical to that of `SRArray`, +so our Rust implementation can be simplified at the cost of equivalence with our Swift code: + +```rust +// We still need to wrap the array in SRObject since +// the wrapper class in Swift is an NSObject +swift!(fn get_numbers() -> SRObject>); +``` + +**NSObject Arrays** + +What if we want to return an `NSObject` array? There are two options on the Swift side: + +1. Continue using `SRArray` and a custom wrapper type, or +2. Use `SRObjectArray`, a wrapper type provided by `swift-rs` that accepts any `NSObject` as its elements. +This can be easier than continuing to create wrapper types, but sacrifices some type safety. + +There is also `SRObjectArray` for Rust, which is compatible with any single-element Swift wrapper type (and of course `SRObjectArray` in Swift), +and automatically wraps its elements in `SRObject`, so there's very little reason to not use it unless you _really_ like custom wrapper types. + +Using `SRObjectArray` in both Swift and Rust with a basic custom class/struct can be done like this: + +```swift +import SwiftRs + +class IntTuple: NSObject { + var item1: Int + var item2: Int + + init(_ item1: Int, _ item2: Int) { + self.item1 = item1 + self.item2 = item2 + } +} + +@_cdecl("get_tuples") +public func getTuples() -> SRObjectArray { + let tuple1 = IntTuple(0,1), + tuple2 = IntTuple(2,3), + tuple3 = IntTuple(4,5) + + let tupleArray: [IntTuple] = [ + tuple1, + tuple2, + tuple3 + ] + + // Type safety is only lost when the Swift array is converted to an SRObjectArray + return SRObjectArray(tupleArray) +} +``` + +```rust +use swift_rs::{swift, Int, SRObjectArray}; + +#[repr(C)] +struct IntTuple { + item1: Int, + item2: Int +} + +// No need to wrap IntTuple in SRObject since +// SRObjectArray does it automatically +swift!(fn get_tuples() -> SRObjectArray); + +fn main() { + let tuples = unsafe { get_tuples() }; + + for tuple in tuples.as_slice() { + // Will print each tuple's contents to the console + println!("Item 1: {}, Item 2: {}", tuple.item1, tuple.item2); + } +} +``` + +Complex types can contain whatever combination of primitives and `SRObject` you like, just remember to follow the 3 rules! + +## Bonuses + +### SRData + +A wrapper type for `SRArray` designed for storing `u8`s - essentially just a byte buffer. + +### Tighter Memory Control with `autoreleasepool!` + +If you've come to Swift from an Objective-C background, you likely know the utility of `@autoreleasepool` blocks. +`swift-rs` has your back on this too, just wrap your block of code with a `autoreleasepool!`, and that block of code now executes with its own autorelease pool! + +```rust +use swift_rs::autoreleasepool; + +for _ in 0..10000 { + autoreleasepool!({ + // do some memory intensive thing here + }); +} +``` + +## Limitations + +Currently, the only types that can be created from Rust are number types, boolean, `SRString`, and `SRData`. +This is because those types are easy to allocate memory for, either on the stack or on the heap via calling out to swift, +whereas other types are not. This may be implemented in the future, though. + +Mutating values across Swift and Rust is not currently an aim for this library, it is purely for providing arguments and returning values. +Besides, this would go against Rust's programming model, potentially allowing for multiple shared references to a value instead of interior mutability via something like a Mutex. + +## License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. diff --git a/swift-rs/example/Cargo.lock b/swift-rs/example/Cargo.lock new file mode 100644 index 0000000..a11f528 --- /dev/null +++ b/swift-rs/example/Cargo.lock @@ -0,0 +1,103 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + +[[package]] +name = "example" +version = "0.1.0" +dependencies = [ + "swift-rs", +] + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4af2ec4714533fcdf07e886f17025ace8b997b9ce51204ee69b6da831c3da57" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "swift-rs" +version = "1.0.5" +dependencies = [ + "base64", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea297be220d52398dcc07ce15a209fce436d361735ac1db700cab3b6cdfb9f54" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" diff --git a/swift-rs/example/Cargo.toml b/swift-rs/example/Cargo.toml new file mode 100644 index 0000000..a2c5888 --- /dev/null +++ b/swift-rs/example/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "example" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +swift-rs = { path = "../" } + +[build-dependencies] +swift-rs = { path = "../", features = ["build"] } diff --git a/swift-rs/example/build.rs b/swift-rs/example/build.rs new file mode 100644 index 0000000..913b415 --- /dev/null +++ b/swift-rs/example/build.rs @@ -0,0 +1,9 @@ +use swift_rs::SwiftLinker; + +fn main() { + // Ensure this matches the versions set in your `Package.swift` file. + SwiftLinker::new("10.15") + .with_ios("11") + .with_package("swift-lib", "./swift-lib/") + .link(); +} diff --git a/swift-rs/example/src/main.rs b/swift-rs/example/src/main.rs new file mode 100644 index 0000000..00d2c15 --- /dev/null +++ b/swift-rs/example/src/main.rs @@ -0,0 +1,39 @@ +use swift_rs::{swift, Bool, Int, SRObject, SRObjectArray, SRString}; + +#[repr(C)] +struct Volume { + pub name: SRString, + path: SRString, + total_capacity: Int, + available_capacity: Int, + is_removable: Bool, + is_ejectable: Bool, + is_root_filesystem: Bool, +} + +#[repr(C)] +struct Test { + pub null: bool, +} + +swift!(fn get_file_thumbnail_base64(path: &SRString) -> SRString); +swift!(fn get_mounts() -> SRObjectArray); +swift!(fn return_nullable(null: Bool) -> Option>); + +fn main() { + let path = "/Users"; + let thumbnail = unsafe { get_file_thumbnail_base64(&path.into()) }; + println!( + "length of base64 encoded thumbnail: {}", + thumbnail.as_str().len() + ); + + let mounts = unsafe { get_mounts() }; + println!("First Volume Name: {}", mounts[0].name); + + let opt = unsafe { return_nullable(true) }; + println!("function returned nil: {}", opt.is_none()); + + let opt = unsafe { return_nullable(false) }; + println!("function returned data: {}", opt.is_some()); +} diff --git a/swift-rs/example/swift-lib/.gitignore b/swift-rs/example/swift-lib/.gitignore new file mode 100644 index 0000000..bb460e7 --- /dev/null +++ b/swift-rs/example/swift-lib/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/swift-rs/example/swift-lib/Package.swift b/swift-rs/example/swift-lib/Package.swift new file mode 100644 index 0000000..7f59df0 --- /dev/null +++ b/swift-rs/example/swift-lib/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "swift-lib", + platforms: [ + .macOS(.v10_15), // macOS Catalina. Earliest version that is officially supported by Apple. + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "swift-lib", + type: .static, + targets: ["swift-lib"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(name: "SwiftRs", path: "../../") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "swift-lib", + dependencies: [.product(name: "SwiftRs", package: "SwiftRs")], + path: "src") + ] +) diff --git a/swift-rs/example/swift-lib/README.md b/swift-rs/example/swift-lib/README.md new file mode 100644 index 0000000..c88b3b8 --- /dev/null +++ b/swift-rs/example/swift-lib/README.md @@ -0,0 +1,3 @@ +# swift + +A description of this package. diff --git a/swift-rs/example/swift-lib/src/lib.swift b/swift-rs/example/swift-lib/src/lib.swift new file mode 100644 index 0000000..df841a8 --- /dev/null +++ b/swift-rs/example/swift-lib/src/lib.swift @@ -0,0 +1,94 @@ +import SwiftRs +import AppKit + +@_cdecl("get_file_thumbnail_base64") +func getFileThumbnailBase64(path: SRString) -> SRString { + let path = path.toString(); + + let image = NSWorkspace.shared.icon(forFile: path) + let bitmap = NSBitmapImageRep(data: image.tiffRepresentation!)!.representation(using: .png, properties: [:])! + + return SRString(bitmap.base64EncodedString()) +} + +class Volume: NSObject { + var name: SRString + var path: SRString + var total_capacity: Int + var available_capacity: Int + var is_removable: Bool + var is_ejectable: Bool + var is_root_filesystem: Bool + + public init(name: String, path: String, total_capacity: Int, available_capacity: Int, is_removable: Bool, is_ejectable: Bool, is_root_filesystem: Bool) { + self.name = SRString(name); + self.path = SRString(path); + self.total_capacity = total_capacity + self.available_capacity = available_capacity + self.is_removable = is_removable + self.is_ejectable = is_ejectable + self.is_root_filesystem = is_root_filesystem + } +} + +@_cdecl("get_mounts") +func getMounts() -> SRObjectArray { + let keys: [URLResourceKey] = [ + .volumeNameKey, + .volumeIsRemovableKey, + .volumeIsEjectableKey, + .volumeTotalCapacityKey, + .volumeAvailableCapacityKey, + .volumeIsRootFileSystemKey, + ] + + let paths = autoreleasepool { + FileManager().mountedVolumeURLs(includingResourceValuesForKeys: keys, options: []) + } + + var validMounts: [Volume] = [] + + if let urls = paths { + autoreleasepool { + for url in urls { + let components = url.pathComponents + if components.count == 1 || components.count > 1 + && components[1] == "Volumes" + { + let metadata = try? url.promisedItemResourceValues(forKeys: Set(keys)) + + let volume = Volume( + name: metadata?.volumeName ?? "", + path: url.path, + total_capacity: metadata?.volumeTotalCapacity ?? 0, + available_capacity: metadata?.volumeAvailableCapacity ?? 0, + is_removable: metadata?.volumeIsRemovable ?? false, + is_ejectable: metadata?.volumeIsEjectable ?? false, + is_root_filesystem: metadata?.volumeIsRootFileSystem ?? false + ) + + + validMounts.append(volume) + } + } + } + } + + return SRObjectArray(validMounts) +} + +class Test: NSObject { + var null: Bool + + public init(_ null: Bool) + { + self.null = null; + } +} + +@_cdecl("return_nullable") +func returnNullable(null: Bool) -> Test? { + if (null == true) { return nil } + + return Test(null) +} diff --git a/swift-rs/src-rs/autorelease.rs b/swift-rs/src-rs/autorelease.rs new file mode 100644 index 0000000..bb1007d --- /dev/null +++ b/swift-rs/src-rs/autorelease.rs @@ -0,0 +1,26 @@ +/// Run code with its own autorelease pool. Semantically, this is identical +/// to [`@autoreleasepool`](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html) +/// in Objective-C +/// +/// +/// ```no_run +/// use swift_rs::autoreleasepool; +/// +/// autoreleasepool!({ +/// // do something memory intensive stuff +/// }) +/// ``` +#[macro_export] +macro_rules! autoreleasepool { + ( $expr:expr ) => {{ + extern "C" { + fn objc_autoreleasePoolPush() -> *mut std::ffi::c_void; + fn objc_autoreleasePoolPop(context: *mut std::ffi::c_void); + } + + let pool = unsafe { objc_autoreleasePoolPush() }; + let r = { $expr }; + unsafe { objc_autoreleasePoolPop(pool) }; + r + }}; +} diff --git a/swift-rs/src-rs/build.rs b/swift-rs/src-rs/build.rs new file mode 100644 index 0000000..394b9eb --- /dev/null +++ b/swift-rs/src-rs/build.rs @@ -0,0 +1,326 @@ +#![allow(dead_code)] +use std::{env, fmt::Display, path::Path, path::PathBuf, process::Command}; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct SwiftTarget { + triple: String, + unversioned_triple: String, + module_triple: String, + //pub swift_runtime_compatibility_version: String, + #[serde(rename = "librariesRequireRPath")] + libraries_require_rpath: bool, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct SwiftPaths { + runtime_library_paths: Vec, + runtime_library_import_paths: Vec, + runtime_resource_path: String, +} + +#[derive(Deserialize)] +struct SwiftEnv { + target: SwiftTarget, + paths: SwiftPaths, +} + +impl SwiftEnv { + fn new(minimum_macos_version: &str, minimum_ios_version: Option<&str>) -> Self { + let rust_target = RustTarget::from_env(); + let target = rust_target.swift_target_triple(minimum_macos_version, minimum_ios_version); + + let swift_target_info_str = Command::new("swift") + .args(["-target", &target, "-print-target-info"]) + .output() + .unwrap() + .stdout; + + serde_json::from_slice(&swift_target_info_str).unwrap() + } +} + +#[allow(clippy::upper_case_acronyms)] +enum RustTargetOS { + MacOS, + IOS, +} + +impl RustTargetOS { + fn from_env() -> Self { + match env::var("CARGO_CFG_TARGET_OS").unwrap().as_str() { + "macos" => RustTargetOS::MacOS, + "ios" => RustTargetOS::IOS, + _ => panic!("unexpected target operating system"), + } + } + + fn to_swift(&self) -> &'static str { + match self { + Self::MacOS => "macosx", + Self::IOS => "ios", + } + } +} + +impl Display for RustTargetOS { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::MacOS => write!(f, "macos"), + Self::IOS => write!(f, "ios"), + } + } +} + +#[allow(clippy::upper_case_acronyms)] +enum SwiftSDK { + MacOS, + IOS, + IOSSimulator, +} + +impl SwiftSDK { + fn from_os(os: &RustTargetOS) -> Self { + let target = env::var("TARGET").unwrap(); + let simulator = target.ends_with("ios-sim") + || (target.starts_with("x86_64") && target.ends_with("ios")); + + match os { + RustTargetOS::MacOS => Self::MacOS, + RustTargetOS::IOS if simulator => Self::IOSSimulator, + RustTargetOS::IOS => Self::IOS, + } + } + + fn clang_lib_extension(&self) -> &'static str { + match self { + Self::MacOS => "osx", + Self::IOS => "ios", + Self::IOSSimulator => "iossim", + } + } +} + +impl Display for SwiftSDK { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::MacOS => write!(f, "macosx"), + Self::IOSSimulator => write!(f, "iphonesimulator"), + Self::IOS => write!(f, "iphoneos"), + } + } +} + +struct RustTarget { + arch: String, + os: RustTargetOS, + sdk: SwiftSDK, +} + +impl RustTarget { + fn from_env() -> Self { + let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + let os = RustTargetOS::from_env(); + let sdk = SwiftSDK::from_os(&os); + + Self { arch, os, sdk } + } + + fn swift_target_triple( + &self, + minimum_macos_version: &str, + minimum_ios_version: Option<&str>, + ) -> String { + let unversioned = self.unversioned_swift_target_triple(); + format!( + "{unversioned}{}{}", + match (&self.os, minimum_ios_version) { + (RustTargetOS::MacOS, _) => minimum_macos_version, + (RustTargetOS::IOS, Some(version)) => version, + _ => "", + }, + // simulator suffix + matches!(self.sdk, SwiftSDK::IOSSimulator) + .then(|| "-simulator".to_string()) + .unwrap_or_default() + ) + } + + fn unversioned_swift_target_triple(&self) -> String { + format!( + "{}-apple-{}", + match self.arch.as_str() { + "aarch64" => "arm64", + a => a, + }, + self.os.to_swift(), + ) + } +} + +struct SwiftPackage { + name: String, + path: PathBuf, +} + +/// Builder for linking the Swift runtime and custom packages. +#[cfg(feature = "build")] +pub struct SwiftLinker { + packages: Vec, + macos_min_version: String, + ios_min_version: Option, +} + +impl SwiftLinker { + /// Creates a new [`SwiftLinker`] with a minimum macOS verison. + /// + /// Minimum macOS version must be at least 10.13. + pub fn new(macos_min_version: &str) -> Self { + Self { + packages: vec![], + macos_min_version: macos_min_version.to_string(), + ios_min_version: None, + } + } + + /// Instructs the [`SwiftLinker`] to also compile for iOS + /// using the specified minimum iOS version. + /// + /// Minimum iOS version must be at least 11. + pub fn with_ios(mut self, min_version: &str) -> Self { + self.ios_min_version = Some(min_version.to_string()); + self + } + + /// Adds a package to be linked against. + /// `name` should match the `name` field in your `Package.swift`, + /// and `path` should point to the root of your Swift package relative + /// to your crate's root. + pub fn with_package(mut self, name: &str, path: impl AsRef) -> Self { + self.packages.extend([SwiftPackage { + name: name.to_string(), + path: path.as_ref().into(), + }]); + + self + } + + /// Links the Swift runtime, then builds and links the provided packages. + /// This does not (yet) automatically rebuild your Swift files when they are modified, + /// you'll need to modify/save your `build.rs` file for that. + pub fn link(self) { + let swift_env = SwiftEnv::new(&self.macos_min_version, self.ios_min_version.as_deref()); + + #[allow(clippy::uninlined_format_args)] + for path in swift_env.paths.runtime_library_paths { + println!("cargo:rustc-link-search=native={path}"); + } + + let debug = env::var("DEBUG").unwrap() == "true"; + let configuration = if debug { "debug" } else { "release" }; + let rust_target = RustTarget::from_env(); + + link_clang_rt(&rust_target); + + for package in self.packages { + let package_path = + Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).join(&package.path); + let out_path = Path::new(&env::var("OUT_DIR").unwrap()) + .join("swift-rs") + .join(&package.name); + + let sdk_path_output = Command::new("xcrun") + .args(["--sdk", &rust_target.sdk.to_string(), "--show-sdk-path"]) + .output() + .unwrap(); + if !sdk_path_output.status.success() { + panic!( + "Failed to get SDK path with `xcrun --sdk {} --show-sdk-path`", + rust_target.sdk + ); + } + + let sdk_path = String::from_utf8_lossy(&sdk_path_output.stdout); + + let mut command = Command::new("swift"); + command.current_dir(&package.path); + + let arch = match std::env::consts::ARCH { + "aarch64" => "arm64", + arch => arch, + }; + + command + // Build the package (duh) + .args(["build"]) + // SDK path for regular compilation (idk) + .args(["--sdk", sdk_path.trim()]) + // Release/Debug configuration + .args(["-c", configuration]) + .args(["--arch", arch]) + // Where the artifacts will be generated to + .args(["--build-path", &out_path.display().to_string()]) + // Override SDK path for each swiftc instance. + // Necessary for iOS compilation. + .args(["-Xswiftc", "-sdk"]) + .args(["-Xswiftc", sdk_path.trim()]) + // Override target triple for each swiftc instance. + // Necessary for iOS compilation. + .args(["-Xswiftc", "-target"]) + .args([ + "-Xswiftc", + &rust_target.swift_target_triple( + &self.macos_min_version, + self.ios_min_version.as_deref(), + ), + ]); + + if !command.status().unwrap().success() { + panic!("Failed to compile swift package {}", package.name); + } + + let search_path = out_path + // swift build uses this output folder no matter what is the target + .join(format!( + "{}-apple-macosx", + arch + )) + .join(configuration); + + println!("cargo:rerun-if-changed={}", package_path.display()); + println!("cargo:rustc-link-search=native={}", search_path.display()); + println!("cargo:rustc-link-lib=static={}", package.name); + } + } +} + +fn link_clang_rt(rust_target: &RustTarget) { + println!( + "cargo:rustc-link-lib=clang_rt.{}", + rust_target.sdk.clang_lib_extension() + ); + println!("cargo:rustc-link-search={}", clang_link_search_path()); +} + +fn clang_link_search_path() -> String { + let output = std::process::Command::new( + std::env::var("SWIFT_RS_CLANG").unwrap_or_else(|_| "/usr/bin/clang".to_string()), + ) + .arg("--print-search-dirs") + .output() + .unwrap(); + if !output.status.success() { + panic!("Can't get search paths from clang"); + } + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines() { + if line.contains("libraries: =") { + let path = line.split('=').nth(1).unwrap(); + return format!("{}/lib/darwin", path); + } + } + panic!("clang is missing search paths"); +} diff --git a/swift-rs/src-rs/dark_magic.rs b/swift-rs/src-rs/dark_magic.rs new file mode 100644 index 0000000..7cb8c36 --- /dev/null +++ b/swift-rs/src-rs/dark_magic.rs @@ -0,0 +1,90 @@ +/// This retain-balancing algorithm is cool but likely isn't required. +/// I'm keeping it around in case it's necessary one day. + +// #[derive(Clone, Copy, Debug)] +// enum ValueArity { +// Reference, +// Value, +// } + +// pub unsafe fn balance_ptrs(args: Vec<(*const c_void, bool)>, ret: Vec<(*const c_void, bool)>) { +// fn collect_references( +// v: Vec<(*const c_void, bool)>, +// ) -> BTreeMap<*const c_void, Vec> { +// v.into_iter().fold( +// BTreeMap::<_, Vec>::new(), +// |mut map, (ptr, is_ref)| { +// map.entry(ptr).or_default().push(if is_ref { +// ValueArity::Reference +// } else { +// ValueArity::Value +// }); +// map +// }, +// ) +// } + +// let mut args = collect_references(args); +// let mut ret = collect_references(ret); + +// let both_counts = args +// .clone() +// .into_iter() +// .flat_map(|(arg, values)| { +// ret.remove(&arg).map(|ret| { +// args.remove(&arg); + +// let ret_values = ret +// .iter() +// .filter(|v| matches!(v, ValueArity::Value)) +// .count() as isize; + +// let arg_references = values +// .iter() +// .filter(|v| matches!(v, ValueArity::Reference)) +// .count() as isize; + +// let ref_in_value_out_retains = min(ret_values, arg_references); + +// (arg, ref_in_value_out_retains) +// }) +// }) +// .collect::>(); + +// let arg_counts = args.into_iter().map(|(ptr, values)| { +// let count = values +// .into_iter() +// .filter(|v| matches!(v, ValueArity::Value)) +// .count() as isize; +// (ptr, count) +// }); + +// let ret_counts = ret +// .into_iter() +// .map(|(ptr, values)| { +// let count = values +// .into_iter() +// .filter(|v| matches!(v, ValueArity::Value)) +// .count() as isize; +// (ptr, count) +// }) +// .collect::>(); + +// both_counts +// .into_iter() +// .chain(arg_counts) +// .chain(ret_counts) +// .for_each(|(ptr, count)| match count { +// 0 => {} +// n if n > 0 => { +// for _ in 0..n { +// retain_object(ptr) +// } +// } +// n => { +// for _ in n..0 { +// release_object(ptr) +// } +// } +// }); +// } diff --git a/swift-rs/src-rs/lib.rs b/swift-rs/src-rs/lib.rs new file mode 100644 index 0000000..3933189 --- /dev/null +++ b/swift-rs/src-rs/lib.rs @@ -0,0 +1,20 @@ +//! Call Swift functions from Rust with ease! +#![cfg_attr(docsrs, feature(doc_cfg))] + +mod autorelease; +mod swift; +mod swift_arg; +mod swift_ret; +mod types; + +pub use autorelease::*; +pub use swift::*; +pub use swift_arg::*; +pub use swift_ret::*; +pub use types::*; + +#[cfg(feature = "build")] +#[cfg_attr(docsrs, doc(cfg(feature = "build")))] +mod build; +#[cfg(feature = "build")] +pub use build::*; diff --git a/swift-rs/src-rs/swift.rs b/swift-rs/src-rs/swift.rs new file mode 100644 index 0000000..0b4f370 --- /dev/null +++ b/swift-rs/src-rs/swift.rs @@ -0,0 +1,101 @@ +use std::ffi::c_void; + +use crate::*; + +/// Reference to an `NSObject` for internal use by [`swift!`]. +#[must_use = "A Ref MUST be sent over to the Swift side"] +#[repr(transparent)] +pub struct SwiftRef<'a, T: SwiftObject>(&'a SRObjectImpl); + +impl<'a, T: SwiftObject> SwiftRef<'a, T> { + pub(crate) unsafe fn retain(&self) { + retain_object(self.0 as *const _ as *const c_void) + } +} + +/// A type that is represented as an `NSObject` in Swift. +pub trait SwiftObject { + type Shape; + + /// Gets a reference to the `SRObject` at the root of a `SwiftObject` + fn get_object(&self) -> &SRObject; + + /// Creates a [`SwiftRef`] for an object which can be used when calling a Swift function. + /// This function should never be called manually, + /// instead you should rely on the [`swift!`] macro to call it for you. + /// + /// # Safety + /// This function converts the [`NonNull`](std::ptr::NonNull) + /// inside an [`SRObject`] into a reference, + /// implicitly assuming that the pointer is still valid. + /// The inner pointer is private, + /// and the returned [`SwiftRef`] is bound to the lifetime of the original [`SRObject`], + /// so if you use `swift-rs` as normal this function should be safe. + unsafe fn swift_ref(&self) -> SwiftRef + where + Self: Sized, + { + SwiftRef(self.get_object().0.as_ref()) + } + + /// Adds a retain to an object. + /// + /// # Safety + /// Just don't call this, let [`swift!`] handle it for you. + unsafe fn retain(&self) + where + Self: Sized, + { + self.swift_ref().retain() + } +} + +swift!(pub(crate) fn retain_object(obj: *const c_void)); +swift!(pub(crate) fn release_object(obj: *const c_void)); +swift!(pub(crate) fn data_from_bytes(data: *const u8, size: Int) -> SRData); +swift!(pub(crate) fn string_from_bytes(data: *const u8, size: Int) -> SRString); + +/// Declares a function defined in a swift library. +/// As long as this macro is used, retain counts of arguments +/// and return values will be correct. +/// +/// Use this macro as if the contents were going directly +/// into an `extern "C"` block. +/// +/// ``` +/// use swift_rs::*; +/// +/// swift!(fn echo(string: &SRString) -> SRString); +/// +/// let string: SRString = "test".into(); +/// let result = unsafe { echo(&string) }; +/// +/// assert_eq!(result.as_str(), string.as_str()) +/// ``` +/// +/// # Details +/// +/// Internally this macro creates a wrapping function around an `extern "C"` block +/// that represents the actual Swift function. This is done in order to restrict the types +/// that can be used as arguments and return types, and to ensure that retain counts of returned +/// values are appropriately balanced. +#[macro_export] +macro_rules! swift { + ($vis:vis fn $name:ident $(<$($lt:lifetime),+>)? ($($arg:ident: $arg_ty:ty),*) $(-> $ret:ty)?) => { + $vis unsafe fn $name $(<$($lt),*>)? ($($arg: $arg_ty),*) $(-> $ret)? { + extern "C" { + fn $name $(<$($lt),*>)? ($($arg: <$arg_ty as $crate::SwiftArg>::ArgType),*) $(-> $ret)?; + } + + let res = { + $(let $arg = $crate::SwiftArg::as_arg(&$arg);)* + + $name($($arg),*) + }; + + $crate::SwiftRet::retain(&res); + + res + } + }; +} diff --git a/swift-rs/src-rs/swift_arg.rs b/swift-rs/src-rs/swift_arg.rs new file mode 100644 index 0000000..689650e --- /dev/null +++ b/swift-rs/src-rs/swift_arg.rs @@ -0,0 +1,75 @@ +use std::ffi::c_void; + +use crate::{swift::SwiftObject, *}; + +/// Identifies a type as being a valid argument in a Swift function. +pub trait SwiftArg<'a> { + type ArgType; + + /// Creates a swift-compatible version of the argument. + /// For primitives this just returns `self`, + /// but for [`SwiftObject`] types it wraps them in [`SwiftRef`]. + /// + /// This function is called within the [`swift!`] macro. + /// + /// # Safety + /// + /// Creating a [`SwiftRef`] is inherently unsafe, + /// but is reliable if using the [`swift!`] macro, + /// so it is not advised to call this function manually. + unsafe fn as_arg(&'a self) -> Self::ArgType; +} + +macro_rules! primitive_impl { + ($($t:ty),+) => { + $(impl<'a> SwiftArg<'a> for $t { + type ArgType = $t; + + unsafe fn as_arg(&'a self) -> Self::ArgType { + *self + } + })+ + }; +} + +primitive_impl!( + Bool, + Int, + Int8, + Int16, + Int32, + Int64, + UInt, + UInt8, + UInt16, + UInt32, + UInt64, + Float32, + Float64, + *const c_void, + *mut c_void, + *const u8, + () +); + +macro_rules! ref_impl { + ($($t:ident $(<$($gen:ident),+>)?),+) => { + $(impl<'a $($(, $gen: 'a),+)?> SwiftArg<'a> for $t$(<$($gen),+>)? { + type ArgType = SwiftRef<'a, $t$(<$($gen),+>)?>; + + unsafe fn as_arg(&'a self) -> Self::ArgType { + self.swift_ref() + } + })+ + }; +} + +ref_impl!(SRObject, SRArray, SRData, SRString); + +impl<'a, T: SwiftArg<'a>> SwiftArg<'a> for &T { + type ArgType = T::ArgType; + + unsafe fn as_arg(&'a self) -> Self::ArgType { + (*self).as_arg() + } +} diff --git a/swift-rs/src-rs/swift_ret.rs b/swift-rs/src-rs/swift_ret.rs new file mode 100644 index 0000000..d853d12 --- /dev/null +++ b/swift-rs/src-rs/swift_ret.rs @@ -0,0 +1,55 @@ +use crate::{swift::SwiftObject, *}; +use std::ffi::c_void; + +/// Identifies a type as being a valid return type from a Swift function. +/// For types that are objects which need extra retains, +/// the [`retain`](SwiftRet::retain) function will be re-implemented. +pub trait SwiftRet { + /// Adds a retain to the value if possible + /// + /// # Safety + /// Just don't use this. + /// Let [`swift!`] handle it. + unsafe fn retain(&self) {} +} + +macro_rules! primitive_impl { + ($($t:ty),+) => { + $(impl SwiftRet for $t { + })+ + }; +} + +primitive_impl!( + Bool, + Int, + Int8, + Int16, + Int32, + Int64, + UInt, + UInt8, + UInt16, + UInt32, + UInt64, + Float32, + Float64, + *const c_void, + *mut c_void, + *const u8, + () +); + +impl SwiftRet for Option { + unsafe fn retain(&self) { + if let Some(v) = self { + v.retain() + } + } +} + +impl SwiftRet for T { + unsafe fn retain(&self) { + (*self).retain() + } +} diff --git a/swift-rs/src-rs/test-build.rs b/swift-rs/src-rs/test-build.rs new file mode 100644 index 0000000..da43c63 --- /dev/null +++ b/swift-rs/src-rs/test-build.rs @@ -0,0 +1,20 @@ +//! Build script for swift-rs that is a no-op for normal builds, but can be enabled +//! to include test swift library based on env var `TEST_SWIFT_RS=true` with the +//! `build` feature being enabled. + +#[cfg(feature = "build")] +mod build; + +fn main() { + println!("cargo:rerun-if-env-changed=TEST_SWIFT_RS"); + + #[cfg(feature = "build")] + if std::env::var("TEST_SWIFT_RS").unwrap_or_else(|_| "false".into()) == "true" { + use build::SwiftLinker; + + SwiftLinker::new("10.15") + .with_ios("11") + .with_package("test-swift", "tests/swift-pkg") + .link(); + } +} diff --git a/swift-rs/src-rs/types/array.rs b/swift-rs/src-rs/types/array.rs new file mode 100644 index 0000000..fc69069 --- /dev/null +++ b/swift-rs/src-rs/types/array.rs @@ -0,0 +1,110 @@ +use std::{ops::Deref, ptr::NonNull}; + +use crate::swift::SwiftObject; + +use super::SRObject; + +/// Wrapper of [`SRArray`] exclusively for arrays of objects. +/// Equivalent to `SRObjectArray` in Swift. +// SRArray is wrapped in SRObject since the Swift implementation extends NSObject +pub type SRObjectArray = SRObject>>; + +#[doc(hidden)] +#[repr(C)] +pub struct SRArrayImpl { + data: NonNull, + length: usize, +} + +/// General array type for objects and scalars. +/// +/// ## Returning Directly +/// +/// When returning an `SRArray` from a Swift function, +/// you will need to wrap it in an `NSObject` class since +/// Swift doesn't permit returning generic types from `@_cdecl` functions. +/// To account for the wrapping `NSObject`, the array must be wrapped +/// in `SRObject` on the Rust side. +/// +/// ```rust +/// use swift_rs::{swift, SRArray, SRObject, Int}; +/// +/// swift!(fn get_int_array() -> SRObject>); +/// +/// let array = unsafe { get_int_array() }; +/// +/// assert_eq!(array.as_slice(), &[1, 2, 3]) +/// ``` +/// [_corresponding Swift code_](https://github.com/Brendonovich/swift-rs/blob/07269e511f1afb71e2fcfa89ca5d7338bceb20e8/tests/swift-pkg/doctests.swift#L19) +/// +/// ## Returning in a Struct fIeld +/// +/// When returning an `SRArray` from a custom struct that is itself an `NSObject`, +/// the above work is already done for you. +/// Assuming your custom struct is already wrapped in `SRObject` in Rust, +/// `SRArray` will work normally. +/// +/// ```rust +/// use swift_rs::{swift, SRArray, SRObject, Int}; +/// +/// #[repr(C)] +/// struct ArrayStruct { +/// array: SRArray +/// } +/// +/// swift!(fn get_array_struct() -> SRObject); +/// +/// let data = unsafe { get_array_struct() }; +/// +/// assert_eq!(data.array.as_slice(), &[4, 5, 6]); +/// ``` +/// [_corresponding Swift code_](https://github.com/Brendonovich/swift-rs/blob/07269e511f1afb71e2fcfa89ca5d7338bceb20e8/tests/swift-pkg/doctests.swift#L32) +#[repr(transparent)] +pub struct SRArray(SRObject>); + +impl SRArray { + pub fn as_slice(&self) -> &[T] { + self.0.as_slice() + } +} + +impl SwiftObject for SRArray { + type Shape = SRArrayImpl; + + fn get_object(&self) -> &SRObject { + &self.0 + } +} + +impl Deref for SRArray { + type Target = [T]; + + fn deref(&self) -> &Self::Target { + self.0.as_slice() + } +} + +impl SRArrayImpl { + pub fn as_slice(&self) -> &[T] { + unsafe { std::slice::from_raw_parts(self.data.as_ref(), self.length) } + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for SRArray +where + T: serde::Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeSeq; + + let mut seq = serializer.serialize_seq(Some(self.len()))?; + for item in self.iter() { + seq.serialize_element(item)?; + } + seq.end() + } +} diff --git a/swift-rs/src-rs/types/data.rs b/swift-rs/src-rs/types/data.rs new file mode 100644 index 0000000..e982235 --- /dev/null +++ b/swift-rs/src-rs/types/data.rs @@ -0,0 +1,75 @@ +use crate::{ + swift::{self, SwiftObject}, + Int, +}; + +use super::{array::SRArray, SRObject}; + +use std::ops::Deref; + +type Data = SRArray; + +/// Convenience type for working with byte buffers, +/// analagous to `SRData` in Swift. +/// +/// ```rust +/// use swift_rs::{swift, SRData}; +/// +/// swift!(fn get_data() -> SRData); +/// +/// let data = unsafe { get_data() }; +/// +/// assert_eq!(data.as_ref(), &[1, 2, 3]) +/// ``` +/// [_corresponding Swift code_](https://github.com/Brendonovich/swift-rs/blob/07269e511f1afb71e2fcfa89ca5d7338bceb20e8/tests/swift-pkg/doctests.swift#L68) +#[repr(transparent)] +pub struct SRData(SRObject); + +impl SRData { + /// + pub fn as_slice(&self) -> &[u8] { + self + } + + pub fn to_vec(&self) -> Vec { + self.as_slice().to_vec() + } +} + +impl SwiftObject for SRData { + type Shape = Data; + + fn get_object(&self) -> &SRObject { + &self.0 + } +} + +impl Deref for SRData { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef<[u8]> for SRData { + fn as_ref(&self) -> &[u8] { + self + } +} + +impl From<&[u8]> for SRData { + fn from(value: &[u8]) -> Self { + unsafe { swift::data_from_bytes(value.as_ptr(), value.len() as Int) } + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for SRData { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_bytes(self) + } +} diff --git a/swift-rs/src-rs/types/mod.rs b/swift-rs/src-rs/types/mod.rs new file mode 100644 index 0000000..90d9465 --- /dev/null +++ b/swift-rs/src-rs/types/mod.rs @@ -0,0 +1,11 @@ +mod array; +mod data; +mod object; +mod scalars; +mod string; + +pub use array::*; +pub use data::*; +pub use object::*; +pub use scalars::*; +pub use string::*; diff --git a/swift-rs/src-rs/types/object.rs b/swift-rs/src-rs/types/object.rs new file mode 100644 index 0000000..49748a7 --- /dev/null +++ b/swift-rs/src-rs/types/object.rs @@ -0,0 +1,75 @@ +use crate::swift::{self, SwiftObject}; +use std::{ffi::c_void, ops::Deref, ptr::NonNull}; + +#[doc(hidden)] +#[repr(C)] +pub struct SRObjectImpl { + _nsobject_offset: u8, + data: T, +} + +/// Wrapper for arbitrary `NSObject` types. +/// +/// When returning an `NSObject`, its Rust type must be wrapped in `SRObject`. +/// The type must also be annotated with `#[repr(C)]` to ensure its memory layout +/// is identical to its Swift counterpart's. +/// +/// ```rust +/// use swift_rs::{swift, SRObject, Int, Bool}; +/// +/// #[repr(C)] +/// struct CustomObject { +/// a: Int, +/// b: Bool +/// } +/// +/// swift!(fn get_custom_object() -> SRObject); +/// +/// let value = unsafe { get_custom_object() }; +/// +/// let reference: &CustomObject = value.as_ref(); +/// ``` +/// [_corresponding Swift code_](https://github.com/Brendonovich/swift-rs/blob/07269e511f1afb71e2fcfa89ca5d7338bceb20e8/tests/swift-pkg/doctests.swift#L49) +#[repr(transparent)] +pub struct SRObject(pub(crate) NonNull>); + +impl SwiftObject for SRObject { + type Shape = T; + + fn get_object(&self) -> &SRObject { + self + } +} + +impl Deref for SRObject { + type Target = T; + + fn deref(&self) -> &T { + unsafe { &self.0.as_ref().data } + } +} + +impl AsRef for SRObject { + fn as_ref(&self) -> &T { + self + } +} + +impl Drop for SRObject { + fn drop(&mut self) { + unsafe { swift::release_object(self.0.as_ref() as *const _ as *const c_void) } + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for SRObject +where + T: serde::Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.deref().serialize(serializer) + } +} diff --git a/swift-rs/src-rs/types/scalars.rs b/swift-rs/src-rs/types/scalars.rs new file mode 100644 index 0000000..226f3bd --- /dev/null +++ b/swift-rs/src-rs/types/scalars.rs @@ -0,0 +1,34 @@ +/// Swift's [`Bool`](https://developer.apple.com/documentation/swift/bool) type +pub type Bool = bool; + +/// Swift's [`Int`](https://developer.apple.com/documentation/swift/int) type +pub type Int = isize; +/// Swift's [`Int8`](https://developer.apple.com/documentation/swift/int8) type +pub type Int8 = i8; +/// Swift's [`Int16`](https://developer.apple.com/documentation/swift/int16) type +pub type Int16 = i16; +/// Swift's [`Int32`](https://developer.apple.com/documentation/swift/int32) type +pub type Int32 = i32; +/// Swift's [`Int64`](https://developer.apple.com/documentation/swift/int64) type +pub type Int64 = i64; + +/// Swift's [`UInt`](https://developer.apple.com/documentation/swift/uint) type +pub type UInt = usize; +/// Swift's [`UInt8`](https://developer.apple.com/documentation/swift/uint8) type +pub type UInt8 = u8; +/// Swift's [`UInt16`](https://developer.apple.com/documentation/swift/uint16) type +pub type UInt16 = u16; +/// Swift's [`UInt32`](https://developer.apple.com/documentation/swift/uint32) type +pub type UInt32 = u32; +/// Swift's [`UInt64`](https://developer.apple.com/documentation/swift/uint64) type +pub type UInt64 = u64; + +/// Swift's [`Float`](https://developer.apple.com/documentation/swift/float) type +pub type Float = f32; +/// Swift's [`Double`](https://developer.apple.com/documentation/swift/double) type +pub type Double = f64; + +/// Swift's [`Float32`](https://developer.apple.com/documentation/swift/float32) type +pub type Float32 = f32; +/// Swift's [`Float64`](https://developer.apple.com/documentation/swift/float64) type +pub type Float64 = f64; diff --git a/swift-rs/src-rs/types/string.rs b/swift-rs/src-rs/types/string.rs new file mode 100644 index 0000000..3f6f86f --- /dev/null +++ b/swift-rs/src-rs/types/string.rs @@ -0,0 +1,84 @@ +use std::{ + fmt::{Display, Error, Formatter}, + ops::Deref, +}; + +use crate::{ + swift::{self, SwiftObject}, + Int, SRData, SRObject, +}; + +/// String type that can be shared between Swift and Rust. +/// +/// ```rust +/// use swift_rs::{swift, SRString}; +/// +/// swift!(fn get_greeting(name: &SRString) -> SRString); +/// +/// let greeting = unsafe { get_greeting(&"Brendan".into()) }; +/// +/// assert_eq!(greeting.as_str(), "Hello Brendan!"); +/// ``` +/// [_corresponding Swift code_](https://github.com/Brendonovich/swift-rs/blob/07269e511f1afb71e2fcfa89ca5d7338bceb20e8/tests/swift-pkg/doctests.swift#L56) +#[repr(transparent)] +pub struct SRString(SRData); + +impl SRString { + pub fn as_str(&self) -> &str { + unsafe { std::str::from_utf8_unchecked(&self.0) } + } +} + +impl SwiftObject for SRString { + type Shape = ::Shape; + + fn get_object(&self) -> &SRObject { + self.0.get_object() + } +} + +impl Deref for SRString { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} + +impl AsRef<[u8]> for SRString { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl From<&str> for SRString { + fn from(string: &str) -> Self { + unsafe { swift::string_from_bytes(string.as_ptr(), string.len() as Int) } + } +} + +impl Display for SRString { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + self.as_str().fmt(f) + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for SRString { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} +#[cfg(feature = "serde")] +impl<'a> serde::Deserialize<'a> for SRString { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'a>, + { + let string = String::deserialize(deserializer)?; + Ok(SRString::from(string.as_str())) + } +} diff --git a/swift-rs/src-swift/lib.swift b/swift-rs/src-swift/lib.swift new file mode 100644 index 0000000..7bee3ad --- /dev/null +++ b/swift-rs/src-swift/lib.swift @@ -0,0 +1,94 @@ +import Foundation + +public class SRArray: NSObject { + // Used by Rust + let pointer: UnsafePointer + let length: Int; + + // Actual array, deallocates objects inside automatically + let array: [T]; + + public override init() { + self.array = []; + self.pointer = UnsafePointer(self.array); + self.length = 0; + } + + public init(_ data: [T]) { + self.array = data; + self.pointer = UnsafePointer(self.array) + self.length = data.count + } + + public func toArray() -> [T] { + return Array(self.array) + } +} + +public class SRObjectArray: NSObject { + let data: SRArray + + public init(_ data: [NSObject]) { + self.data = SRArray(data) + } +} + +public class SRData: NSObject { + let data: SRArray + + public override init() { + self.data = SRArray() + } + + public init(_ data: [UInt8]) { + self.data = SRArray(data) + } + + public init (_ srArray: SRArray) { + self.data = srArray + } + + public func toArray() -> [UInt8] { + return self.data.toArray() + } +} + +public class SRString: SRData { + public override init() { + super.init([]) + } + + public init(_ string: String) { + super.init(Array(string.utf8)) + } + + init(_ data: SRData) { + super.init(data.data) + } + + public func toString() -> String { + return String(bytes: self.data.array, encoding: .utf8)! + } +} + +@_cdecl("retain_object") +func retainObject(ptr: UnsafeMutableRawPointer) { + let _ = Unmanaged.fromOpaque(ptr).retain() +} + +@_cdecl("release_object") +func releaseObject(ptr: UnsafeMutableRawPointer) { + let _ = Unmanaged.fromOpaque(ptr).release() +} + +@_cdecl("data_from_bytes") +func dataFromBytes(data: UnsafePointer, size: Int) -> SRData { + let buffer = UnsafeBufferPointer(start: data, count: size) + return SRData(Array(buffer)) +} + +@_cdecl("string_from_bytes") +func stringFromBytes(data: UnsafePointer, size: Int) -> SRString { + let data = dataFromBytes(data: data, size: size); + return SRString(data) +} diff --git a/swift-rs/tests/swift-pkg/Package.swift b/swift-rs/tests/swift-pkg/Package.swift new file mode 100644 index 0000000..d34291b --- /dev/null +++ b/swift-rs/tests/swift-pkg/Package.swift @@ -0,0 +1,31 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "test-swift", + platforms: [ + .macOS(.v11), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "test-swift", + type: .static, + targets: ["test-swift"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(name: "SwiftRs", path: "../../") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "test-swift", + dependencies: [.product(name: "SwiftRs", package: "SwiftRs")], + path: ".", + exclude: ["test_example.rs", "test_bindings.rs"]) + ] +) diff --git a/swift-rs/tests/swift-pkg/doctests.swift b/swift-rs/tests/swift-pkg/doctests.swift new file mode 100644 index 0000000..11b28ca --- /dev/null +++ b/swift-rs/tests/swift-pkg/doctests.swift @@ -0,0 +1,70 @@ +import Foundation +import SwiftRs + +// SRArray +// +// Notice that IntArray and ArrayStruct are almost identical! +// The only actual difference between these types is how they're used in Rust, +// but if you added more fields to ArrayStruct then that wouldn't be the case anymore. + +class IntArray: NSObject { + var data: SRArray + + init(data: [Int]) { + self.data = SRArray(data) + } +} + +@_cdecl("get_int_array") +func getIntArray() -> IntArray { + return IntArray(data: [1, 2, 3]) +} + +class ArrayStruct: NSObject { + var array: SRArray + + init(array: [Int]) { + self.array = SRArray(array) + } +} + +@_cdecl("get_array_struct") +func getArrayStruct() -> ArrayStruct { + return ArrayStruct(array: [4, 5, 6]) +} + +// SRObject + +class CustomObject: NSObject { + var a: Int + var b: Bool + + init(a: Int, b: Bool) { + self.a = a + self.b = b + } +} + +@_cdecl("get_custom_object") +func getCustomObject() -> CustomObject { + return CustomObject(a: 3, b: true) +} + +// SRString + +@_cdecl("get_greeting") +func getGreeting(name: SRString) -> SRString { + return SRString("Hello \(name.toString())!") +} + +@_cdecl("echo") +func echo(string: SRString) -> SRString { + return string +} + +// SRData + +@_cdecl("get_data") +func getData() -> SRData { + return SRData([1, 2, 3]) +} diff --git a/swift-rs/tests/swift-pkg/lib.swift b/swift-rs/tests/swift-pkg/lib.swift new file mode 100644 index 0000000..5f0b2bd --- /dev/null +++ b/swift-rs/tests/swift-pkg/lib.swift @@ -0,0 +1,29 @@ +import SwiftRs +import Foundation + +class Complex: NSObject { + var a: SRString + var b: Int + var c: Bool + + public init(a: SRString, b: Int, c: Bool) { + self.a = a + self.b = b + self.c = c + } +} + +@_cdecl("complex_data") +func complexData() -> SRObjectArray { + return SRObjectArray([ + Complex(a: SRString("Brendan"), b: 0, c: true), + Complex(a: SRString("Amod"), b: 1, c: false), + Complex(a: SRString("Lucas"), b: 2, c: true), + Complex(a: SRString("Oscar"), b: 3, c: false), + ]) +} + +@_cdecl("echo_data") +func echoData(data: SRData) -> SRData { + return SRData(data.toArray()) +} diff --git a/swift-rs/tests/test_bindings.rs b/swift-rs/tests/test_bindings.rs new file mode 100644 index 0000000..35f9b4c --- /dev/null +++ b/swift-rs/tests/test_bindings.rs @@ -0,0 +1,150 @@ +//! Test for swift-rs bindings +//! +//! Needs to be run with the env var `TEST_SWIFT_RS=true`, to allow for +//! the test swift code to be linked. + +use serial_test::serial; +use std::{env, process::Command}; +use swift_rs::*; + +macro_rules! test_with_leaks { + ( $op:expr ) => {{ + let leaks_env_var = "TEST_RUNNING_UNDER_LEAKS"; + if env::var(leaks_env_var).unwrap_or_else(|_| "false".into()) == "true" { + let _ = $op(); + } else { + // we run $op directly in the current process first, as leaks will not give + // us the exit code of $op, but only if memory leaks happened or not + $op(); + + // and now we run the above codepath under leaks monitoring + let exe = env::current_exe().unwrap(); + + // codesign the binary first, so that leaks can be run + let debug_plist = exe.parent().unwrap().join("debug.plist"); + let plist_path = &debug_plist.to_string_lossy(); + std::fs::write(&debug_plist, DEBUG_PLIST_XML.as_bytes()).unwrap(); + let status = Command::new("codesign") + .args([ + "-s", + "-", + "-v", + "-f", + "--entitlements", + plist_path, + &exe.to_string_lossy(), + ]) + .status() + .expect("cmd failure"); + assert!(status.success(), "failed to codesign"); + + // run leaks command to detect memory leaks + let status = Command::new("leaks") + .args(["-atExit", "--", &exe.to_string_lossy(), "--nocapture"]) + .env(leaks_env_var, "true") + .status() + .expect("cmd failure"); + assert!(status.success(), "leaks detected in memory pressure test"); + } + }}; +} + +swift!(fn echo(string: &SRString) -> SRString); + +#[test] +#[serial] +fn test_reflection() { + test_with_leaks!(|| { + // create memory pressure + let name: SRString = "Brendan".into(); + for _ in 0..10_000 { + let reflected = unsafe { echo(&name) }; + assert_eq!(name.as_str(), reflected.as_str()); + } + }); +} + +swift!(fn get_greeting(name: &SRString) -> SRString); + +#[test] +#[serial] +fn test_string() { + test_with_leaks!(|| { + let name: SRString = "Brendan".into(); + let greeting = unsafe { get_greeting(&name) }; + assert_eq!(greeting.as_str(), "Hello Brendan!"); + }); +} + +#[test] +#[serial] +fn test_memory_pressure() { + test_with_leaks!(|| { + // create memory pressure + let name: SRString = "Brendan".into(); + for _ in 0..10_000 { + let greeting = unsafe { get_greeting(&name) }; + assert_eq!(greeting.as_str(), "Hello Brendan!"); + } + }); +} + +#[test] +#[serial] +fn test_autoreleasepool() { + test_with_leaks!(|| { + // create memory pressure + let name: SRString = "Brendan".into(); + for _ in 0..10_000 { + autoreleasepool!({ + let greeting = unsafe { get_greeting(&name) }; + assert_eq!(greeting.as_str(), "Hello Brendan!"); + }); + } + }); +} + +#[repr(C)] +struct Complex { + a: SRString, + b: Int, + c: Bool, +} + +swift!(fn complex_data() -> SRObjectArray); + +#[test] +#[serial] +fn test_complex() { + test_with_leaks!(|| { + let mut v = vec![]; + + for _ in 0..10_000 { + let data = unsafe { complex_data() }; + assert_eq!(data[0].a.as_str(), "Brendan"); + v.push(data); + } + }); +} + +swift!(fn echo_data(data: &SRData) -> SRData); + +#[test] +#[serial] +fn test_data() { + test_with_leaks!(|| { + let str: &str = "hello"; + let bytes = str.as_bytes(); + for _ in 0..10_000 { + let data = unsafe { echo_data(&bytes.into()) }; + assert_eq!(data.as_slice(), bytes); + } + }); +} + +const DEBUG_PLIST_XML: &str = r#" + + + com.apple.security.get-task-allow + +"#;