【Rust】Rust與Swift相遇…

經過了這麼多年,個人算在iOS上算是小有成就,能混口飯吃,這篇也算是補上之前純Swift編成Framework的一個補充包,只要是能轉成C語言二進制的,通通都可以接在iOS系統上,iOS生可謂是功德圓滿啊,不過呢,最近AI是越來越強大了,像我這種還在寫套件 / 文章的人越來越少了,因為只要問一下AI,程式馬上就出來了,但是個人覺得還是自己寫的最香,就算是用AI問出來的,我還是會去了解,改成自己喜歡的樣子,我對寫程式可算是真愛啊…

做一個長得像這樣的東西

作業環境

項目 版本
macOS Sequoia 15.7
Visual Studio Code 1.108.0
Rust 1.90.0
Xcode 16.4

建立Rust Library庫

  • 因為我們是要做一個套件庫,所以要加上--lib字樣…
  • 而裡面會有一個lib.rs,而不是main.rs
cargo new --lib rust_caesar

凱撒密碼

  • 還記得之前有提到有關加密的文章,讓就來做個最簡單的凱撒密碼吧…
  • 其實凱薩密碼就是簡單的字母移位,程式碼很簡單,就不多做說明了…
use std::ffi::{CString, CStr};
use std::os::raw::c_char;

#[unsafe(no_mangle)]
pub extern "C" fn caesar_encrypt(input: *const c_char, shift: u8) -> *mut c_char {
    unsafe {
        let c_str = CStr::from_ptr(input);
        let rust_str = c_str.to_str().unwrap();
        let encrypted = _caesar_encrypt(rust_str.to_string(), shift).unwrap();
        CString::new(encrypted).unwrap().into_raw()
    }
}

#[unsafe(no_mangle)]
pub extern "C" fn caesar_free(ptr: *mut c_char) {
    unsafe { let _ = CString::from_raw(ptr); }
}

fn _caesar_encrypt(text: String, shift: u8) -> Result<String, String> {

    Ok(text.chars()
        .map(|c| if c.is_ascii_uppercase() {
            ((c as u8 - b'A' + shift) % 26 + b'A') as char
        } else if c.is_ascii_lowercase() {
            ((c as u8 - b'a' + shift) % 26 + b'a') as char
        } else { c }
        )
        .collect()
    )
}
  • 其中extern “C”,就是把這支func轉成C語言的形式輸出,畢竟Objective-C可以直接接C語言,後面使用起來方便很多…
  • 再來就是#[unsafe(no_mangle)]了,因為在編成二進制後,func的名稱就會改變,有可能是Y2Flc2FyX2VuY3J5cHQ=(),在程式內部是沒問題的,但在外部用的話,這樣子我們就沒辦法知道func名稱,所以加上這個註記,就是在生成後不要改變func名稱,不然要用就麻煩了…

編成二進制標準函式庫 (.a)

  • 先在Cargo.toml上加入crate-type = ["staticlib"],因為我們要編成的是靜態函式庫 - Static Library Archive (.a)嘛…
[package]
name = "rust_caesar"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["staticlib"]

[dependencies]
  • 然後就給它編譯下去,這裡形式選擇iOS實機aarch64-apple-ios / M系列模擬器 aarch64-apple-ios-sim的版本…
  • target資料夾的位置,就可以看到.a檔了,Congratulations !
cargo build --target aarch64-apple-ios --release
cargo build --target aarch64-apple-ios-sim --release

  • 當然如果要編成x86_64也是可以的,可以使用以下的指令查詢Rust支援的項目列表…
rustc --print target-list
rustc --print target-list | head -1

接下來我們來使用它

  • 先把模擬器版的.a檔加在Swift專案之中…
  • 再加入一個Header.h檔,寫出我們要給外部知道哪些是可以用的函數…
  • 目前可用的函數就是caesar_encrypt()caesar_free()兩個…

#ifndef CAESAR_RUST_H
#define CAESAR_RUST_H

#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

char* caesar_encrypt(const char* input, uint8_t shift);

void caesar_free(char* ptr);

#ifdef __cplusplus
}
#endif

#endif

再來就是見證奇蹟的時刻

  • 接下來就是大家常常接Objective-C程式碼的橋接功能了…
  • Header.h引入後,就可以給Swift使用了…
  • 所以…換上實機的.a檔就可以給實機用了…
#import "Header.h"
import UIKit

final class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let cString = caesar_encrypt("Hello,World!", 3)
        print(String(cString: cString))
    }
}

模組地圖 - modulemap

  • 但如果每次都要這樣子設定是不是很麻煩?
  • 於是就有了module.modulemap這個的設定檔,把在Xcode要在Build Settings填入的設定,事先就寫好,也一起設定了模組名稱…
  • 其實在Xcode新增Bridging-Header.h時,就幫我們做了這個在Build Settings填入的基本設定,不過還是會有其它的設定值要填…
Framework Search Paths = $(SRCROOT)
Header Search Paths = $(SRCROOT)
Library Search Paths = $(SRCROOT)
Other Linker Flags = -force_load librust_caesar.a
  • 我們先在Rust中加入headers資料夾,把Header.hmodule.modulemap放在一起…
  • module.modulemap中的內容,就是把.h.a的位置告訴Xcode,也就是在Build Settings裡面寫的值…
  • 之所以副檔名取名為.modulemap,就是指找module的地圖,Xcode根據這張地圖就能找到相對應的檔案了,是不是很厲害啊…
module CaesarRust [system] {
    header "Header.h"
    link "libcaesar_rust"
    export *
}

就是愛xcframework

  • 接下來就是把這些.a + .h + .modulemap打包成.xcframework
  • 打包完成之後,不管是實機,或者模擬器,就變成是通用了…
  • 先編譯成各平台.a檔…
cargo clean
cargo build --target aarch64-apple-ios --release
cargo build --target aarch64-apple-ios-sim --release
  • 先把這些.a + .h + .modulemap包起來…
xcodebuild -create-xcframework \
  -library target/aarch64-apple-ios/release/librust_caesar.a \
  -headers headers \
  -library target/aarch64-apple-ios-sim/release/librust_caesar.a \
  -headers headers \
  -output CaesarRust.xcframework

馬上來試用

  • 最後,當然就是把打包好的.xcframework拿來用看看了…

import UIKit
import CaesarRust

final class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let cString = caesar_encrypt("Hello,World!", 3)!
        print(String(cString: cString))
    }
}
  • 其它還有一些關於xcframework的指令,大家可以試用看看…
# 檢查結構
find CaesarRust.xcframework -name "*.a" -o -name "Headers"

# 檢查 .a 內容
lipo -info CaesarRust.xcframework/ios-arm64/libcaesar_rust.a
lipo -info CaesarRust.xcframework/ios-arm64-simulator/libcaesar_rust.a

# 檢查 plist
plutil -p CaesarRust.xcframework/Info.plist | grep -A10 LibraryIdentifier

範例程式碼下載

後記

以前我曾經聽過前輩說,其實程式碼寫得亂的才是大師,因為都是照著他的想法,連續寫下來的,也就是說,程式是有前後關係的;而寫得乾淨的,基本上都是些簡單的程式,才有辦法切得那麼開。後來想想好像也對,像我程式碼分得那麼細,還80%以上有註解的程式碼,真的是少見啊,因為也是要花二倍的時間整理,而在畫面功能上,一點也沒變,真的是浪費時間,但是因為我不是個天才,所以慢工出細活,套件化對我來說,也是減少了再動腦一次的機會,畢竟我不是個聰明人,要再想一次是很累的啊,直接用不是很好嘛…