Điều gì sẽ xảy ra nếu tôi nói với bạn rằng bạn có thể sử dụng một bộ mã cho cả Android, iOS thậm chí là Flutter mà vẫn hiệu quả. Trong bài viết này, bạn sẽ hiểu được quy trình đạt được điều này với Rust. 

Nhưng, tại sao chúng ta muốn điều gì đó giống như thế này ?

Hãy tưởng tượng rằng bạn có một ứng dụng di động cần xử lý một số âm thanh để có được một số thông tin chi tiết về người dùng nhưng bạn không muốn âm thanh này được gửi đến máy chủ xử lý. Bạn muốn giữ gìn sự riêng tư của người dùng. Trong tình huống này, một cải tiến mới mẻ hơn là không viết thư viện cho Android và iOS. Điều này sẽ tiết kiệm cho chúng tôi không phải duy trì hai codebase khác nhau và hơn nữa sẽ giảm việc gặp lỗi nhiều hơn. 

Điều đó rất tuyệt, nhưng làm thế nào để chúng ta có thể làm điều gì đó như thế này ? Nhập Rust. Với Rust, bạn không chỉ có thể chia sẻ cùng một mã giữa nhiều nền tảng mà còn có thể tận dụng sự tăng cường hiệu suất mà bạn sẽ nhận được. 

Chúng ta đang làm gì

Chúng ta sẽ viết một thư viện Rust được chia sẻ đơn giản và biên dịch nó cho Android và iOS, và như đó là một phần thưởng, chúng tôi cũng sẽ viết một plugin Flutter bằng cách sử dụng cùng một mã.

Như bạn có thể thấy, phạm vi của bài viết này khá rộng nên chúng tôi sẽ cố gắng sắp xếp mọi thứ theo trình tự. 

Bạn cũng có thể đọc bài viết này trong khi xem kho lưu trữ Github liên quan. 

Khởi đầu dự án

Bắt việc bằng việc tạo một thư mục có tên là Rust-for-android-iOS-flutter và tạo bốn thư mục trong đó ( android, iOS, flutter & Rust )

mkdir rust-for-android-ios-flutter
cd rust-for-android-ios-flutter
mkdir ios android rust flutter

Khi đã có thư mục, chỉ cần cd vào thư mục rust và tạo một thư viện Rust mới có tên là Rustylib : 

cd rust
cargo init --name rustylib --lib

Thư viện này sẽ có một hàm lấy một chuỗi string làm đối số của nó và sẽ trả về một chuỗi string mới. Về cơ bản, chỉ là một Hello, World! Nhưng chỉ cần nghĩ về nó như một hàm có thể làm việc như là một điểm khởi đầu cho một quá trình truy cập phức tạp hơn hoàn toàn được viết bằng Rust. 

Cài đặt một vài targets

Để biên dịch thư viện rustylib của chúng tôi sang Android và iOS, chúng tôi sẽ cần cài đặt một số targets trong hệ thống máy : 

# 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

Công cụ cho iOS 

Đối với iOS chắc chắn XCode sẽ là công cụ cần thiết để cài đặt cho việc xây dựng chương trình. 

# install the Xcode build tools.
xcode-select --install
# this cargo subcommand will help you create a universal library for use with iOS.
cargo install cargo-lipo
# this tool will let you automatically create the C/C++11 headers of the library.
cargo install cbindgen

Như bạn có thể thấy, chúng tôi cũng đã cài đặt cargo-lipo and cbindgen.

Công cụ cho Android

Đối với Android, chúng tôi phải chắc chắn đã thiết lập biến môi trường  $ ANDROID_HOME.  Trong macOS, phần này đã được thiết lập thành ~/Library/Android/sdk.

Bạn cũng lên cài đặt Abdroid studio và NDK. Khi bạn đã cài đặt mọi thứ. hãy đảm bảo rằng biến môi trường $ NDK_HOME được đặt đúng. Trong macOS, phần này nên được đặt thành ~/Library/Android/sdk/ndk-bundle.

Cuối cùng, chúng tôi sẽ cài đặt Freight-ndk, xử lý việc tìm kiếm các trình liên kết chính xác và chuyển đổi giữa các bộ ba được sử dụng trong thế giới Rust sang bộ ba được sử dụng trong thế giới Android : 

cargo install cargo-ndk

Cấu hình thư viện Rust 

Bước tiếp theo là thực hiện sửa đổi cargo.toml của chúng tôi. Hãy chắc chắn nó giống như thế này : 

[package]
name = "rustylib"
version = "0.1.0"
authors = ["Roberto Huertas <roberto.huertas@outlook.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
name = "rustylib"
# this is needed to build for iOS and Android.
crate-type = ["staticlib", "cdylib"]

# this dependency is only needed for Android.
[target.'cfg(target_os = "android")'.dependencies]
jni = { version = "0.13.1", default-features = false }

Dự án iOS 

Bây giờ, Hãy tạo một project iOS sử dụng Xcode 

Trong iOS bạn có thể sử dụng 2 loại giao diện người dùng khác nhau. Vì chúng tôi muốn chỉ ra cách sử dụng cả hai loại này, vậy nên tôi sẽ trình bày việc tạo cả 2 project khác nhau 

Storyboard 

Chúng ta đã chọn storyboard làm giao diện người dùng và ta sẽ đặt tên cho project là rusty-ios-classic.

Lưu file trong thư mục iOS đã tạo trước đó.

Swift UI 

Tạo một project mới. Nhưng lần này, ta sẽ chọn SwiftUI làm giao diện người dùng interface và đặt tên là ruby-ios.

Lưu file vào thư mục iOS trước đó. 

Khi đó "treeview" của bạn sẽ như thế này : 

Viết chương trình Rust đầu tiên

 Bây giờ, ta đi đến project Rust, mở file lib.rs và viết chương trình chính xác giống như vậy : 

use std::ffi::{CStr, CString};
use std::os::raw::c_char;

#[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!("Hello from Rust: {}", recipient))
        .unwrap()
        .into_raw()
}

