🔰

FlutterとRustでAWS S3エクスプローラを作る(1/2)

2023/03/20に公開

自己紹介

初めての投稿になります。
普段はSIerでシステムエンジニアとして働いています。投稿する内容は趣味の範囲で色々と開発してみた内容になります。主にAWS、Flutterを行っています。

作成したアプリ

Flutterでのアプリ開発を数年行っていましたが、Rustの勉強もしたい..と思い始めました。
そこで、FlutterとRustを活用したアプリを
flutter_rust_bridgeを使って試験的に実装してみましたので利用方法を紹介してみます。
FlutterとRustを使ってアプリ開発してみたいという方のご参考になればと思います。

まずは今回の記事を通して作成するアプリのスクリーンショットを添付しておきます。

screenshot

https://github.com/mytooyo/flutter_s3_explorer

はじめに

下記の条件を満たしている方を対象としています。
満たしていない方は後述の参考をご確認いただき、インストールを実施してください。
今回は主にflutter_rust_bridgeの使い方についての紹介になります。

  • Flutterインストール済み (3.7.7)
  • Rustインストール済み (rustup: 1.25.2, cargo: 1.67.1)

参考

flutter_rust_bridgeの利用

まずはflutter_rust_bridgeを利用してFlutterとRustの連携を行ってみたいと思います。
実行はmacOSを対象としています。

flutter_rust_bridgeインストール

まずはflutter_rust_bridgeをインストールしてプロジェクトを作成していきます。

cargo install flutter_rust_bridge_codegen
cargo install cargo-xcode

インストールには様々な方法があるみたいなので、他の方法は公式ドキュメントをご参照ください。

プロジェクト作成

公式にテンプレートがあるので、そこからcloneして触ってみることもできるのですが、
今回はFlutterプロジェクト作成してからflutter_rust_bridgeを利用する手順で行っていきます。

# プロジェクト作成
flutter create app

# flutter_rust_bridge用のCargoプロジェクト作成
cd app
cargo new --lib native

appディレクトリ配下にnativeディレクトリが作成されたと思います。
nativeディレクトリ内は通常のCargoプロジェクトと同様の構成になっています。

native
├── src
│   └── lib.rs
├── Cargo.toml

Flutterプロジェクトに必要なパッケージをインストールしておきます

flutter pub add --dev ffigen && flutter pub add ffi
flutter pub add flutter_rust_bridge
flutter pub add -d build_runner
flutter pub add -d freezed
flutter pub add freezed_annotation
flutter pub add meta

Cargoプロジェクト設定

native/Cargo.tomlファイルに下記の[lib][dependencies]部分を追記します

[package]
edition = "2021"
name = "native"
version = "0.1.0"

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

[dependencies]
flutter_rust_bridge = "1"

macOS、iOSで利用するXcodeプロジェクトファイルを作成します。

cd native
cargo-xcode

nativeディレクトリ配下にnative.xcodeprojが作成されたことを確認します。
こちらはmacOSまたはiOSのXcodeプロジェクトの設定で利用します。

Xcodeプロジェクト設定

macOSのXcodeプロジェクトに作成したCargoプロジェクトを設定していきます。
ここの設定はios/Runner.xcworkspaceでも同様となります。

  1. Xcodeプロジェクトを開く

    open macos/Runner.xcworkspace
    
  2. プロジェクト追加

    • Runnerディレクトリ上で右クリックし、「Add File to "Runner"...」を選択

    • 先程作成した「native/native.xcodeproj」を選択
      ※「Copy item if needed」にチェックがついていないこと

      macos_xcode_1.png

  3. プロジェクトにsoファイルを追加

    1. 「Runner」プロジェクトから Build Phases タブを選択
    2. Target Dependencies の「+」ボタンを押下し、native-staticlibを選択して追加
      • 公式ドキュメントではMacOSの場合は$crate-cdylibを選択とありますが、そちらでは正常にアプリ起動できなかったため、staticlibを選択しています。
    3. Build Phases タブ内の Link Binary With Librariesの「+」ボタンを押下し、libnative_static.aを選択して追加
      • こちらも公式ドキュメントではMacOSの場合は$crate.dylibを選択とありますが、そちらでは正常にアプリ起動できなかったため、static.aを選択しています。

    macos_xcode_2.png

サンプルコード実装

まずはサンプルコードを記述し、コードジェネレータを利用してDartRustのコードとiOS、macOS用のBridgeHeaderファイルを出力します。
新たにnative/src/api.rsファイルを作成して関数を一つ実装します。

api.rs
pub fn greet() -> String {
    "Hello from Rust!".into()
}

