Skip to content

Commit

Permalink
Document Swift String copying (#318)
Browse files Browse the repository at this point in the history
This commit adds documentation explaining that there is is no zero-copy
way to create a Swift `String`.

We also add documentation explaining that the reason the `RustString`
type exists is to avoid the implicit allocation that would be required
if we automatically converted Rust's `std::string::String` to Swift's
`String`.

We also document some potential new attributes that would allow users to
opt into automatically converted from a `Rust std::string::String` to a
Swift String.
  • Loading branch information
chinedufn authored Feb 6, 2025
1 parent 4827500 commit 864cb4e
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 9 deletions.
14 changes: 5 additions & 9 deletions book/src/built-in/string/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# String <---> String
# Rust `std::string::String` <---> Swift `String`

Rust's `std::string::String` can be passed to Swift as an owned `String`, a referenced `&String` or a mutably referenced `&mut String`.

Expand Down Expand Up @@ -49,15 +49,11 @@ func make_swift_string() -> String {

## RustString

Since both Rust and Swift have their own `String` type, it is technically possible for us to automatically
convert owned Rust `std::string::String`s into Swift `String`s.
It is technically possible for us to automatically convert owned Rust `std::string::String`s into Swift `String`s,
but we do not do this because it would require copying the Rust `std::string::String` bytes to a new `Swift` allocation.

However, we do not do this since Swift does not currently have a way to construct a Swift `String` without copying.
This is because there is no way to construct a Swift `String` without copying.

This means that if we were to automatically convert from a Rust `std::string::String` to a Swift `String` we would have to create a new
Swift allocation and copy all of the Rust `std::string::String` bytes to that allocation.

`swift-bridge` seeks to avoid unnecessary allocations, so instead of performing this implicit allocation
we pass a `RustString` type from Rust to Swift.
`swift-bridge` seeks to avoid unnecessary allocations, so instead of performing an implicit allocation we pass a `RustString` type from Rust to Swift.

The `RustString`'s `.toString()` method can then be called on the Swift side to get a Swift `String`.
49 changes: 49 additions & 0 deletions src/std_bridge/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,55 @@
//! crates/swift-bridge-build/src/generate_core/rust_string.{c.h,swift}
pub use self::ffi::*;

/// Ideally, we would bridge Rust's [`String`] to Swift's `String` type directly.
/// We do not do this because there is no zero-copy way to create a Swift `String`, and, since
/// `swift-bridge` aims to be useful in performance sensitive applications, we avoid unnecessary
/// allocations.
///
/// Instead, users that wish to go from a `Rust std::string::String` to a Swift String must call
/// `RustString.toString()` on the Swift side.
/// We can consider introducing annotations that allow a user to opt in to an automatic conversion.
/// For instance, something along the lines of:
/// ```rust,no_run
/// #[swift_bridge::bridge]
/// mod ffi {
/// extern "Rust" {
/// #[swift_bridge(return_clone)]
/// fn return_a_string() -> String;
///
/// #[swift_bridge(return_map_ok_clone)]
/// fn return_a_string_ok() -> Result<String, ()>;
///
/// #[swift_bridge(return_map_err_clone)]
/// fn return_a_string_err() -> Result<(), String>;
/// }
/// }
/// ```
/// When such an attribute was present `swift-bridge` would allocate a Swift String on the Swift
/// side, instead of initializing an instance of the `RustString` class.
///
/// Such an automatic conversion could be made more efficient than using the `RustString.toString()`
/// method to create a Swift String.
/// For instance, to go from `Rust std::string::String -> Swift String` via a `RustString` we:
/// - allocate a `class RustString` instance
/// - call `RustString.toString()`, which constructs a Swift String using the `RustString`'s
/// underlying buffer
///
/// An automatic conversion would look like:
/// - construct a Swift String using the Rust `std::string::String`'s underlying buffer
///
/// Regardless of whether one is using `swift-bridge`, creating instances of Swift reference types
/// requires a small heap allocation.
/// By not creating an instance of the `RustString` class we would be eliminating one small
/// allocation.
///
/// ## References
/// - claim: Impossible to create a Swift `String` without copying:
/// - `init(bytesNoCopy was deprecated in macOS 13` - https://forums.swift.org/t/init-bytesnocopy-was-deprecated-in-macos-13/61231
/// - "String does not support no-copy initialization" - https://developer.apple.com/documentation/swift/string/init(bytesnocopy:length:encoding:freewhendone:)
/// - `Does String(bytesNoCopy:) copy bytes?` - https://forums.swift.org/t/does-string-bytesnocopy-copy-bytes/51643
/// - claim: Class instances allocate
/// - "For example, a class instance (which allocates)" https://www.swift.org/documentation/server/guides/allocations.html#other-perf-tricks
#[swift_bridge_macro::bridge(swift_bridge_path = crate)]
mod ffi {
extern "Rust" {
Expand Down

0 comments on commit 864cb4e

Please sign in to comment.