Closed10

「dart:ffi」の技術調査

ピン留めされたアイテム
Ryo24Ryo24

まとめ

dart:ffi技術調査の動機

卒業研究用で開発しているアプリケーションは、

  • ネイティブで開発
    • iOS : Swift,OpenCV
    • Android : Java,OpenCV
  • アジャイル
  • 今後も後輩に引き継ぎ、アップデートをしていく

のためネイティブではなく、クロスプラットフォームにリプレースする必要があった。

目的

FlutterでOpenCV(現状の資産)を活用することが目的である。そのため、FFIで直接C++のコードを有効活用したかった。

結果

dart:ffiは使用しない。

理由

  • OpenCVを理解していない(する気もない)
  • C++が意味わからん
  • 2次情報が少ない
  • 調査段階でC++の関数が呼び出せていない

最後に

一応成功例はあるため、技術的にFlutterでOpenCVをdart:ffiで運用することはできる。
だが開発が難しく、保守も困難になるため断念する。
今後は、MethodChannelの調査を行う。

Ryo24Ryo24

C interop using dart:ffiの内容を和訳し、学習した時のメモ。

準備

Dart samplesのクローン

https://github.com/dart-lang/samples
$ git clone git@github.com:dart-lang/samples.git

cmakeのインストール

$ brew install cmake

ビルドし、実行する

$ cd samples
$ cd ffi
$ cd hello_library
$ cmake .
$ ls
CMakeCache.txt          CMakeFiles              CMakeLists.txt          Makefile                cmake_install.cmake     hello.c                 hello.def               hello.h
$ make
$ ls
CMakeCache.txt          CMakeLists.txt          cmake_install.cmake     hello.def               hello_test              libhello.1.dylib
CMakeFiles              Makefile                hello.c                 hello.h                 libhello.1.0.0.dylib    libhello.dylib
$ cd ..
$ dart pub get
$ dart run hello.dart
Hello World

「hello.dart」の内容


import 'dart:ffi' as ffi;
import 'dart:io' show Platform, Directory;

import 'package:path/path.dart' as path;

// FFI signature of the hello_world C function
typedef HelloWorldFunc = ffi.Void Function();
// Dart type definition for calling the C foreign function
typedef HelloWorld = void Function();

void main() {
  // Open the dynamic library
  var libraryPath =
      path.join(Directory.current.path, 'hello_library', 'libhello.so');
  if (Platform.isMacOS) {
    libraryPath =
        path.join(Directory.current.path, 'hello_library', 'libhello.dylib');
  }
  if (Platform.isWindows) {
    libraryPath = path.join(
        Directory.current.path, 'hello_library', 'Debug', 'hello.dll');
  }

  final dylib = ffi.DynamicLibrary.open(libraryPath);

  // Look up the C function 'hello_world'
  final HelloWorld hello = dylib
      .lookup<ffi.NativeFunction<HelloWorldFunc>>('hello_world')
      .asFunction();
  // Call the function
  hello();
}

手順

  1. dart:ffiをインポート
import 'dart:ffi' as ffi; 
  1. パスライブラリをインポート(ダイナミックリンクライブラリのパスを格納する)
import 'dart:io' show Platform, Directory;
import 'package:path/path.dart' as path;
  1. typedefでFFIを用いたC言語の関数を呼び出すときに必要な変数(hello_world_func)を作成
 typedef hello_world_func = ffi.Void Function();
  1. typedefでC言語の関数を呼び出すときに必要な変数(HelloWorld)を作成
typedef HelloWorld = void Function();
  1. ダイナミックリンクライブラリのパスを格納する変数を作成
var libraryPath = path.join(Directory.current.path, 'hello_library',
 'libhello.so');
if (Platform.isMacOS) { 
  libraryPath = path.join(Directory.current.path, 'hello_library', 
   'libhello.dylib');
} else if (Platform.isWindows) { 
  libraryPath = path.join(Directory.current.path, 'hello_library', 
   'Debug', 'hello.dll');
} 
  1. C言語の関数を含むダイナミックリンクライブラリを開く
final dylib = ffi.DynamicLibrary.open(libraryPath);
  1. C言語への参照先を取得し、hello_world_func を代入す
    ⚠️ hello_world_func(3)とHelloWorld(4)、dyli(5)を使用する。「('hello_world')」は、C言語側の関数名。
  final HelloWorld hello = dylib
   .lookup<ffi.NativeFunction<hello_world_func>>('hello_world')
   .asFunction();
  1. C言語の関数を呼び出す
 hello();
Ryo24Ryo24

「Cmake」とは?

