その簡単さに驚く
前回の記事で作ったC++製のプログラムをアプリで利用しない手は無いと思いちょっと調べてみたら非常に簡単に作れそうなのでやってみました。自分の場合は取り敢えずandroid版しか考えていないので、公式サイトに書かれている僅かな手順を踏めば、ライブラリ(Plugin)とそのテストをするためのExampleプロジェクトが出来上がります。
情報が少なめ
Flutter,Pluginで検索するとPluginを公開する前提で書かれている記事が多くて、自分のアプリ用のライブラリとして使うケースについては情報が少ないような気がします。Java(あるいはKotlin)やSwiftを経由してC++のライブラリを使う方法なんかもあるようですが、C++だけで済ませたかった自分には参考になる記事は少なかったです。
前述の公式サイトの記事でDart:ffiを使えばint型のデータだけなら簡単にやり取りできるのは分かったのですが、intだけじゃ大変だなと思っていたところ、「Flutterプラグインでdart:ffiを使ってみる」というPDFファイルを読んでポインタ経由なら他のデータでもやり取り出来そうなことが分かって、やってみる気になりました。Dart:ffiだけで不足している機能はffiパッケージとして公開されているものを使います。
ライブラリ(Plugin)用のプロジェクト作成
iosでの利用も考えている人は違ってくると思います。
flutter create --org io.github.happyclam --template=plugin --platforms=android karah_think
プロジェクトを作成したらflutter build apkで一度ビルドした後、android/下のCMakeList.txtの編集
# CMakeList.txt
cmake_minimum_required(VERSION 3.4.1) # for example
set(CMAKE_CXX_FLAGS "-O2 -std=c++11 -Wall")
add_library( karah_think
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
../android/Classes/karah_think.cpp )
android/下にClassesという名のディレクトリを作成(何でもいい)して、C++のソースは全てそこに入れました。/android/Classes/karah_think.cppがアプリから呼び出される関数(エクスポート関数)が書かれているファイルです。前回の記事でも書いたコンパイルオプションもこのファイル(CMakeList.txt)で定義します。
これだけでライブラリ開発環境が出来て、karah_thinkプロジェクト内に自動作成されたExampleプロジェクトをAndroid Studioで読み込めばライブラリの開発&テストが出来ます。Exampleプロジェクトをビルドすればライブラリ(libkarah_think.so)もビルドされます。
ここまでは上記の公式サイトに書かれている内容です。
作ったライブラリ(Plugin)をアプリのプロジェクトで使う
ライブラリの中身はマンカラで使う要素14個の固定長配列、手番(先手・後手)、読みの深さを渡して最善手を返して貰うラッパー関数を書くだけなのですぐに実装出来ました。でも、この出来上がったライブラリをどうやって元々存在するマンカラアプリのプロジェクトから使うのか分からなかったのですが、Flutterで公開されているパッケージを使う時と同じように、pubspec.yamlに依存関係を記述するだけでした。
マンカラアプリのプロジェクトのpubspec.yaml内の依存関係に、ライブラリのプロジェクトが存在するローカルのPathを追記します。
#pubspec.yaml
dependencies:
flutter:
sdk: flutter
karah_think:
path: "../karah_think/"
ffi: ^0.1.3
マンカラアプリプロジェクト(mancala)とC++ライプラリプロジェクト(karah_think)とライブラリ動作確認用プロジェクト(example)の関係は以下のようになっています。
├── mancala
│ ├── pubspec.yaml
├── karah_think
│ ├── example
ライブラリを使う側のマンカラアプリプロジェクトのlib/main.dartの宣言部分はこんな感じです。
引数は評価値と指し手を受け取るための配列、マンカラの盤面情報を渡す配列、手番(先手・後手)、読みの深さになってます。
#mancala/lib/main.dart
import 'dart:typed_data';
import 'dart:ffi'; // For FFI
import 'dart:io'; // For Platform.isX
import 'package:ffi/ffi.dart';
final DynamicLibrary karahThinkLib = Platform.isAndroid
? DynamicLibrary.open("libkarah_think.so")
: DynamicLibrary.process();
final int Function(Pointer<Int32> pResult, Pointer<Int32> board, int turn, int depth) karahThink =
karahThinkLib
.lookup<NativeFunction<Int32 Function(Pointer<Int32>, Pointer<Int32>, Int32, Int32)>>("karah_think")
.asFunction();
ライブラリ(libkarah_think.so)のエクスポート関数を呼び出す部分
#mancala/lib/main.dart
List _cppThink(List board, int turn, int depth){
final pBoard32 = calloc<Int32>(board.length);
Pointer<Int32> pResult32 = calloc<Int32>(3);
pResult32[0] = 0; pResult32[1] = 0; pResult32[2] = 0;
for (var i = 0; i < board.length; i++){
pBoard32[i] = board[i];
}
int ret = karahThink(pResult32, pBoard32, turn, depth);
Int32List list = pResult32.asTypedList(3);
List result = List.from(list);
// print("=== result = ${result}");
calloc.free(pBoard32);
calloc.free(pResult32);
return result;
}
ライブラリのエクスポート関数
/* karah_think/android/Classes/karah_think.cpp */
#include <stdint.h>
#include <iostream>
#include <array>
#include <map>
#include "./const.cpp"
#include "./player.cpp"
extern "C" __attribute__((visibility("default"))) __attribute__((used))
int32_t karah_think(int32_t* pResult, int32_t* pBoard, int32_t turn, int32_t depth) {
int32_t ret = 0;
std::array<int, DENTS> board{0,0,0,0,0,0,0,0,0,0,0,0,0,0};
std::array<int, 2> result{0, 0};
for(int i = 0; i < DENTS; i++){
board[i] = pBoard[i];
}
Player *pFirst = new Player(FIRST);
Player *pSecond = new Player(SECOND);
if (turn == FIRST){
result = pFirst->think(board, pSecond, depth, FIRST_WIN);
} else {
result = pSecond->think(board, pFirst, depth, SECOND_WIN);
}
ret = result[0];
pResult[0] = result[0];
pResult[1] = result[1];
delete pFirst;
delete pSecond;
return ret;
}
C++側はSTLを使っているので型に合わせた変数に代入し直して利用します。
効果は歴然
今まではマンカラアプリのAIのレベルを「やばい」にすると一手毎に待たされていたのですが、C++製ライブラリを使ったものは他のAIレベルと同じぐらいのレスポンスになりました。
Windows用のライブラリと違って、様々なCPUに合わせたAndroid端末毎のDLLが簡単なGUI操作(中で活躍してるのはgradleですが)でビルド出来るのはすごいです。
将棋関連アプリも早くこの形でリリースしたいところですが、マンカラのように一つの固定長配列を渡すだけでは済まないので、いろいろ試してみないと分からないことが多く道のりは遠いです。
追記(2023-09-20)
上で紹介している公式サイトのURLが変わった?ー>現在はここです
あと、ffiパッケージの記述も古かったので現在アプリで使っているバージョン(ffi: ^2.1.0)のコードに修正しました。
追記(2026-07-25)
最近新しい環境でビルドし直す機会があったので更新しておきます。->公式サイト
Dart側もC++側もソースコードは書き換える必要ありませんが、Flutterが出力するテンプレートファイル群もディレクトリ構成も結構変わっていますので、CMakeLists.txtとヘッダーファイルは書き換える必要がありました。
現環境
- Flutter 3.44.6
- Android Studio Quail 2
- Gradle 9.5
- ffi: 2.1.3
プロジェクト作成時に現在はGUI画面でFFI_PLUGIN作成のためのメニューが選べます。
古い記事内容との相違点
1、C++のソースファイルはプロジェクト下のsrc/ディレクトリ内に配置(昔のようにディレクトリを適当に作るのは良くなさそう)
プロジェクト作成時にFFI_PLUGINを選ぶと、Flutterによってsumとsum_long_runningというサンプルのエクスポート関数が一緒に出力されますが、サンプルの関数はC++ではなくC言語ですし、メソッドチャネル呼び出し?用のkalah_think.dart内にバインディング処理が書かれていますが、この記事の趣旨のようにFFI Pluginを直接使う場合はdartファイルを編集する必要はありません。
2、MakeファイルをC++用に書き換える
//CMakeLists.txt
# The Flutter tooling requires that developers have CMake 3.10 or later
# installed. You should not increase this version, as doing so will cause
# the plugin to fail to compile for some customers of the plugin.
cmake_minimum_required(VERSION 3.10)
project(kalah_think_library VERSION 1.14.0 LANGUAGES CXX)
add_library(kalah_think SHARED
"kalah_think.cpp"
)
set(CMAKE_CXX_FLAGS "-O2 -std=c++11 -Wall")
set_target_properties(kalah_think PROPERTIES
PUBLIC_HEADER kalah_think.h
OUTPUT_NAME "kalah_think"
)
target_compile_definitions(kalah_think PUBLIC DART_SHARED_LIB)
if (ANDROID)
# Support Android 15 16k page size
target_link_options(kalah_think PRIVATE "-Wl,-z,max-page-size=16384")
endif()
※CからCXXに書き換える
3、ライブラリ用ヘッダーファイルをC++用に書き換える
// kalah_think.h
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <array>
#include <map>
#include "./const.cpp"
#include "./player.cpp"
#if _WIN32
#include <windows.h>
#else
#include <pthread.h>
#include <unistd.h>
#endif
#if _WIN32
#define FFI_PLUGIN_EXPORT __declspec(dllexport)
#else
#define FFI_PLUGIN_EXPORT
#endif
#ifdef __cplusplus
extern "C" {
#endif
FFI_PLUGIN_EXPORT int32_t think(int32_t* pResult, int32_t* pBoard, int32_t turn, int32_t depth);
#ifdef __cplusplus
}
#endif
※extern "C"を追記
