13 个版本 (7 个稳定版)

1.0.6 2023年8月6日
1.0.5 2023年4月10日
1.0.3 2023年3月18日
1.0.0 2023年2月28日
0.1.0 2021年10月20日

#88 in Rust 模式

Download history 3262/week @ 2024-03-14 3416/week @ 2024-03-21 3369/week @ 2024-03-28 3351/week @ 2024-04-04 4116/week @ 2024-04-11 4248/week @ 2024-04-18 4098/week @ 2024-04-25 3337/week @ 2024-05-02 4309/week @ 2024-05-09 5550/week @ 2024-05-16 5865/week @ 2024-05-23 6691/week @ 2024-05-30 3487/week @ 2024-06-06 4129/week @ 2024-06-13 4078/week @ 2024-06-20 4221/week @ 2024-06-27

16,731 每月下载量
用于 56 个 crate (2 个直接使用)

MIT/Apache

49KB
667

swift-rs

Crates.io docs.rs

轻松从 Rust 调用 Swift 函数!

设置

swift-rs 添加到项目的 dependenciesbuild-dependencies

[dependencies]
swift-rs = "1.0.5"

[build-dependencies]
swift-rs = { version = "1.0.5", features = ["build"] }

接下来,需要进行一些设置工作

  1. 确保您的 Swift 代码已组织成 Swift 包。这可以通过在 XCode 中选择 File -> New -> Project -> Multiplatform -> Swift Package 并导入现有代码来完成。
  2. SwiftRs 添加为 Swift 包的依赖项,并将构建类型设置为 .static
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"
                )
            ],
        )
    ]
)
  1. 如果项目根目录中没有,则在项目根目录下创建一个 build.rs 文件。
  2. build.rs 文件中使用 SwiftLinker 来链接 Swift 运行时和您的 Swift 包。包名应与您的 Package.swift 文件中指定的名称相同,路径应指向相对于 crate 根目录的 Swift 项目的根目录。
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
}

完成这些步骤后,您应该可以开始从 Rust 使用 Swift 代码了!

如果您在使用 Tauriswift-rs 时遇到错误 dyld[16008]: Library not loaded: @rpath/libswiftCore.dylib,请确保您已在您的 tauri.config.json 中将 Tauri 最低系统版本 设置为 10.15 或更高。

调用基本函数

要允许从 Rust 调用 Swift 函数,它必须遵循一些规则

  1. 它必须是全局的
  2. 它必须使用@_cdecl进行标记,以便可以从C调用
  3. 它只能使用在Objective-C中可以表示的类型,因此只有继承自NSObject的类,以及Int和Bool这样的标量。这排除了字符串、数组、泛型(尽管可以通过变通方法发送这些)和结构体(结构体是严格禁止的)。

在这个例子中,我们将使用一个简单的平方数字的函数

public func squareNumber(number: Int) -> Int {
	return number * number
}

到目前为止,这个函数满足了第1和第3项要求:它是全局的和公共的,并且只使用了Int类型,这是Objective-C兼容的。但是,它没有使用@_cdecl进行标记。为了修复这个问题,我们必须在函数声明之前调用@_cdecl,并指定函数暴露给Rust的名称作为其唯一的参数。为了遵循Rust的命名约定,我们将以snake case的形式导出这个函数,名称为square_number

@_cdecl("square_number")
public func squareNumber(number: Int) -> Int {
    return number * number
}

现在squareNumber已经正确地暴露给Rust,我们可以开始与它交互。这可以通过使用swift!宏来实现,其中Int类型有助于提供类似的函数签名

use swift_rs::swift;

swift!(fn square_number(number: Int) -> Int);

最后,您可以从常规的Rust函数中调用此函数。请注意,所有对Swift函数的调用都是不安全的,并且需要在unsafe {}块或unsafe fn中包装。

fn main() {
    let input: Int = 4;
    let output = unsafe { square_number(input) };

    println!("Input: {}, Squared: {}", input, output);
    // Prints "Input: 4, Squared: 16"
}

请查看文档以获取所有可用的辅助类型。

从Swift返回对象

假设我们想让我们的squareNumber函数不仅返回结果,还返回原始输入。在Swift中,这是一种标准的做法,使用结构体

struct SquareNumberResult {
    var input: Int
    var output: Int
}

然而,我们不能这样做,因为结构体不能在Objective-C中表示。相反,我们必须使用一个扩展了NSObject的类

class SquareNumberResult: NSObject {
    var input: Int
    var output: Int

    init(_ input: Int, _ output: Int) {
        self.input = input;
        self.output = output
    }
}

是的,这个类可以包含平方逻辑,但这对本例来说并不重要

然后可以从squareNumber返回这个类的实例

@_cdecl("square_number")
public func squareNumber(input: Int) -> SquareNumberResult {
    let output = input * input
    return SquareNumberResult(input, output)
}

如你所见,从Swift返回NSObject并不困难。然而,Rust实现则不然。squareNumber实际上并没有返回包含inputoutput的结构体,而是返回存储在内存中某个地方的SquareNumberResult的指针。此外,这个值包含的数据不仅仅是inputoutput:由于它是一个NSObject,它还包含额外的数据,这些数据在使用Rust时必须予以考虑。

这听起来可能令人畏惧,但实际上并不是一个问题,多亏了SRObject<T>。这个类型内部管理指针,并接受一个可以访问数据的结构体的泛型参数。让我们看看如何在Rust中实现SquareNumberResult

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<SquareNumberResult>);

然后,使用新的返回值就像直接使用SquareNumberResult一样简单

fn main() {
    let input = 4;
    let result = unsafe { square_number(input) };

    let result_input = result.input; // 4
    let result_output = result.output; // 16
}