コンパイラに依存せず自動化するためのツール。つまり、開発環境(OS)が変化しても同一のソースコードでビルド可能なツール。

サポートする任意のプロジェクトファイルを作成する

CmakeLists.txtという設定ファイルを作成すれば、Cmakeがサポートする任意のプロジェクトファイルを作成することができる。
つまり、

  1. Cmakeで*.sln,*.xcodeproj,Makefileなどのプロジェクトファイルを生成する
  2. 生成したプロジェクトファイルを使ってビルドする

以上、2ステップでプロジェクトをビルドできる。

多彩な開発環境では、必要不可欠な技術

C++をはじめ、OSに縛られない言語をチームで開発する場合、Cmakeで開発環境を整備するのは必要不可欠な作業である。

なぜ必要なのか?

コンパイラの違い

プログラムの実行 = 標準ライブラリ + コンパイラ 

のため、開発環境(コンパイラ)が変化するとソースコードを書き直す必要があった。

プロジェクトファイルの使い分け

標準ライブラリだけで書かれたプログラムは、どのコンパイラでもビルドは可能である。そのため、ソースコードの共有は可能である。しかしコンパイラのオプション、つまりビルドに必要な設定項目やそれを格納する設定ファイルのフォーマットは統一されていない。

- GCC on Linux: `Makefile`
- MSVC on Windows: `*.sln`
- Clang on macOS: `Makefile` or `*.xcodeproj`

といったように、開発環境に合わせてプロジェクトファイルを使い分ける必要がある。

「プロジェクトファイルを自動生成」すればいいのではないか?

任意のプロジェクトファイルを自動生成すれば、開発環境が変化してもソースコードを共有化できるのではないか?

という考えから、Cmakeが生まれた。

参考文献

https://kamino.hatenablog.com/entry/cmake_tutorial1

Ryo24Ryo24

独自に試してみる

pubspec.yamlの追加

$ vim pubspec.yaml

name: c_hello_world_ffi
version: 0.0.1
description: >-
  Simple example of calling C code from Dart with FFI

# This example isn't intended for publishing on pub.dev.
publish_to: none

environment:
  sdk: ">=2.12.0 <3.0.0"

pathライブラリの追加

$ dart pub add path

最終的な内容

name: c_hello_world_ffi
version: 0.0.1
description: >-
  Simple example of calling C code from Dart with FFI

# This example isn't intended for publishing on pub.dev.
publish_to: none

environment:
  sdk: ">=2.12.0 <3.0.0"
dependencies: 
  path: ^1.8.0

Dartのプログラム

import 'dart:ffi' as ffi;
import 'dart:io' show Platform, Directory;

import 'package:path/path.dart' as path;

// FFI signature of the hello_world C function
typedef HelloWorldFunc = ffi.Void Function();
// Dart type definition for calling the C foreign function
typedef HelloWorld = void Function();

void main() {
  // Open the dynamic library
  var libraryPath =
      path.join(Directory.current.path, 'hello_library', 'libhello.dylib');

  final dylib = ffi.DynamicLibrary.open(libraryPath);

  // Look up the C function 'hello_world'
  final HelloWorld hello = dylib
      .lookup<ffi.NativeFunction<HelloWorldFunc>>('hello_world')
      .asFunction();
  // Call the function
  hello();
}

C言語のプログラムを作成

$ vim hello.c

#include <stdio.h>
#include "hello.h"

int main()
{
    hello_world();
    return 0;
}

void hello_world()
{
    printf("Hello World\n");
}
$ vim hello.h

void hello_world();

ダイナミックライブラリを作成

$ pwd
../UseOpenCVwithFlutter/ffi/c/ffi_library
$ ls
hello.c         hello.h
$ gcc -dynamiclib -o libhello.dylib hello.c
$ ls
hello.c         hello.h         libhello.dylib

ファイル構造

.
├── ffi_library
│   ├── hello.c
│   ├── hello.h
│   └── libhello.dylib
├── hello.dart
├── pubspec.lock
└── pubspec.yaml

1 directory, 6 files

実行

$ dart hello.dart
Unhandled exception:
Invalid argument(s): Failed to load dynamic library '/Users/ryo/github/UseOpenCVwithFlutter/dart-ffi/c/hello_library/libhello.dylib': dlopen(/Users/ryo/github/UseOpenCVwithFlutter/dart-ffi/c/hello_library/libhello.dylib, 1): image not found
#0      _open (dart:ffi-patch/ffi_dynamic_library_patch.dart:11:55)
#1      new DynamicLibrary.open (dart:ffi-patch/ffi_dynamic_library_patch.dart:20:12)
#2      main (file:///Users/ryo/github/UseOpenCVwithFlutter/dart-ffi/c/hello.dart:16:36)
#3      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:283:19)
#4      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)

