Flutter FFI Plugin Project

cornpip
|2025. 11. 26. 23:39

New Project FFI Plugin

Android Studio New Flutter Project에 FFI Plugin type이 있다. 살펴보자

 

https://github.com/cornpip/ffi_plugin_test.git

프로젝트 생성 후 OpenCV SDK를 추가하고, 몇 가지 예제 코드를 추가했다.

※ OpenCV 설정은 안드로이드만 적용했다.

new flutter project
project 구조
test

ffigen.yaml

FFI Plugin으로 프로젝트를 만들어보면 FFI 기본 구조와 `ffigen.yaml` 이 있다.

# ffigem.yaml
# Run with `dart run ffigen --config ffigen.yaml`.
name: FfiPluginLookBindings
description: |
  Bindings for `src/ffi_plugin_look.h`.
output: 'lib/ffi_plugin_look_bindings_generated.dart'
headers:
  entry-points:
    - 'src/ffi_plugin_look.h'
  include-directives:
    - 'src/ffi_plugin_look.h'
preamble: |
  // ignore_for_file: always_specify_types
  // ignore_for_file: camel_case_types
  // ignore_for_file: non_constant_identifier_names
comments:
  style: any
  length: full

옵션들을 작성하고 스크립트를 실행하면 binding dart 코드를 생성해 준다.

# ffi_plugin_look_bindings_generated.dart
void apply_heavy_blur(
  ffi.Pointer<ffi.Uint8> rgba_pixels,
  int width,
  int height,
  int iterations,
) {
  return _apply_heavy_blur(rgba_pixels, width, height, iterations);
}

late final _apply_heavy_blurPtr =
    _lookup<
      ffi.NativeFunction<
        ffi.Void Function(ffi.Pointer<ffi.Uint8>, ffi.Int, ffi.Int, ffi.Int)
      >
    >('apply_heavy_blur');
late final _apply_heavy_blur = _apply_heavy_blurPtr
    .asFunction<void Function(ffi.Pointer<ffi.Uint8>, int, int, int)>();

2025.04.08 - [flutter] - Flutter C++ OpenCV로 이미지 처리하기 + 16KB Memory Page Size

(예제 = 이전 발행 글 예제)

 

생성된 코드는 기존 예제의 `lib/native/native_process.dart` 부분에 해당한다.

`lookupFunction`을 late final로 한 번만 호출하게 했다.

CMakeLists.txt

` target_link_options(ffi_plugin_look PRIVATE "-Wl,-z,max-page-size=16384")`

기본 CMake 설정에 Support Android 15 16k page size 옵션도 있다.

예제에선 전역 링크 플래그를 수정했는데, 여기선 특정 타깃에만 16KB 페이지 정렬 옵션을 지정했다.

 

ffi_plugin_look.dart

`ffi_plugin_look.dart`는 예제의 `lib/native/native_control.dart` 부분에 해당한다.

DynamicLibrary.open, 포인터 처리, FFI 사용 로직 등이 있다.

 

`_helperIsolateSendPort` 는  예제의 `compute(_convert_to_gray_func, ..)` 역할을 한다.

시간이 드는 작업은 별도의 isolate에서 돌리는 것이 일반적이다.

기본 실행 컨텍스트인 main isolate에서 무거운 작업을 수행하면, 그동안 UI 스레드가 멈춘 것처럼 보이기 때문이다.

