用 Rust 搭建一個小程式執行環境
搭建一個FinClip社群版docker執行環境,安裝設定Rust開發編譯iOS程式碼的環境,設定xcode的專案配合,整合FinClip SDK,準備好實現從FinClip小程式到Rust演算法邏輯的端到端融合。
本篇以 iOS 為例介紹開發環境的準備。
從零到一:構建一個能執行小程式的App
我們先從FinClip官網下載最新的FinClip SDK,解壓後應獲得 FinApplet.framework、FinAppletExt.framework、FinAppletWebRTC.framework、FinAppletBLE.framework 等一系列庫。
用 xcode 建立一個新專案,簡單起見我們建一個基於 Objective-C 的 Storyboard。
此處注意,我們要小心命名這個 App 並記住它的 Bundle ID,如下圖,我們這個App 的 Bundle ID 是 com.finogeeks.rustful.clip。
然後把 FinClip SDK 解壓包裡的 FinApplet.framework 新增至工程裡,注意勾選“Copy items if needed”。
在macOS 11.1以上使用xcode較新的版本(筆者所用版本為13.0)編譯上述專案,會出現報錯無法繼續,如果你有這個情況,可在專案Build Settings處作以下配置。
clip.xcodeproj Building for iOS Simulator, but the linked and embedded framework 'FinApplet.framework' was built for iOS + iOS Simulator
報錯資訊
Apple 從 Xcode 12.3 開始推薦使用 xcframework 替代 Framework,本文所依賴的FinClip SDK 2.36.5 尚未提供 xcframework 版本,所以有上述問題,編譯過程且有系列warning,但不影響執行。在未來版本應會被解決。
FinClip SDK 中包含 x86_64 架構,便於我們開發時用模擬器除錯。本文主要目的是試驗在 iOS 上 FinClip 小程式和 Rust 程式碼的整合,以能執行在 simulator 為要。但是x86_64 架構的 SDK,打包上傳應用市場時會報錯,如何打包時自動去除模擬器架構的指令碼,可以讓我們既可以用模擬器開發除錯,又能正常提交應用市場,不在本文探討範圍,詳情可參考官網iOS整合。
FinClip 安全沙箱的初始化
FinClip SDK 程式碼庫成功編譯構建至 App後,是時候進行程式碼整合。這裡包括註冊生成 SDK Key 和 SDK Secret,用最少至僅 4 行程式碼即可在 App 中把 FinClip SDK 初始化,準備好載入執行 FinClip 小程式。
獲得 SDK Key 以及 SDK Secret 的兩種方式
FinClip 技術分成端側和雲(伺服器)側兩大部分,端側即 FinClip SDK,雲(伺服器)側則是 FinClip 小程式管理中心/小程式商店,用於實時、動態管理小程式的上下架以及小程式開發者的管理(正如你所熟悉的網際網路小程式平臺一樣)。凡泰極客提供整套方案的兩種部署使用方式:
- FinClip.com Managed Service 方式:即由凡泰極客運行雲側,開發者把小程式的上下架管理託管。從而降低自己在伺服器端的運維成本
- On-Premise 方式:即由開發者或開發者所在的機構,自行部署運維FinClip伺服器側,自行管理自己的開發者,自行管控自己的小程式開發生態。普通開發者也可以自行免費體驗和使用社群版(功能和企業版版無異),在一臺個人電腦即可以執行完整環境。
取決於我們打算用誰的伺服器端,則 SDK Key 和SDK Secret 需要在該伺服器生成,因為最終 App 所嵌入的 SDK 需要被所連線的目標伺服器作安全授權。
方式一:採用 FinClip.com 託管服務
這是最簡單直接的方式,也就是說我們準備開發的小程式,將上架至 FinClip.com。(注意:本系列所描述內容的驗證,需要使用自己部署安裝的社群版。FinClip.com服務在此為了完整起見作簡單介紹)。
首先,需到 FinClip.com 註冊一個開發者賬戶。
其次,登入後在管理頁面「應用管理-新增合作應用」,新增要整合 SDK 的目標應用。
具體操作詳情見關聯移動端應用
你將獲得類似以下的 Key 和 Secret:
準備把它們貼上、複製至初始化的程式碼中。
方式二:自行部署 FinClip 社群版
如果閣下按捺不止自己動手搭建一套 FinClip、擁有一個自己掌控的小程式商店,那麼也可以輕而易舉的在自己的開發環境部署個社群版(作為前置條件,注意先安裝好 docker 相關工具):
mkdir my-finclip
cd my-finclip
sudo sh -c "$(curl -fsSL http://static.finogeeks.club/deploy/mop/release/install.sh)"
成功安裝後,在上述目錄下執行:
docker-compose up -d
假如你用 MacOS 上的 Docker Desktop,開啟 Dashboard 應能看到下圖,其中每一個 container 都應該處於 running 狀態(除了mop-init "EXITED(0)" 為正常)。
此時 FinClip 管理後臺(分成面向開發者的“企業端”以及面向運營管理者的“運營端”)可通過以下 URL 訪問:
登入企業端與運營端的預設使用者名稱為“[email protected]”,密碼為“123Abc”。
首先,我們自己扮演管理角色,在運營端登記自己準備開發移動端應用的 Bundle ID,Bundle ID 是你在 Apple App Store 或者某個 Android 應用商店準備釋出的 App 的應用標識。
在這裡作為例子,我們新增了一個 Bundle ID "com.finogeeks.rustful.clip"(記得之前在 Xcode 建立 App 的時候所定義的名字):
輸入後在 FinClip 也關聯了同樣的 Bundle ID
其次,我們扮演開發者角色,到企業端中,新增一款合作應用,姑且稱之為 rust-ios,並關聯相應的 Bundle ID:
至此,我們把以下資訊關聯了起來:
- 我們要開發的 App 名稱,在這個例子裡,叫“rust-ios”
- 這個 App 的 Bundle ID 是:com.finogeeks.rustful.clip,它將適用於iOS和Android,雖然在本文我們只針對 iOS 作開發。我們首先是在xcode建立專案的時候採用了這個 ID,現在我們把它登記到 FinClip,目的是讓平臺知道一個小程式可以執行在什麼 App 中;
- FinClip SDK 嵌入到這個 App 時,需要使用一對指定的 Key 以及 Secret 去對接伺服器端
- 伺服器端,取決於你用的是 FinClip.com的託管/SaaS 服務,還是用自己部署的社群版。前者的 API Server是api.finclip.com;後者的話,預設是127.0.0.1:8000。
FinClip SDK 在 App 中的初始化
現在我們準備好在 Xcode 建立的 clip 專案中寫初始化 SDK 的程式碼。在AppDelegate.m,加入以下程式碼:
``` // // AppDelegate.m // clip //
import "AppDelegate.h"
import
@interface AppDelegate ()
@end
@implementation AppDelegate
-
(BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary )launchOptions {
NSString appKey = @"22LyZEib0gLTQdU3MUauARgvo5OK1UkzIY2eR+LFy28NAKxKlxHnzqdyifD+rGyG"; FATConfig config = [FATConfig configWithAppSecret:@"8fe39ccd4c9862ae" appKey:appKey]; config.apiServer = @"http://127.0.0.1:8000"; [[FATClient sharedClient] initWithConfig:config error:nil]; [[FATClient sharedClient] setEnableLog:YES];
return YES; }
pragma mark - UISceneSession lifecycle
-
(UISceneConfiguration )application:(UIApplication )application configurationForConnectingSceneSession:(UISceneSession )connectingSceneSession options:(UISceneConnectionOptions )options { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; }
-
(void)application:(UIApplication )application didDiscardSceneSessions:(NSSet
@end ```
Rust 開發環境的準備
安裝R ust 環境比較簡單,例如在 Mac/Linux上,一行指令碼即可:
curl --proto '=https' --tlsv1.2 -sSf http://sh.rustup.rs | sh
其他相關內容可參考官網。
為了能把 Rust 程式碼編譯成 iOS、Android 的元件庫,我們需要安裝一些平臺架構的target:
```
Android targets
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
iOS targets
rustup target add aarch64-apple-ios armv7-apple-ios armv7s-apple-ios x86_64-apple-ios i386-apple-ios ```
此外,我們還需要安裝兩個工具,用於構建 iOS 的 universal library,以及從 Rust 程式碼生成 C/C++ 標頭檔案,供 Objective-C/Swift 的專案在匯入靜態庫時使用:
```
安裝 Xcode build tools(如果已經安裝,請忽略)
xcode-select --install
這個cargo subcommand用於構建iOS上的universal library
cargo install cargo-lipo
這個工具用於自動生成 C/C++11 標頭檔案
cargo install cbindgen
在Android環境,請先安裝Android Studio和NDK,但不在本文討論範圍
cargo install cargo-ndk ```
關於 cargo-lip 的介紹,可以看這裡,關於 cbindgen,可以參考這裡。但實際上你也可以以後有興趣慢慢看,知其然不知其所然在這裡沒毛病,不影響使用。
Rust 程式碼編譯成 iOS 靜態庫的驗證
在開始正式的開發前,我們可以寫一個簡單的“Hello World”驗證一下上述環境。首先用 cargo 建立一個新的 Rust 的 Library 工程型別的專案,原因是我們會把這個library 匯入到 iOS 的專案中並把其中函式註冊至 FinClip SDK 供小程式側通過 JavaScript 介面呼叫。
cargo new --lib hello
Cargo 自動生成以下目錄:
hello
|--src
| |--lib.rs
|--Cargo.toml
我們的 Cargo.toml 如下:
``` [package] name = "rustylib" version = "0.1.0" authors = ["me [email protected]"] edition = "2021"
[lib] name = "rustylib"
構建iOS和Android版本需要的兩個crate
crate-type = ["staticlib", "lib"]
編譯Android版時需要,本文不涉及,但列出在此供Android開發者參考
[target.'cfg(target_os = "android")'.dependencies] jni = { version = "0.19.0", default-features = false } ```
現在我們修訂一下 lib.rs。因為這個實驗專案並不是為了簡單跑一個 Rust 'Hello World',而是為了驗證輸出一個可以供異構語言呼叫的 C Library,所以在這裡我們用了 Rust FFI(Foreign Function Interface)來寫(看上去比“正常”的'Hello World'複雜):
``` // lib.rs
use std::ffi::{CStr, CString}; use std::os::raw::c_char;
[cfg(target_os = "android")]
mod android;
[no_mangle]
pub unsafe extern "C" fn hello(to: const c_char) -> mut c_char { let c_str = CStr::from_ptr(to); let recipient = match c_str.to_str() { Ok(s) => s, Err(_) => "you", };
CString::new(format!("From Rust: {}", recipient))
.unwrap()
.into_raw()
}
[no_mangle]
pub unsafe extern "C" fn hello_release(s: *mut c_char) { if s.is_null() { return; } drop(CString::from_raw(s)); }
[no_mangle]
pub extern "C" fn hello_world() { println!("Hello, World"); } ```
Rust 編譯器編譯程式碼時,會修改我們定義的函式名稱,增加一些用於其編譯過程的額外資訊。為了使 Rust 函式能在其它語言(例如Objective-C、Swift)中被呼叫,必須禁用 Rust 編譯器的名稱修改功能。所以我們使用了 no_mangle 的函式屬性宣告去指示編譯這些準備註冊到 FinClip SDK 的函式。
另外,我們還使用了 extern "C"的宣告,以告知編譯器這些被如此宣告的函式是為了供 Rust 以外的其他語言程式碼呼叫,編譯器需要保證按C語言的標準規範去編譯輸出。
其他更多關於 FFI(Foreign Function Interface)以及 unsafe 等 Rust 語言的能力,不是本文焦點,可參考 Rust 相關方面的內容。在本系列後面的章節也會繼續涉及。
為了能驗證一下上述函式能否執行,我們編寫一個測試例子:
cd hello
mkdir examples
touch examples/test.rs
一個簡單的測試如下:
``` // test.rs
use std::ffi::{CStr, CString}; use rustylib::{hello, hello_release};
fn main() { let input = CString::new("Hello, world!").unwrap();
unsafe {
let c_buf = hello(input.as_ptr());
let slice = CStr::from_ptr(c_buf);
println!("{}", slice.to_str().unwrap());
hello_release(c_buf);
}
} ```
在 hello 專案的根目錄下,執行測試:
cargo run --example test
應產生如下結果:
Blocking waiting for file lock on build directory
Compiling rustylib v0.1.0 (/Users/myself/projects/hello)
Finished dev [unoptimized + debuginfo] target(s) in 5.89s
Running `target/debug/examples/test`
From Rust: Hello, world!
現在可以嘗試為 iOS 進行編譯:
$ cargo lipo --release
我們可以檢查一下生成的靜態庫:
``` lipo -info target/aarch64-apple-ios/release/librustylib.a Non-fat file: target/aarch64-apple-ios/release/librustylib.a is architecture: arm64
ipo -info target/x86_64-apple-ios/release/librustylib.a
Non-fat file: target/x86_64-apple-ios/release/librustylib.a is architecture: x86_64
```
但我們用於開發的是一個合併了上述兩個架構的通用庫 Fat library,在以下目錄中:
ls -l target/universal/release/librustylib.a
要在 iOS 驗證這部分程式碼,可以先生成一個 C 的標頭檔案,在 hello 這個 Rust 專案的根:
cbindgen src/lib.rs -l c > rustylib.h
然後把這個標頭檔案新增至AppDelegate.m,再對其進行修訂,把'hello'和'hello_release'直接按C的方式呼叫一下即可。程式碼非常簡單,不在此贅述。但是在xcode中需要把上述生成的librustylib.a以及rustylib.h新增至專案中(如果不是iOS開發者不熟悉xcode,可以跳過本部分驗證,繼續閱讀本系列後續篇章的詳細介紹)。
至此,我們把iOS Native App、FinClip SDK和Rust library三個部分整合起來,接下來的內容,將是聚焦開發一個比“Hello World”複雜點的、確實適合用Rust實現的library,並讓它通過FinClip小程式來展現人機互動的介面。開發過程所用到的工具有點多,你需要:
- xcode:用於編譯構建“殼”應用,以及通過simulator測試你的應用
- FinClip IDE:用於開發除錯小程式
- FinClip.com(或者執行在你本地電腦上的FinClip社群版)的企業端和運營端
- vscode以及一些有助於開發測試Rust程式碼的extension(當然,你也可以用其他vscode替代工具)
對於首次開發Rust的朋友,在vscode推薦安裝以下extension:
- Better TOML,用於支援Cargo.toml檔案的syntax highlight
- crates,用於支援Cargo.toml中crate的版本依賴關係管理
- rust-analyzer,似乎優於官方的rust extension
- CodeLLDB,能支援C++、Rust等編譯語言的debugger
- Tabnine AI Auto-complete,一句話,智慧好使
That's for now。