エラーの内容

  • Invalid argument(s) | 無効な引数
  • Failed to load dynamic library | ダイナミックライブラリの読み込みに失敗

ダイナミックライブラリにアクセスするための引数が無効のため、イメージが取得できないよ!
とエラーを出している。

公式のサンプルとの違い

公式のファイル構造

.
├── README.md
├── hello_world
│   ├── analysis_options.yaml
│   ├── hello.dart
│   ├── hello_library
│   │   ├── CMakeCache.txt
│   │   ├── CMakeFiles
│   │   │   ├── 3.21.1
│   │   │   │   ├── CMakeCCompiler.cmake
│   │   │   │   ├── CMakeDetermineCompilerABI_C.bin
│   │   │   │   ├── CMakeSystem.cmake
│   │   │   │   └── CompilerIdC
│   │   │   │       ├── CMakeCCompilerId.c
│   │   │   │       ├── CMakeCCompilerId.o
│   │   │   │       └── tmp
│   │   │   ├── CMakeDirectoryInformation.cmake
│   │   │   ├── CMakeError.log
│   │   │   ├── CMakeOutput.log
│   │   │   ├── CMakeTmp
│   │   │   ├── Makefile.cmake
│   │   │   ├── Makefile2
│   │   │   ├── TargetDirectories.txt
│   │   │   ├── cmake.check_cache
│   │   │   ├── hello_library.dir
│   │   │   │   ├── DependInfo.cmake
│   │   │   │   ├── build.make
│   │   │   │   ├── cmake_clean.cmake
│   │   │   │   ├── compiler_depend.make
│   │   │   │   ├── compiler_depend.ts
│   │   │   │   ├── depend.make
│   │   │   │   ├── flags.make
│   │   │   │   ├── hello.c.o
│   │   │   │   ├── hello.c.o.d
│   │   │   │   ├── link.txt
│   │   │   │   └── progress.make
│   │   │   ├── hello_test.dir
│   │   │   │   ├── DependInfo.cmake
│   │   │   │   ├── build.make
│   │   │   │   ├── cmake_clean.cmake
│   │   │   │   ├── compiler_depend.make
│   │   │   │   ├── compiler_depend.ts
│   │   │   │   ├── depend.make
│   │   │   │   ├── flags.make
│   │   │   │   ├── hello.c.o
│   │   │   │   ├── hello.c.o.d
│   │   │   │   ├── link.txt
│   │   │   │   └── progress.make
│   │   │   └── progress.marks
│   │   ├── CMakeLists.txt
│   │   ├── Makefile
│   │   ├── cmake_install.cmake
│   │   ├── hello.c
│   │   ├── hello.def
│   │   ├── hello.h
│   │   ├── hello_test
│   │   ├── libhello.1.0.0.dylib
│   │   ├── libhello.1.dylib -> libhello.1.0.0.dylib
│   │   └── libhello.dylib -> libhello.1.dylib
│   ├── mono_pkg.yaml
│   ├── pubspec.yaml
│   ├── setup.sh
│   └── test
│       └── hello_world_test.dart
├── primitives
│   ├── analysis_options.yaml
│   ├── mono_pkg.yaml
│   ├── primitives.dart
│   ├── primitives_library
│   │   ├── CMakeLists.txt
│   │   ├── primitives.c
│   │   ├── primitives.def
│   │   └── primitives.h
│   ├── pubspec.yaml
│   └── test
│       └── primitives_test.dart
├── structs
│   ├── analysis_options.yaml
│   ├── mono_pkg.yaml
│   ├── pubspec.yaml
│   ├── structs.dart
│   ├── structs_library
│   │   ├── CMakeLists.txt
│   │   ├── structs.c
│   │   ├── structs.def
│   │   └── structs.h
│   └── test
│       └── structs_test.dart
├── system-command
│   ├── README.md
│   ├── analysis_options.yaml
│   ├── linux.dart
│   ├── macos.dart
│   ├── mono_pkg.yaml
│   ├── pubspec.yaml
│   ├── win32ui.dart
│   └── windows.dart
└── test_utils
    ├── analysis_options.yaml
    ├── lib
    │   └── test_utils.dart
    ├── mono_pkg.yaml
    └── pubspec.yaml

19 directories, 83 files

考察

チャレンジでは、テストコードやCMakeを実装していない。そのため、ファイルやディレクトリの数で比較することはできない。
エラーの内容から、ダイナミックライブラリーが関係していると考えられる。

