summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2025-04-15 08:18:45 +0200
committerBug Magnet <marco.nikic@mullvad.net>2025-05-14 16:28:18 +0200
commit4ca32c5c746fbe622c9ae9c0d707bd98e8a5f58b (patch)
tree18d53d440dacfb5710bf698a9f19298af9cf729e
parent90b42c8d08717f54fcc2d9a3ee7135002f3cef50 (diff)
downloadmullvadvpn-codify-swift-rust-interactions-in-a-markdown-file-ios-1140.tar.xz
mullvadvpn-codify-swift-rust-interactions-in-a-markdown-file-ios-1140.zip
-rw-r--r--ios/FFI.md156
-rw-r--r--ios/README.md5
2 files changed, 161 insertions, 0 deletions
diff --git a/ios/FFI.md b/ios/FFI.md
new file mode 100644
index 0000000000..9a72a6ed23
--- /dev/null
+++ b/ios/FFI.md
@@ -0,0 +1,156 @@
+# Rust and FFI
+
+This document is meant to provide best practices and conventions to follow
+when writing FFI code between Swift and Rust.
+
+## String and Data types
+
+- Whenever possible, try to take advantage of Swift's automatic String conversion
+- When dealing with buffers, a pointer to the start of the buffer, and its length should be passed
+- Swift will most of the time do the right thing by passing a pointer to the start of the buffer
+> [!IMPORTANT]
+> Always remember that the pointers passed to the FFI functions are **only valid for the lifetime of the call**
+> If you need to keep the contents of the pointer around for longer, make sure to copy it.
+
+### Examples
+
+Assuming the following FFI function
+
+```C
+void ffi_function(const char *str);
+void other_ffi_function(const uint8_t *address, uintptr_t address_len);
+```
+
+How to call it from Swift
+
+```swift
+let someString = "hello"
+ffi_function(hello)
+
+...
+
+let data = someString.data(using: .utf8)!
+let dataAsArray = data.map { $0 }
+other_ffi_function(dataAsArray, UInt(data.count))
+
+```
+
+## Opaque types
+
+When dealing with opaque types, keep in mind that only types that are natively representable in C can be passed across the FFI boundary.
+Fortunately, opaque types can be declared in such a way that they can be sent across the FFI to be reused later.
+
+### Example
+
+Here is a Rust type that accepts a pointer to an opaque Swift class sent as a `void *` type to Rust
+
+```rust
+#[repr(C)]
+pub struct LoaderWrapperContext {
+ // This pointer is a reference to a Swift object, and is only ever read by Rust.
+ // It is used to call that Swift object across the FFI
+ loader: *const c_void,
+}
+
+#[repr(C)]
+pub struct SwiftLoaderWrapper(LoaderWrapperContext);
+impl SwiftLoaderWrapper {
+ pub fn new(context: LoaderWrapperContext) -> SwiftLoaderWrapper {
+ SwiftLoaderWrapper(context)
+ }
+}
+```
+
+This allows to have a clean separation of the `unsafe` FFI boundary with a safe API in Rust
+
+```rust
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn init_swift_loader_wrapper(
+ loader: *const c_void,
+) -> SwiftLoaderWrapper {
+ let context = LoaderWrapperContext { loader };
+ SwiftLoaderWrapper::new(context)
+}
+
+impl SwiftLoaderWrapper {
+ pub fn safe_call(&self) -> Option<()> {
+ let context = self.context_ref();
+ Some(context.load_things())
+ }
+
+ fn context_ref(&self) -> &LoaderWrapperContext {
+ &self.0
+ }
+}
+
+impl LoaderWrapperContext {
+ pub fn load_things() -> Option<()> {
+ Some(unsafe { use_loader(self.loader) })
+ }
+}
+```
+
+
+Following the example above, here's how to provide an API from swift (That used from Rust in the previous example)
+
+```swift
+public func initLoaderWrapper(loader: Loader) -> SwiftLoaderWrapper {
+ let rawLoader = Unmanaged.passUnretained(loader).toOpaque()
+ return init_swift_loader_wrapper(rawLoader)
+}
+
+@_cdecl("use_loader")
+func useLoader(rawLoader: UnsafeMutableRawPointer?) {
+ guard let rawLoader else { return } }
+ let loader = Unmanaged<Loader>.fromOpaque(rawLoader).takeUnretainedValue()
+}
+```
+
+### More opaque types examples
+
+### Digesting C style arrays
+As there are no indications that some data type passed across the FFI boundary is a collection type,
+the documentation has to be explicit about what's being sent.
+
+As seen with the `other_ffi_function` example, a pointer to the start of the collection must be passed, alongside the number of elements in the collection.
+If the collection is contiguous in memory, it can be read directly from the raw parts like so.
+> [!IMPORTANT]
+> Make sure to read the warning notice on `from_raw_parts`
+
+```rust
+unsafe fn generic_array_conversion_example<A>(raw_array: *const c_void, elems: usize) -> Vec<A>
+where
+ A: Sized,
+{
+ let raw_array: *mut *mut A = raw_array as _;
+ // SAFETY: `raw_array` must be aligned, non-null and initialized for `count` reads
+ let slice = unsafe { slice::from_raw_parts(raw_array, elems) };
+ slice
+ .iter()
+ // SAFETY: Safety comment
+ .map(|&ptr| ... /* Turn the raw pointer into type A here */ )
+ .collect()
+}
+```
+
+### Boxing types
+
+`Box`es are a very convenient way to transport opaque data around.
+Chances are that you already have written some code that interacts with `Box` in one way or another.
+
+Things to keep in mind when working with Box:
+- Calling `Box::into_raw` *consumes* the boxed value.
+ - In other words, the caller is responsible for making sure the memory managed by the `Box` instance that was turned to a raw pointer is taken care of.
+- Calling `Box::from_raw` is the opposite operation and consumes the raw pointer, creating a `Box` in the process, and freeing the allocated memory for the raw pointer passed to it.
+- Try not to move a pointer around after it's been boxed
+- Always make sure to have matching calls to `into_raw` and `from_raw`
+
+### Safety
+
+Raw pointers are always *unsafe* to use in Rust.
+This is why the separation of concerns is important, and you should always strive
+to segregate the use of raw pointers and more idiomatic Rust code. Some guidelines to keep in mind
+
+- Try to limit the scope of `unsafe` blocks to just where it's needed
+- When working with `SAFETY: ` notices, describe the assumptions that are made at the call site, and the expectations placed upon the `unsafe` block
+- Draw the line carefully between `safe` and `unsafe` Rust, try whenever possible to handle all the unsafe code in one single place (to make it easier to review) and build the safe layers upon that.
diff --git a/ios/README.md b/ios/README.md
index bf05b8fd60..364366455c 100644
--- a/ios/README.md
+++ b/ios/README.md
@@ -103,3 +103,8 @@ ios/convert-assets.rb --additional-assets
The script `relays-prebuild.sh` runs on each Xcode build and will download and cache a list of relays if it is not already present for a given configuration.
The cached list for a given configuration will always override the current relays file.
To get a fresh relay file on demand, issue a `clean` command to Xcode and re-build the project.
+
+## Rust and FFI
+The application code makes extensive use of FFIs to leverage all the code written in Rust that can be reused across multiple platforms.
+
+As such, we wrote some [documentation](./FFI.md) to describe what best practices we are following.