その簡単さに驚く

     前回の記事で作った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)のコードに修正しました。