動作環境

- OS: macOS Big Sur Version 11.5.1
- PC: MacBook Pro (13-inch, 2018, Four Thunderbolt 3 Ports)
- CPU: 2.7 GHz Quad-Core Intel Core i7
- RAM: 16 GB 2133 MHz LPDDR3

のため、ダイナミックライブラリの拡張子は.dylibとなる。

今回作成したダイナミックライブラリ

../c/ffi_library/libhello.dylib

公式のダイナミックライブラリ

- ../hello_world/hello_library/libhello.1.0.0.dylib
- ../hello_world/hello_library/libhello.1.dylib
- ../hello_world/hello_library/libhello.dylib

完全に比較することはできないが、libhello.1.dyliblibhello.1.0.0.dylibを公式は作成している。そのため、両者もしくはどちらかのダイナミックライブラリが必要だと思われる。

Ryo24Ryo24

DartFFIとの相互運用性|セッション

https://www.youtube.com/watch?v=2MMK7YoFgaA

サンプルプログラム

final dylib = DynamicLibrary.open('utilslib/lib/libutils.1.0.0.dylib')

libutils.1.0.0.dylibを使用している。

ダイナミックライブラリを開く部分を変えてみる

相対パスを直接指定し、ダイナミックライブラリを開いてみる。(動画のサンプルと同じようにしてみる)

Before

var libraryPath = path.join(Directory.current.path, 'hello_library', 'libhello.dylib');
final dylib = ffi.DynamicLibrary.open(libraryPath);

After

 final dylib = ffi.DynamicLibrary.open('dart-ffi/c/ffi_library/libhello.dylib');

結果

Unhandled exception:
Invalid argument(s): Failed to load dynamic library 'dart-ffi/c/ffi_library/libhello.dylib': dlopen(dart-ffi/c/ffi_library/libhello.dylib, 1): image not found
#0      _open (dart:ffi-patch/ffi_dynamic_library_patch.dart:11:55)
#1      new DynamicLibrary.open (dart:ffi-patch/ffi_dynamic_library_patch.dart:20:12)
#2      main (file:///Users/ryo/github/UseOpenCVwithFlutter/dart-ffi/c/hello.dart:18:26)
#3      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:283:19)
#4      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)

結論

ダイナミックライブラリが悪い。

Ryo24Ryo24

CMakeを導入する

公式が公開しているCMakeLists.txtを活用し、導入する。

$ rm -rf libhello.dylib 
$ vim CMakeLists.txt

cmake_minimum_required(VERSION 3.7 FATAL_ERROR)
project(hello_library VERSION 1.0.0 LANGUAGES C)
add_library(hello_library SHARED hello.c hello.def)
add_executable(hello_test hello.c)

set_target_properties(hello_library PROPERTIES
    PUBLIC_HEADER hello.h
    VERSION ${PROJECT_VERSION}
    SOVERSION 1
    OUTPUT_NAME "hello"
    XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Hex_Identity_ID_Goes_Here"
)

$ vim hello.def

LIBRARY   hello
EXPORTS
   hello_world


$ ls
CMakeLists.txt              hello.c                 hello.def               hello.h
$ make .
$ make
$ cd ..
$ dart pub get

実行

$ dart run hello.dart
Unhandled exception:
Invalid argument(s): Failed to load dynamic library '/Users/ryo/github/UseOpenCVwithFlutter/dart-ffi/c/hello_library/libhello.dylib': dlopen(/Users/ryo/github/UseOpenCVwithFlutter/dart-ffi/c/hello_library/libhello.dylib, 1): image not found
#0      _open (dart:ffi-patch/ffi_dynamic_library_patch.dart:11:55)
#1      new DynamicLibrary.open (dart:ffi-patch/ffi_dynamic_library_patch.dart:20:12)
#2      main (file:///Users/ryo/github/UseOpenCVwithFlutter/dart-ffi/c/hello.dart:16:36)
#3      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:283:19)
#4      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)

結果

かわんねぇ

Ryo24Ryo24

絶対パスでダイナミックライブラリを指定すると実行できる

Before

var libraryPath = path.join(Directory.current.path, 'hello_library', 'libhello.dylib');
final dylib = ffi.DynamicLibrary.open(libraryPath);

After

final dylib = ffi.DynamicLibrary.open('/Users/ryo/github/UseOpenCVwithFlutter/dart-ffi/c/ffi_library/libhello.1.0.0.dylib');

実行結果

Hello World

相対パスだと実行できない

相対パスで指定

