본문 바로가기

Mobile/Flutter

Flutter 에서 Rust library 사용하기 (2): iOS + C library in Rust

이번에 소개할 내용은 Flutter(iOS) 에서 Rust 기반 library 를 사용하는 방법입니다. iOS는 Android와 다르게 dynamic library를 지원하지 않아 static library로 빌드를 해야 합니다. 그리고, C library를 그대로 사용할 수 있기 때문에 JNA용으로 library 소스가 있다면 변경없이 그대로 사용가능합니다. 다만, iOS용으로 빌드를 다시 해줘야 합니다.

Xcode setup이 좀 간단하지 않아서 유투브 동영상도 올렸으니 참고해보세요.

아래 그림과 같이 Android와 공유되는 코드부분과 iOS용 전용코드에 대한 구분이 되어있습니다.

대상 독자:

  • Android, iOS 간 코드를 하나의 programming language로 공유하고 싶은 분
  • Multi-platform으로써의 Rust의 가능성을 확인하고 싶은 분
  • iOS와 Swift, C에 대한 이해가 있으신 분
  • Rust에 대한 기본적인 이해가 있으신 분

 

준비물:

  • iOS 개발환경
    • Xcode 14 이상
  • Flutter 개발환경 
    • v3.3.4 사용
  • Rust 개발환경 (rustc v1.64.0)
  • macOS

 

단계:

  1. Flutter 프로젝트 생성
  2. Rust 개발 환경 설정
  3. Rust library 생성
  4. Rust library 코드 작성
  5. Rust library wrapper for C API 프로젝트 생성
  6. Rust library wrapper for C API 코드 작성
  7. Rust library 프로젝트 설정
  8. Rust library workspace 설정
  9. Rust library 빌드
  10. C library header 파일 작성
  11. Xcode 설정
  12. iOS용 Flutter Method Channel 작성
  13. Flutter UI 코드 수정

 

 

1. Flutter 프로젝트 생성

이 후 flutter_rust_app 폴더가 (app path)를 의미합니다.

flutter create flutter_rust_app

2. Rust 개발 환경 설정

android용 library로 빌드하기 위해 target을 추가합니다.

rustup target add aarch64-apple-ios
rustup target add aarch64-apple-ios-sim

3. Rust library 생성: rust_lib

순수 Rust 코드로만 작성된 Rust 용 library 입니다.

cd flutter_rust_app
cargo new --lib rust_lib

4. Rust library 코드  작성: rust_lib/src/lib.rs

기본적으로 제공되는 코드를 삭제하고 아래의 greet 함수와 unit test 코드로 대체한다.

pub fn greet(to: String) -> String {
    format!("Hello, {}", to)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_greet() {
        let result = greet("World".to_owned());
        assert_eq!(result, "Hello, World".to_owned());
    }
}

5. Rust library wrapper for C API 프로젝트 생성: rust_clib

Rust library를 C API 기반으로 wrapping 해주는 library입니다.

cargo init --lib rust_clib

6. Rust library wrapper for C API 코드 작성: rust_clib/src/lib.rs

lib.rs 에 있는 코드를 삭제하고 아래의 코드를 넣습니다.

이 코드는 Rust library 함수를 C library 형태로 wrapping 하고 C library에 맞게  conversion 해주는 코드입니다.

use rust_lib;
use std::ffi::{c_char, CStr, CString};

fn string_to_c_chars(s: String) -> *mut c_char {
    let cstring = CString::new(s).unwrap();
    cstring.into_raw()
}

fn c_chars_to_string(c_chars: *const c_char) -> String {
    let c_str = unsafe { CStr::from_ptr(c_chars) };
    match c_str.to_str() {
        Err(_) => "error".to_owned(),
        Ok(string) => string.to_owned(),
    }
}

#[no_mangle]
pub extern "C" fn cgreet(to: *const c_char) -> *mut c_char {
    let result = rust_lib::greet(c_chars_to_string(to));
    string_to_c_chars(result)
}

7. Rust library 프로젝트 설정: rust_android_lib/Cargo.toml, .cargo/config.toml

build 결과는 C용 static library 이고, rust_lib 의존성 추가하는 설정입니다. iOS용으로는 "staticlib"만 유효합니다. "cdylib"은 Android에서도 빌드하기 위해서 추가해 놓았습니다.

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

[dependencies]
rust_lib = { path = "../rust_lib" }

(app path) 밑에 .cargo 폴더를 만들고 그 안에 config.toml 파일을 아래와 같이 추가합니다. 이미 Android용으로 작성한게 있다면 target만 추가하면 됩니다.

[build]
target = [
    "aarch64-apple-ios",
    "aarch64-apple-ios-sim",
]

8. Rust library workspace 설정: Cargo.toml

두개의 Rust library를 하나의 workspace로 묶기 위한 설정 파일(Cargo.toml)을 app path에 추가합니다.

[workspace]
members = ["rust_lib", "rust_clib"]

