「dart:ffi」の技術調査
まとめ
dart:ffi技術調査の動機
卒業研究用で開発しているアプリケーションは、
- ネイティブで開発
- iOS : Swift,OpenCV
- Android : Java,OpenCV
- アジャイル
- 今後も後輩に引き継ぎ、アップデートをしていく
のためネイティブではなく、クロスプラットフォームにリプレースする必要があった。
目的
FlutterでOpenCV(現状の資産)を活用することが目的である。そのため、FFIで直接C++のコードを有効活用したかった。
結果
dart:ffiは使用しない。
理由
- OpenCVを理解していない(する気もない)
- C++が意味わからん
- 2次情報が少ない
- 調査段階でC++の関数が呼び出せていない
最後に
一応成功例はあるため、技術的にFlutterでOpenCVをdart:ffiで運用することはできる。
だが開発が難しく、保守も困難になるため断念する。
今後は、MethodChannelの調査を行う。
C interop using dart:ffiの内容を和訳し、学習した時のメモ。
準備
Dart 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();
}
手順
- dart:ffiをインポート
import 'dart:ffi' as ffi;
- パスライブラリをインポート(ダイナミックリンクライブラリのパスを格納する)
import 'dart:io' show Platform, Directory;
import 'package:path/path.dart' as path;
- typedefでFFIを用いたC言語の関数を呼び出すときに必要な変数(hello_world_func)を作成
typedef hello_world_func = ffi.Void Function();
- typedefでC言語の関数を呼び出すときに必要な変数(HelloWorld)を作成
typedef HelloWorld = void Function();
- ダイナミックリンクライブラリのパスを格納する変数を作成
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');
}
- C言語の関数を含むダイナミックリンクライブラリを開く
final dylib = ffi.DynamicLibrary.open(libraryPath);
- 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();
- C言語の関数を呼び出す
hello();
「Cmake」とは?
コンパイラに依存せず自動化する
ためのツール。つまり、開発環境(OS)が変化しても同一のソースコードでビルド可能なツール。
サポートする任意のプロジェクトファイルを作成する
CmakeLists.txt
という設定ファイルを作成すれば、Cmakeがサポートする任意のプロジェクトファイルを作成する
ことができる。
つまり、
- Cmakeで
*.sln
,*.xcodeproj
,Makefile
などのプロジェクトファイルを生成する - 生成したプロジェクトファイルを使ってビルドする
以上、2ステップでプロジェクトをビルドできる。
多彩な開発環境では、必要不可欠な技術
C++をはじめ、OSに縛られない言語をチームで開発する場合、Cmakeで開発環境を整備するのは必要不可欠な作業である。
なぜ必要なのか?
コンパイラの違い
プログラムの実行 = 標準ライブラリ + コンパイラ
のため、開発環境(コンパイラ)が変化するとソースコードを書き直す必要があった。
プロジェクトファイルの使い分け
標準ライブラリだけで書かれたプログラムは、どのコンパイラでもビルドは可能である。そのため、ソースコードの共有は可能である。しかしコンパイラのオプション、つまりビルドに必要な設定項目
やそれを格納する設定ファイルのフォーマット
は統一されていない。
- GCC on Linux: `Makefile`
- MSVC on Windows: `*.sln`
- Clang on macOS: `Makefile` or `*.xcodeproj`
といったように、開発環境に合わせてプロジェクトファイルを使い分ける必要がある。
「プロジェクトファイルを自動生成」すればいいのではないか?
任意のプロジェクトファイルを自動生成すれば、開発環境が変化してもソースコードを共有化できるのではないか?
という考えから、Cmake
が生まれた。
参考文献
独自に試してみる
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.dylib
とlibhello.1.0.0.dylib
を公式は作成している。そのため、両者もしくはどちらかのダイナミックライブラリが必要だと思われる。
DartFFIとの相互運用性|セッション
サンプルプログラム
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)
結論
ダイナミックライブラリが悪い。
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)
結果
かわんねぇ
絶対パスでダイナミックライブラリを指定すると実行できる
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)
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
躓いていた原因
ダイナミックライブラリのパスを指定する方法を間違えていた。
確認
指定方法を間違えたプログラム
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
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
関数がないらしい...
わけわからん