final dylib = ffi.DynamicLibrary.open('dart-ffi/c/ffi_library/libhello.1.0.0.dylib');

実行結果

Unhandled exception:
Invalid argument(s): Failed to load dynamic library 'dart-ffi/c/ffi_library/libhello.1.0.0.dylib': dlopen(dart-ffi/c/ffi_library/libhello.1.0.0.dylib, 1): image not found
#0      _open (dart:ffi-patch/ffi_dynamic_library_patch.dart:11:55)
#1      new DynamicLibrary.open (dart:ffi-patch/ffi_dynamic_library_patch.dart:20:12)
#2      main (file:///Users/ryo/github/UseOpenCVwithFlutter/dart-ffi/c/hello.dart:14:26)
#3      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:283:19)
#4      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
Ryo24Ryo24

CMakeは必要なのか?

結論:不要!

gccコマンドで.dylibを作成する

$ pwd
../UseOpenCVwithFlutter/dart-ffi/c/ffi_library
$ mkdir dll-test
$ cp hello.c hello.h dll-test
$ cd dll-test
$ gcc -dynamiclib -o libhello.dylib hello.c
$ ls
hello.c         hello.h         libhello.dylib

絶対パス

/Users/ryo/github/UseOpenCVwithFlutter/dart-ffi/c/ffi_library/dll-test/libhello.dylib

hello.dartのパスの指定部分を変更

実行結果

Hello World
Ryo24Ryo24

躓いていた原因

ダイナミックライブラリのパスを指定する方法を間違えていた。

確認

指定方法を間違えたプログラム

import 'dart:io' show Platform, Directory;
import 'package:path/path.dart' as path;

void main() {
  // Open the dynamic library
  var libraryPath = path.join(Directory.current.path, 'hello_library', 'libhello.dylib');
  print(libraryPath);
}

実行結果

/Users/ryo/github/UseOpenCVwithFlutter/ffi/c/hello_library/libhello.dylib

指定方法が正しいプログラム

import 'dart:io' show Platform, Directory;
import 'package:path/path.dart' as path;

void main() {
  // Open the dynamic library
  var libraryPath = path.join(Directory.current.path, 'ffi_library', 'libhello.dylib');
  print(libraryPath);
}

実行結果

/Users/ryo/github/UseOpenCVwithFlutter/ffi/c/ffi_library/libhello.dylib

結論

libhello.dylibの1階層前のディレクトリが違う。

  • 正: ../ffi_library/libhello.dylib
  • 誤:../hello_library/libhello.dylib
Ryo24Ryo24

C++ の関数をdart:ffiで呼び出す

C++の関数

C++のプログラム(.cpp)

#include "hello.hpp"
#include <iostream>

int main() {
    hello_world();
    return 0;
}

void hello_world() {
    std::cout << "Hello!" << std::endl;
}

C++のヘッダファイル(.hpp)

#ifndef HELLO_H
#define HELLO_H

void hello_world();

#endif

CMakeの準備

サンプルの内容を流用し、準備する。

hello.def

$ vim hello.def

LIBRARY   hello
EXPORTS
   hello_world

CMakeLists.txt

$ vim CMakeLists.txt

cmake_minimum_required(VERSION 3.7 FATAL_ERROR)
project(hello_library VERSION 1.0.0 LANGUAGES CXX)
add_library(hello_library SHARED hello.cpp hello.def)
add_executable(hello_test hello.cpp)

set_target_properties(hello_library PROPERTIES
    PUBLIC_HEADER hello.hpp
    VERSION ${PROJECT_VERSION}
    SOVERSION 1
    OUTPUT_NAME "hello"
    XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Hex_Identity_ID_Goes_Here"
)

実行

$ cmake .
$ make
$ cd ..
$ dart pub get
$ dart run hello.dart

実行結果

/Users/ryo/github/UseOpenCVwithFlutter/ffi/cpp/ffi_library/libhello.dylib
DynamicLibrary: handle=0x7fbf5fd260d0
Unhandled exception:
Invalid argument(s): Failed to lookup symbol (dlsym(0x7fbf5fd260d0, hello_world): symbol not found)
#0      DynamicLibrary.lookup (dart:ffi-patch/ffi_dynamic_library_patch.dart:31:29)
#1      main (file:///Users/ryo/github/UseOpenCVwithFlutter/ffi/cpp/hello.dart:21:8)
#2      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:283:19)
#3      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)

絶対パスはDLL指定し、展開することはできている。しかし、DLLの中身にhello_world関数がないらしい...

わけわからん

このスクラップは2021/08/16にクローズされました