api.rsを読み込めるようlib.rsファイルも合わせて更新します。

lib.rs
mod api;

コード生成

flutter_rust_bridge_codegenコマンドを利用してDartRustのBridgeファイルを生成します。
Dartファイルの出力先はブリッジファイルをまとめるためbridgeディレクトリを作成し、そこに出力するようにしています。
--dart-decl-outputオプションがないと上手くいかなかったです..やり方が悪かったのか。。)

mkdir lib/bridge
flutter_rust_bridge_codegen \
  -r native/src/api.rs \
  -c ios/Runner/bridge_generated.h \
  -e macos/Runner/ \
  --dart-output lib/bridge/bridge_generated.dart \
  --dart-decl-output lib/bridge/bridge_definitions.dart
  
...
Formatted 2 files (2 changed) in 0.10 seconds.
2023/03/19 22:52:42 [INFO] Success!
2023/03/19 22:52:42 [INFO] Now go and use it :)

Success!と表示されれば、生成完了です。Dartのファイルが2つ、Rustのファイルが2つ生成されていると思います。

app
├── lib
│   └── bridge
│       ├── bridge_definitions.dart
│       └── bridge_generated.dart
├── native
│   └── src
│       ├── bridge_generated.io.rs
│       └── bridge_generated.rs

最後にDartでジェネレートファイルを呼び出すためにlib/bridge/ffi.dartを生成します。

ffi.dart
// This file initializes the dynamic library and connects it with the stub
// generated by flutter_rust_bridge_codegen.

import 'dart:ffi';
import 'dart:io' as io;

import 'bridge_definitions.dart';
import 'bridge_generated.dart';

export 'bridge_definitions.dart';
// Re-export the bridge so it is only necessary to import this file.
export 'bridge_generated.dart';

const _base = 'native';

// On MacOS, the dynamic library is not bundled with the binary,
// but rather directly **linked** against the binary.
final _dylib = io.Platform.isWindows ? '$_base.dll' : 'lib$_base.so';

final Native api = NativeImpl(io.Platform.isIOS || io.Platform.isMacOS
    ? DynamicLibrary.executable()
    : DynamicLibrary.open(_dylib));

こちらのファイルは公式からのコピペです。const _base = 'native';の部分のみ、Cargoで生成したプロジェクト名に設定するようにしてください。

macOSのBridgeHeader設定

上記のコード生成時にmacOS用のbridge_generated.hが生成されていますので、プロジェクトの設定を行います。
Xcodeでmacos/Runner.xcworkspaceを開き、「Runner」ディレクトリ上で右クリックし、「 Add File to "Runner"... 」を選択します。同じディレクトリ内にbridge_generated.hがありますので選択して追加します。

macos_xcode_3.png

AppDelegate.swiftdummy_method_to_enforce_bundling()を追加します。

AppDelegate.swift
import Cocoa
import FlutterMacOS

@NSApplicationMain
class AppDelegate: FlutterAppDelegate {
  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
    dummy_method_to_enforce_bundling()
    return true
  }
}
iOSの場合

iOSの場合はRunner-Bridging-Header.hがありますので、そちらにimportの追加を行います。

ios/Runner/Runner-Bridging-Header.h
#import "bridge_generated.h"

その後、macOS同様AppDelegate.swiftdummy_method_to_enforce_bundling()を追加します。

Flutterから呼び出し

ここまでくれば、あとはFlutter側から呼び出すだけです。
デフォルトで生成されているカウンターアプリを少し修正してRustのgreetメソッドを呼び出して結果を出力する部分を実装していきます。

lib/main.dart
class _MyHomePageState extends State<MyHomePage> {
  
  void initState() {
    super.initState();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            FutureBuilder<String>(
              future: api.greet(),
              builder: (context, shapshot) {
                return Text(
                  shapshot.data ?? 'No Data',
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}
flutter pub get
flutter run -d macos

Rustのgreet関数を呼び出し、結果を出力するだけです。api.greet()Future<String>となるので、FutureBuilderを利用して、結果をTextで表示します。上手く呼び出しができない場合はExceptionが発生するか「No Data」が表示されると思います。「Hello from Rust!」が表示されればOKです。

flutter_example

最後に

ここまでお読みいただきましてありがとうございます。

以上でflutter_rust_bridgのセットアップとサンプルの実行は終わりになります。
次は実際にRustのAWS SDKを使ってみてS3へのアクセスを実装していきたいと思います。

次回もよろしくお願いします。

あまり更新頻度は高くないですが、GitHubにいくつかコードをあげているのでお時間ある方は少し覗いていただけると有り難いです。

https://github.com/mytooyo

Discussion