Closed9

flutterのffiでC++ライブラリを呼び出す(iOS)

nabeyangnabeyang

今の所の結論

内容的にはC/C++で書かれたコードをflutterアプリに持ち込む方法についてです。。まだiOS版しか試せていないけれども、その限りではC/C++としてヘッダーが1つだけで動くようなラッパーを書いて静的ライブラリをビルドして、それをリンクさせるように設定するのがflutterに持ち込むときに面倒の少ない気がします。

この方法をとる場合、iphoneシミュレータと実機とでアーキテクチャが違うので、ビルドする対象が変わるごとにリンク先を変える必要があります。これを自動でする方法はありそうですが、普段iOSアプリを書いていないので分かっていません。

nabeyangnabeyang

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
nabeyangnabeyang

ラッパーを追加

簡単のため、flutterアプリでは画像ピッカーで選んだ写真のパスから画像を読み込むようにしたいと思います。なので、ラッパー関数は次のようにします。

ZXingReader.h
#pragma once
#ifdef __cplusplus
extern "C"
{
#endif
 __attribute__((visibility("default"))) __attribute__((used))
void read_barcode(const char* filePath, char* out);
#ifdef __cplusplus
}
#endif
ZXingReader.cpp
#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());
}
nabeyangnabeyang

cmakeファイルの修正

ライブラリにラッパー関数を追加します。

CMakeLists.txt
--- 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にします。

nabeyangnabeyang

ライブラリを設置

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を追加します。

zxing_sample.podspec
  s.source           = { :path => '.' }
  s.source_files = 'Classes/**/*'
 + s.ios.vendored_library = 'libs/arm64/libZXing.a'
  s.dependency 'Flutter'
  s.platform = :ios, '8.0'
nabeyangnabeyang

ライブラリをロード

ライブラリはオフィシャルサイトのdocsと同様に行います。Utf8などはパッケージの方のffiが必要です。

lib/zxing_sample.dart
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();

パッケージの設定は次のようにします。

pabspec.yaml
dev_dependencies:
  flutter_test:
    sdk: flutter
  ffi: ^1.0.0
nabeyangnabeyang

exampleから呼び出す

イメージピッカーとPointer<Utf8>を扱う関係でimage_pickerffiを使います。

pubspec.yaml
  cupertino_icons: ^1.0.2
  image_picker: ^0.7.4
  ffi: ^1.0.0

イメージギャラリーへのアクセスのためinfo.plistを編集します。

<key>NSPhotoLibraryUsageDescription</key>
<string>写真使います</string>

画面はこんな感じにします。ボタンを押すと画像選択にし、選択した画像をスキャンして結果を表示します。

example/lib/main.dart
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),
        ), 
      ),
    );
  }
}

nabeyangnabeyang

macOS用にビルド(おまけ)

スタティックライブラリをビルド

$ cmake . \
    -B ./bin/macos \
    -DBUILD_WRITERS=OFF \
    -DBUILD_EXAMPLES=OFF \
    -DBUILD_BLACKBOX_TESTS=OFF 
$ cmake --build ./bin/macos

main関数

main.cpp
#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
このスクラップは2021/04/24にクローズされました