用 Rust 搭建一個小程式執行環境

語言: CN / TW / HK

搭建一個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 https://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 )sceneSessions { // Called when the user discards a scene session. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. }

@end ```

Rust 開發環境的準備

安裝R ust 環境比較簡單,例如在 Mac/Linux上,一行指令碼即可:

curl --proto '=https' --tlsv1.2 -sSf https://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。