#[no_mangle]
pub unsafe extern "C" fn hello_release(s: *mut c_char) {
    if s.is_null() {
        return;
    }
    CString::from_raw(s);
}

Thuộc tính # [no_mangle] attribute là để tránh trình biên dịch thay đổi tên hàm. Nếu muốn tên của hàm được xuất như hiện tại. 

Cũng lưu ý rằng ta sử dụng extern "C". Điều này cho trình biên dịch biết rằng hàm sẽ được gọi từ bên ngoài Rust và đảm bảo rằng nó được biên dịch bằng các quy ước gọi "C".

Bạn có thể  tự hỏi tại sao trên trái đất chúng ra cần hàm hello_realease này. Khóa ở đây là hãy xem chức năng hello. Sử dụng CString và trả về raw representation giữ cho string trong bộ nhỡ và không cho nó realeased ở cuối hàm. Nếu bộ nhớ được realeased, con trỏ được cung cấp lại cho người gọi bây giờ sẽ trỏ đến bộ nhớ trống hoặc hoàn toàn khác. 

Để tránh rò rỉ bộ nhớ, vì hiện tại chúng ta có một chuỗi string nằm ở xung quanh sau khi hàm hoàn thành thực thi, chúng ta phải cung cấp hàm hello_realease đưa con trỏ đến chuỗi C và realeased bộ nhớ đó. Nó rất quan trọng vậy nên đừng quên gọi hàm này từ code iOS nếu ta không muốn gặp vấn đề. Nếu bạn nhìn kĩ vào hàm này, bạn sẽ nhận thấy rằng nó thúc đẩy cách quản lý bộ nhớ trong Rust bằng cách sử dụng phạm vi function's scope để giải phóng con trỏ. 

Code này sẽ là một trong những nội dung để sử dụng trong project iOS. 

Biên dịch cho iOS 

Trước khi biên dịch thư viện cho iOS, ta sẽ tạo một C header sẽ hoạt động như một bridge cho  swift code của để  thể gọi Rust code.

Ta sẽ tận dụng cbindgen : 

cd rust
cbindgen src/lib.rs -l c > rustylib.h

Sau đó nó sẽ tạo ra một tệp gọi là rustylib.h chứa code sau đây : 

#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>

char *hello(const char *to);

void hello_release(char *s);

Lưu ý rằng cbindgen đã tự động tạo C interface cho ta một cách thuận tiện. 

Bây giờ, hãy tiến hành biên dịch thư viện Rust để có thể sử dụng nó trong bất kỳ project iOS nào : 

# it's important to not forget the release flag.
cargo lipo --release

Kiểm tra thư mục  target/universal/release và tìm kiếm tập tin librustylib.a. Đó là một bản nhị phân mà ta sẽ sử dụng trong các dự án iOS.

Sử dụng iOS binary 

Đầu tiên, ta sẽ sao chép các tập tin librustylib.a và rustylib.h vào thư mục iOS : 

# we're still in the `rust` folder so...
inc=../ios/include
libs=../ios/libs

mkdir ${inc}
mkdir ${libs}

cp rustylib.h ${inc}
cp target/universal/release/librustylib.a ${libs}

Bạn sẽ thấy một treeview giống như hình dưới đây, với tập tin gồm một include và một libs : 

Như bạn có thể tưởng tượng, việc phải tự làm việc này mỗi khi bạn phải biên dịch một phiên bản mới của thư viện Rust sẽ rất mất thời gian. May mắn rằng, bạn có thể tự động hóa quá trình này bằng cách sử dụng một tập lệnh bash  đơn giản như thế này. 

Bây giờ, theo dõi và bạn chỉ cần thực hiện một lần (hai lần nếu bạn đã tạo hai project iOS như bài viết mô tả ).

Hãy mở project rusty-ios-classic trong Xcode và làm theo hướng dẫn : 

Thêm tập tin librustylib.a trong General > Frameworks, Libraries and Embedded Content. Đảm bảo rằng bạn nhìn thấy tên của thư viện đó. Nếu nó không hiển thị, hãy thử lại. Vì không chắc nó có lỗi Xcode hay không, nhưng hầu hết những lần đó bạn sẽ cần thêm hai lần để hoạt động chính xác. 

 

Sau đó, đi đến tab Bulding settings, Tìm kiếm search paths và thêm header và library search paths. Bạn có thể sử dụng đường dẫn tương đối hoặc sử dụng  $(PROJECT_DIR) có sẵn để tránh mã hóa đường dẫn cục bộ của bạn.

Cuối cùng, hãy thêm Objective-C Bridging header. Tìm kiếm bridging header trong tab Build Settings. 

Lặp lại tương tự với project rusty-ios trong trường hợp bạn muốn thử cả hai projects iOS.