/// The SendPort belonging to the helper isolate.
Future<SendPort> _helperIsolateSendPort = () async {
  // Receive port on the main isolate to receive messages from the helper.
  // We receive two types of messages:
  // 1. A port to send messages on.
  // 2. Responses to requests we sent.
  final Completer<SendPort> completer = Completer<SendPort>();

  final ReceivePort receivePort = ReceivePort()
    ..listen((dynamic data) {
      print(data);
      if (data is SendPort) {
        // The helper isolate sent us the port on which we can sent it requests.
        completer.complete(data);
        return;
      }
      if (data is _SumResponse) {
        // The helper isolate sent us a response to a request we sent.
        final Completer<int> completer = _sumRequests[data.id]!;
        _sumRequests.remove(data.id);
        completer.complete(data.result);
        return;
      }
      if (data is _HeavyBlurResponse) {
        final Completer<Uint8List>? completer =
            _heavyBlurRequests.remove(data.id);
        if (completer != null) {
          final Uint8List pixels =
              data.rgbaPixels.materialize().asUint8List();
          completer.complete(pixels);
        }
        return;
      }
      throw UnsupportedError('Unsupported message type: ${data.runtimeType}');
    });

  // Start the helper isolate.
  await Isolate.spawn((SendPort sendPort) async {
    final ReceivePort helperReceivePort = ReceivePort()
      ..listen((dynamic data) {
        if (data is _SumRequest) {
          final int result = _bindings.sum_long_running(data.a, data.b);
          final _SumResponse response = _SumResponse(data.id, result);
          sendPort.send(response);
          return;
        }
        if (data is _HeavyBlurRequest) {
          final Uint8List rgba =
              data.rgbaPixels.materialize().asUint8List();
          final Uint8List blurred = _runHeavyBlurNative(
            rgba,
            data.width,
            data.height,
            data.iterations,
          );
          final _HeavyBlurResponse response = _HeavyBlurResponse(
            data.id,
            TransferableTypedData.fromList(<Uint8List>[blurred]),
          );
          sendPort.send(response);
          return;
        }
        throw UnsupportedError('Unsupported message type: ${data.runtimeType}');
      });
    sendPort.send(helperReceivePort.sendPort);
  }, receivePort.sendPort);

  return completer.future;
}();

Future<SendPort> _helperIsolateSendPort = () async {} ()

바로 함수 실행하는 표현이다.

 

메인 isolate = default

헬퍼 isolate = 별도 isolate.spawn을 의미한다.

 

헬퍼 isolate 마지막에 sendPort.send(helperReceivePort.sendPort); 가 있어서

final SendPort helperIsolateSendPort = await _helperIsolateSendPort;

는 헬퍼 ReceivePort를 얻을 수 있다.

 

두 개의 ReceivePort가 있는데, 메인 isolate 포트가 응답을 받고 헬퍼 isolate 포트가 요청을 받는다.

헬퍼 포트에서 작업을 끝내고 메인 쪽 포트로 응답을 send 하면, 메인 쪽 listener가 대응하는 Completer를 완료하는 흐름이다.

compute vs ( Isolate.spawn + ReceivePort )

compute

  • 내부적으로 별도 isolate를 만들고 메시지 포맷/전송을 모두 숨겨 준다.
  • 간단한 CPU 바운드 작업을 분리할 때 사용하기 좋다. (일회성 작업)

Isolate.spawn + ReceivePort

  • isolate를 재사용하면서 여러 요청을 주고받거나, 복잡한 타입을 줄 수 있다.
  • 메시지 라우팅, 에러 처리, isolate 수명 등을 모두 직접 제어하므로 유연성과 제어권이 크다.

 

pubspec.yaml (./example)

만든 FFI Plugin 패키지를 사용하는 방법에는

  • pub.dev 배포 (public)
  • github (private & public)
  • local path (private)

등등 몇 가지 있다.

 

깃헙으로 사용한다면

  # pubspec.yaml(example)
  ffi_plugin_look:
    git:
      url: https://github.com/cornpip/ffi_plugin_test.git
      ref: master

위와 같이 세팅하고 `Pub get` 해서 저장한다.

최초 `Pub get` 이후 Repository의 최신 커밋을 업데이트하고 싶으면 `pubspec.lock`을 지우고 get 해야 한다.

 

# main.dart
import 'package:ffi_plugin_look/ffi_plugin_look.dart' as ffi_plugin_look;
    ....
    matrixResult = ffi_plugin_look.multiplyMatrices(
      const [1, 2, 3, 4],
      const [5, 6, 7, 8],
      2,
    );
    ....
    final Uint8List filtered = await ffi_plugin_look.applyHeavyBlurAsync(
      pixels,
      _heavyDemoWidth,
      _heavyDemoHeight,
      iterations: 1000,
    );

만든 FFI plugin을 사용할 수 있다.