9. Rust library 빌드

cargo build --release

 

10. C library header 파일 작성: rust_clib/include/rust_clib.h

Flutter iOS앱에서 Rust library를 직접 접근할 수 있게 C header 파일을 아래와 같이 작성합니다.

char *cgreet(const char *to);
void free_string(char *chars);

swift code에서 위의 헤더 파일을 참조할 수 있도록, ios 폴더를 Xcode를 열어서 Runner > Runner > Runner-Bridge-Header를 선택하고, 아래의 내용을 추가합니다.

#include "rust_clib.h"

11. Xcode 설정

크게 3가지 작업이 필요합니다. 

  1. 헤더 파일을 찾을 수 있게 User Header Search Path에 "../rust_clib/include" 경로 추가
    1. Build Settings 에서 "search path"로 검색하신 다음 User Header Search Path 를 찾아가세요.
  2. 라이브러리 바이너리를 찾을 수 있게 Library Search Path에 "../target/aarch64-apple-ios-sim/release" 경로 추가
    1. Build Settings 에서 "search path"로 검색하신 다음 Library Search Path 를 찾아가세요.
  3. 라이브러리 바이너리를 링크하게 경로 추가
    1.  Build Phases 에서 Link Binary with Libraries 에 ../target/aarch64-apple-ios-sim/release 경로에 있는 librust_clib.a 파일을 추가합니다

 

12. iOS용 Flutter Method Channel 작성: ios/Runner/AppDelegate.swift

아래와 같이 AppDelegate.swift 파일을 수정합니다.

Rust library를 로딩하고, Flutter Platform Integration API 를 통해서 실행하고 결과를 반환해주면 됩니다.

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    
    private func cgreetInternal(to: String, result: FlutterResult) {
        let cstr = cgreet(to.cString(using: .utf8)).unsafelyUnwrapped
        let str = String(cString:cstr)
        free_string(cstr)
        result(str)
    }
    
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        
        
        let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
        let methodChannel = FlutterMethodChannel(name:"com.example.flutter_rust_app/greet",
                                                 binaryMessenger: controller.binaryMessenger)
        
        methodChannel.setMethodCallHandler({(call:FlutterMethodCall, result:@escaping FlutterResult)->Void in
            switch call.method{
            case "greet":
                guard let args = call.arguments else {
                    print("no args")
                    return
                }
                let myargs = args as? [String: Any]
                guard let to = myargs?["to"] as? String else {
                    print("no arg to")
                    return
                }
                self.cgreetInternal(to: to, result: result)
                break
            default:
                print("no method")
                result(FlutterMethodNotImplemented)
                break
            }
        })
        
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}

13. Flutter UI 코드 수정: lib/main.dart

이제 마지막 단계입니다. Flutter App에서 Android 에서 구현한 Platform Integration API를 호출하고 결과를 화면에 출력하는 코드입니다.

기본 counter 소스에 아래의 내용을 추가하면 됩니다. Rust library의 함수를 호출하기 위한 Button과 그 결과를 출력하는 Label 위젯 2개를 추가합니다.

이 부분은 iOS 에서도 재사용할 예정입니다.

...
import 'package:flutter/services.dart';
...

class _MyHomePageState extends State<MyHomePage> {
...
  static const platform = MethodChannel('com.example.flutter_rust_app/greet');
  String _message = 'message here';
...

  @override
  Widget build(BuildContext context) {
...
            ElevatedButton(
              onPressed: () async {
                String result;
                try {
                  result =
                      await platform.invokeMethod('greet', {'to': 'World'});
                } on PlatformException catch (e) {
                  result = "Failed to get greet message: '${e.message}'.";
                }
                setState(() {
                  _message = result;
                });
              },
              child: const Text('Greet'),
            ),
            Text(
              _message,
              style: Theme.of(context).textTheme.headline4,
            ),
...

 

이제 모든 작업이 완료되었고 flutter run으로 앱을 빌드하고 실행하면 아래와 같은 화면이 나오고, 화면에서 Greet 버튼을 클릭하면 버튼 아래의 메시지가 "Hello World"로 바뀔 겁니다.

결론

지금까지 Rust library code를 Flutter app (iOS)에서 호출하기 위해서 해줘야할 것에 대해서 알아봤습니다.

Xcode 설정을 포함한 전체 작업에 대한 영상은 아래의 동영상을 참고하세요. (동영상의 Flutter 예제코드는 살짝 다릅니다만 전체적인 내용은 같습니다.)

https://www.youtube.com/watch?v=3AbYB-PcmH8

실행가능한 예제 코드는 아래의 링크에서 공유합니다.

https://github.com/yeoupooh/flutter_rust_app

 

GitHub - yeoupooh/flutter_rust_app: Sharing Rust libray with Flutter

Sharing Rust libray with Flutter. Contribute to yeoupooh/flutter_rust_app development by creating an account on GitHub.

github.com