在Rust中创建对象然后将其传递给Swift是不支持的。

可选类型

swift-rs也支持Swift的nil类型,但仅限于返回可选NSObject的函数。返回可选原语类型的函数无法在Objective C中表示,因此不受支持。

假设我们有一个返回可选SRString的函数

@_cdecl("optional_string")
func optionalString(returnNil: Bool) -> SRString? {
    if (returnNil) return nil
    else return SRString("lorem ipsum")
}

得益于 Rust 的空指针优化SRString?的可选性质可以通过将 SRString 包装在 Rust 的 Option<T> 类型中来表示!

use swift_rs::{swift, Bool, SRString};

swift!(optional_string(return_nil: Bool) -> Option<SRString>)

空指针实际上是无法在 C 中表示返回可选原语函数的原因。如果支持这一点,如何区分 nil 和数字?这是不可能的!

复杂数据类型

到目前为止,我们只看了使用原语类型和结构体/类,但这忽略了某些最重要的数据结构:数组(SRArray<T>)和字符串(SRString)。然而,这些类型必须谨慎处理,并且不如 Swift & Rust 的本地对应类型灵活。

字符串

字符串可以通过 SRString 在 Rust 和 Swift 之间传递,它可以从两种语言中的本地字符串创建。

作为参数

import SwiftRs

@_cdecl("swift_print")
public func swiftPrint(value: SRString) {
    // .to_string() converts the SRString to a Swift String
    print(value.to_string())
}
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
}

作为返回值

import SwiftRs

@_cdecl("get_string")
public func getString() -> SRString {
    let value = "lorem ipsum"

    // SRString can be created from a regular String
    return SRString(value)
}
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
}

数组

原语数组

正确表示数组很棘手,因为我们不能像 Swift 遵循规则 3 使用泛型作为参数或返回值。相反,swift-rs 提供了一个泛型 SRArray<T>,它可以嵌入到扩展了非泛型 NSObject 的另一个类中,但这仅限于单个元素类型。

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<Int>

    init(_ data: [Int]) {
        self.data = SRArray(data)
    }
}

@_cdecl("get_numbers")
public func getNumbers() -> IntArray {
    let numbers = [1, 2, 3, 4]

    return IntArray(numbers)
}
use swift_rs::{Int, SRArray, SRObject};

#[repr(C)]
struct IntArray {
    data: SRArray<Int>
}

// Since IntArray extends NSObject in its Swift implementation,
// it must be wrapped in SRObject on the Rust side
swift!(fn get_numbers() -> SRObject<IntArray>);

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]);
}

为了简化 Rust 侧的事情,我们实际上可以去掉 IntArray 结构体。因为 IntArray 只有一个字段,其内存布局与 SRArray<usize> 相同,因此我们的 Rust 实现可以在牺牲与 Swift 代码等价性的情况下简化。

// We still need to wrap the array in SRObject since
// the wrapper class in Swift is an NSObject
swift!(fn get_numbers() -> SRObject<SRArray<Int>>);

NSObject 数组

如果我们想返回一个 NSObject 数组,Swift 侧有两种选择

  1. 继续使用 SRArray 和自定义包装类型,或者
  2. 使用 SRObjectArray,这是 swift-rs 提供的包装类型,可以接受任何 NSObject 作为其元素。这可能比继续创建包装类型更容易,但牺牲了一些类型安全性。

还有适用于 Rust 的 SRObjectArray<T>,它与任何单个元素 Swift 包装类型(以及当然 Swift 中的 SRObjectArray)兼容,并自动将元素包装在 SRObject<T> 中,因此几乎没有不使用它的理由,除非你真的很喜欢自定义包装类型。

在 Swift 和 Rust 中使用 SRObjectArray 并与一个基本的自定义类/结构体一起使用,可以这样做

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)
}
use swift_rs::{swift, Int, SRObjectArray};

#[repr(C)]
struct IntTuple {
    item1: Int,
    item2: Int
}

// No need to wrap IntTuple in SRObject<T> since
// SRObjectArray<T> does it automatically
swift!(fn get_tuples() -> SRObjectArray<IntTuple>);

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);
    }
}

复杂数据类型可以包含您喜欢的任何原语和 SRObject<T> 的组合,但请记住遵循 3 个规则!

额外奖励

SRData

SRArray<T> 的包装类型,用于存储 u8 - 实质上就是一个字节数据缓冲区。

使用 autoreleasepool! 紧密控制内存

如果你从 Objective-C 背景转向 Swift,你很可能知道 @autoreleasepool 块的实用性。《swift-rs》也为你提供了支持,只需将代码块用 autoreleasepool! 包裹起来,那么这个代码块现在将执行自己的 autorelease pool!

use swift_rs::autoreleasepool;

for _ in 0..10000 {
    autoreleasepool!({
        // do some memory intensive thing here
    });
}

限制

目前,可以从 Rust 创建的类型只有数字类型、布尔值、SRStringSRData。这是因为这些类型易于在栈上或通过调用 swift 在堆上分配内存,而其他类型则不行。虽然这可能会在未来实现。

跨 Swift 和 Rust 修改值目前不是这个库的目标,它纯粹是为了提供参数和返回值。此外,这会违反 Rust 的编程模型,可能会允许对值的多个共享引用,而不是通过类似 Mutex 的方式实现内部可变性。

许可证

根据您的选择,许可如下

贡献

除非您明确声明,否则您提交的任何旨在包含在作品中的贡献,根据 Apache-2.0 许可证的定义,应按上述方式双许可,不附加任何额外条款或条件。

依赖项

~0.8–1.6MB
~36K SLoC