flutterのffiでC++ライブラリを呼び出す(iOS)
今の所の結論
内容的にはC/C++で書かれたコードをflutterアプリに持ち込む方法についてです。。まだiOS版しか試せていないけれども、その限りではC/C++としてヘッダーが1つだけで動くようなラッパーを書いて静的ライブラリをビルドして、それをリンクさせるように設定するのがflutterに持ち込むときに面倒の少ない気がします。
この方法をとる場合、iphoneシミュレータと実機とでアーキテクチャが違うので、ビルドする対象が変わるごとにリンク先を変える必要があります。これを自動でする方法はありそうですが、普段iOSアプリを書いていないので分かっていません。
C++ライブラリのソースコードを入手
ここではzxing-cppというzxingのC++版の1つをビルドしてみます。zxingはバーコードスキャン用のライブラリです。
ghq get https://github.com/nu-book/zxing-cpp.git
cd /path/to/zxing-cpp
git checkout -b ios refs/tags/v1.1.1
ラッパーを追加
簡単のため、flutterアプリでは画像ピッカーで選んだ写真のパスから画像を読み込むようにしたいと思います。なので、ラッパー関数は次のようにします。
#pragma once
#ifdef __cplusplus
extern "C"
{
#endif
__attribute__((visibility("default"))) __attribute__((used))
void read_barcode(const char* filePath, char* out);
#ifdef __cplusplus
}
#endif
#include "ZXingReader.h"
#include "ReadBarcode.h"
#include "TextUtfEncoding.h"
#include <cstring>
#include <string>
#include <algorithm>
#include <cctype>
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include <string.h>
using namespace ZXing;
void read_barcode(const char* filePath, char* result_text)
{
DecodeHints hints;
int width, height, channels;
std::unique_ptr<stbi_uc, void(*)(void*)> buffer(stbi_load(filePath, &width, &height, &channels, 4), stbi_image_free);
if (buffer == nullptr) {
return;
}
auto result = ReadBarcode({buffer.get(), width, height, ImageFormat::RGBX}, hints);
strcpy(result_text, TextUtfEncoding::ToUtf8(result.text()).c_str());
}
cmakeファイルの修正
ライブラリにラッパー関数を追加します。
--- a/core/CMakeLists.txt
+++ b/core/CMakeLists.txt
@@ -435,7 +435,7 @@ source_group (Sources\\textcodec FILES ${TEXT_CODEC_FILES})
find_package (Threads)
add_library (ZXing
${COMMON_FILES}
${AZTEC_FILES}
${DATAMATRIX_FILES}
@@ -445,11 +445,14 @@ add_library (ZXing
${PDF417_FILES}
${QRCODE_FILES}
${TEXT_CODEC_FILES}
+ src/ZXingReader.cpp
+ src/ZXingReader.h
)
target_include_directories (ZXing
PUBLIC "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>"
INTERFACE "$<INSTALL_INTERFACE:include>"
+ PRIVATE ../thirdparty/stb
)
target_compile_options (ZXing
実機用にビルド
今回はバーコードスキャン機能だけ欲しいので、他はOFFにします。iosはスタチックライブラリを使うので、BUILD_SHARED_LIBS=OFF
にします。iosビルド用のツールチェインファイルもダウンロードします。デバッグモードでビルドしている理由は、swift内でライブラリの関数を呼ばないとxcodeの最適化により消えてしまうからです。回避できることですが、今回はデバッグでいきます。
$ curl -OL https://raw.githubusercontent.com/leetal/ios-cmake/master/ios.toolchain.cmake
$ cmake -Sios -G Xcode . \
-B bin/ios_arm64 \
-DPLATFORM=OS64 \
-DBUILD_WRITERS=OFF \
-DBUILD_EXAMPLES=OFF \
-DBUILD_BLACKBOX_TESTS=OFF \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_TOOLCHAIN_FILE=ios.toolchain.cmake
$ cmake --build ./bin/ios_arm64 --config Debug
** BUILD SUCCEEDED **
と出たら成功です。bin/ios_arm64/core/Debug-iphoneos/libZXing.a
が生成されたライブラリです。シミュレータ用にビルドする場合は-DPLATFORM=SIMULATOR64
にします。
flutter pluginを作成
ここからは、次のチュートリアルにほぼ従います。
いまのところiosしか考えてないので、iosだけ作るようにします。$ flutter create --platforms=ios --template=plugin zxing_sample
ライブラリを設置
zxing_sampleは次のようにファイルを設置します。libs
にビルドしたライブラリを(arm64が実機でarm86_64はシミュレータ用です)置き、ClassesのZXingReader.h
はラッパー関数のヘッダーファイルを設置します。
$ cd xzing_sample/ios
$ tree .
├── Assets
├── Classes
│ ├── SwiftZxingSamplePlugin.swift
│ ├── ZXingReader.h
│ ├── ZxingSamplePlugin.h
│ └── ZxingSamplePlugin.m
├── libs
│ ├── arm64
│ │ └── libZXing.a
│ └── x86_64
│ └── libZXing.a
└── zxing_sample.podspec
podspec
ファイルはios.vendored_library
を追加します。
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
+ s.ios.vendored_library = 'libs/arm64/libZXing.a'
s.dependency 'Flutter'
s.platform = :ios, '8.0'
ライブラリをロード
ライブラリはオフィシャルサイトのdocsと同様に行います。Utf8
などはパッケージの方のffiが必要です。
import 'dart:ffi'; // For FFI
import 'package:ffi/ffi.dart';
final DynamicLibrary zxingLib = DynamicLibrary.process();
final void Function(Pointer<Utf8>, Pointer<Utf8>) barcodeRead =
zxingLib
.lookup<NativeFunction<Void Function(Pointer, Pointer)>>("read_barcode")
.asFunction();
パッケージの設定は次のようにします。
dev_dependencies:
flutter_test:
sdk: flutter
ffi: ^1.0.0
exampleから呼び出す
イメージピッカーとPointer<Utf8>
を扱う関係でimage_picker
とffi
を使います。
cupertino_icons: ^1.0.2
image_picker: ^0.7.4
ffi: ^1.0.0
イメージギャラリーへのアクセスのためinfo.plist
を編集します。
<key>NSPhotoLibraryUsageDescription</key>
<string>写真使います</string>
画面はこんな感じにします。ボタンを押すと画像選択にし、選択した画像をスキャンして結果を表示します。
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:zxing_sample/zxing_sample.dart';
import 'package:image_picker/image_picker.dart';
import 'package:ffi/ffi.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
String code = '';
final picker = ImagePicker();
// 画像の読み込み
Future _getImage() async {
//final pickedFile = await picker.getImage(source: ImageSource.camera);//カメラ
final pickedFile = await picker.getImage(source: ImageSource.gallery);//アルバム
setState(() {
if(pickedFile!=null) {
String path = pickedFile.path;
var result = code.toNativeUtf8();
barcodeRead(path.toNativeUtf8(), result);
code = result.toDartString();
}
});
}
void initState() {
super.initState();
initPlatformState();
}
Future<void> initPlatformState() async {
String platformVersion;
try {
platformVersion = await ZxingSample.platformVersion;
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
}
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: Text(code),
),
floatingActionButton: FloatingActionButton(
onPressed: _getImage,
tooltip: 'Pick',
child: Icon(Icons.add),
),
),
);
}
}
macOS用にビルド(おまけ)
スタティックライブラリをビルド
$ cmake . \
-B ./bin/macos \
-DBUILD_WRITERS=OFF \
-DBUILD_EXAMPLES=OFF \
-DBUILD_BLACKBOX_TESTS=OFF
$ cmake --build ./bin/macos
main
関数
#include "ZXingReader.h"
int main(int argc, char* argv[]) {
char result[80];
read_barcode(argv[1], result);
printf("%s\n", result);
return 0;
}
リンク
$ g++ -c main.cpp
$ g++ main.o libZXing.a
実行
$ ./a.out sample.png