【Flutter】C / C++で定義した関数を呼ぶ

この記事は約12分で読めます。

FlutterからC / C++の関数を呼ぶことにより得られる効果

FlutterからCやC++の関数を呼ぼうとしているような方はすでにご存知かと思いますが、下記のような効果を得ることが出来ます。

  1. CやC++の資産を利用できる
  2. 処理速度の高速化が見込める

既にCやC++で実装済みのライブラリがある場合にはわざわざDartで書き直すよりも既存のライブラリをそのまま利用した方がよい場合があります。Flutter側から呼ぶインターフェースとしてはC言語である必要がありますが、そこから先はC++を利用することもできるのでC++のライブラリも利用できます。また、とても重い処理を実行する場合などはCやC++で実装してしまった方が処理速度が速くなり、ユーザの使用感などの面からよい場合もあります。

本記事では、C言語のインターフェースを介してC++のコードを利用する例について説明します。

ベースとなるコード

まず、Dartのみで実装したベースとしてlib/main.dartを下記のように書きます。

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: MainView());
  }
}

class MainView extends StatefulWidget {
  const MainView({super.key});

  @override
  State<StatefulWidget> createState() => MainViewState();
}

class MainViewState extends State<MainView> {
  var ivalue = 0;
  var fvalue = 0.0, dvalue = 0.0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
            child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
              Flexible(child: Text('int: $ivalue')),
              Flexible(child: Text('float: $fvalue')),
              Flexible(child: Text('double: $dvalue')),
            ])),
        floatingActionButton: IconButton(
          icon: const Icon(Icons.add),
          onPressed: () {
            setState(() {
              // ここをC++の処理にあとで置き換える
              ivalue++;
              fvalue += 1.0;
              dvalue += 1.0;
            });
          },
        ));
  }
}

この時点でもちろんflutter runすれば動作します。本記事では簡単な例として44〜46行目の処理をCの関数呼び出しに置き換えることを考えます。

C++側の実装

まずはC++側の実装を行っていきます。前述のコードの足し算を置き換えるためにint型、float型、double型の足し算の関数を実装します。

まず、ヘッダファイルから示します。本記事ではandroid/app/src/main/cpp/ffi.hに配置している想定です。

#pragma once

#ifdef __cplusplus
// コンパイルはC++で行うので、Dart側にCの関数として見えるようにする
#define EXPORT_FUNCTION extern "C" __attribute__((visibility("default"))) __attribute__((used))
#else
// ffigenから見るときは何も付加しない
#define EXPORT_FUNCTION
#endif

// Dart側から呼ぶ関数のプロトタイプ宣言
EXPORT_FUNCTION int addInt(int a, int b);
EXPORT_FUNCTION float addFloat(float a, float b);
EXPORT_FUNCTION double addDouble(double a, double b);

コメントにも記載していますが、_cplusplusが定義されているかどうかでEXPORT_FUNCTIONの定義を変えています。コンパイルはC++として行いますが、Dartから呼び出すときにはCとして見えなければいけないため、コンパイル時にはextern "C"が付加されている必要があります。一方で、D後述するffigenからはC言語のヘッダファイルとして見える必要があるため、extern "C"などのC++特有の記述が含まれないようにする必要があります。そのため、__cplusplusが定義されているかどうかでEXPORT_FUNCTIONの内容を変えています。

注意点として、ヘッダファイルの拡張子は必ず.hである必要があります。拡張子を.hppとしてしまうと、後述するffigenからはC++と認識されてしまい、__cplusplusが定義されているものとして扱われてしまうからです。

次にソースファイルです。本記事では、android/app/src/main/cpp/ffi.cppに配置している想定です。

#include "ffi.h"

template<class T>
T add(T a, T b) {
  return a + b;
}

EXPORT_FUNCTION int addInt(int a, int b) {
  return add(a, b);
}

EXPORT_FUNCTION float addFloat(float a, float b) {
  return add(a, b);
}

EXPORT_FUNCTION double addDouble(double a, double b) {
  return add(a, b);
}

それぞれ引数と戻り値の型が異なりますが、いずれも単純に足し算を行っているだけです。ここでは無理やりC++感を出すためにテンプレートで実装したadd関数を使用した実装としました。

ffigenによるDart側のインターフェース生成

ターミナルなどからflutter pub add ffigenとコマンドを打つことによりffigenを導入します。次に、pubspec.yamlに以下の記述を追加します。

ffigen:
  output: 'lib/generated_bindings.dart'
  headers:
    entry-points:
      - 'android/app/src/main/cpp/ffi.h'

outputは出力するDartのソースコードのパスを記述します。headersentry-pointsには元となるヘッダファイルのパスを記述します。Dartのソースコード生成を実行するためには下記コマンドを実行します。

$ dart run ffigen

このコマンドによりpubspec.yamlに記述した内容に従ってDart側のソースコードが生成されます。ただし、ここで生成されるDart側のソースコードはC++側の関数を呼び出すためのものなので、C++側のコンパイルが別途必要になります。

C++のコンパイル

AndroidではCMakeとgradleへの記述によりC++のコンパイルを実行することになります。適切に記述しておけばflutter runを実行するときにCのコンパイルが自動的に実行されます。

今回は下記のようにCMakeLists.txtを書きました。これはandroid/app/CMakeLists.txtに配置する想定です。

cmake_minimum_required(VERSION 3.10)

project(ffi)

file(GLOB_RECURSE SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/*.cpp)

add_library(ffi SHARED ${SOURCES})
target_compile_options(ffi PUBLIC -O2)

add_libraryffiという名前を指定しているため、成果物としてlibffi.soという名称の共有ライブラリが生成されます。共有ライブラリの名称はDartから呼び出すときに必要です。

CMakeLists.txtを書いただけではflutter runに連動してコンパイルは実行されません。android/app/build.gradleにも追加で記述する必要があります。下記の記述を追加します。

android {
    // ・・・
    // 中略
    // ・・・

    // 追加
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

これでC++のコンパイルも自動で実行されるようになりました。

C++で定義した関数の利用

最後にDartからC++で定義した関数を呼び出すように修正します。修正後のmain.dartを示しますが、コメントの入っているところが修正点です。

import 'package:flutter/material.dart';

// 共有ライブラリを読み込むために追加
import 'dart:ffi';

// C++関数のDart側の定義
import 'generated_bindings.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: MainView());
  }
}

class MainView extends StatefulWidget {
  const MainView({super.key});

  @override
  State<StatefulWidget> createState() => MainViewState();
}

class MainViewState extends State<MainView> {
  var ivalue = 0;
  var fvalue = 0.0, dvalue = 0.0;

  // 共有ライブラリを読み込み、C++側の関数を呼ぶ準備をする
  final lib = NativeLibrary(DynamicLibrary.open('libffi.so'));

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
            child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
              Flexible(child: Text('int: $ivalue')),
              Flexible(child: Text('float: $fvalue')),
              Flexible(child: Text('double: $dvalue')),
            ])),
        floatingActionButton: IconButton(
          icon: const Icon(Icons.add),
          onPressed: () {
            setState(() {
              // C++側の関数を呼ぶ
              ivalue = lib.addInt(ivalue, 1);
              fvalue = lib.addFloat(fvalue, 1.0);
              dvalue = lib.addDouble(dvalue, 1.0);
            });
          },
        ));
  }
}

これでflutter runすれば、Dartだけで書いたベースのコードと同様の動作をします。

まとめ

Flutterを用いたAndroidアプリでC++の関数を呼び出す方法について確認しました。下記の対応をすることでC++の関数を呼び出すことが出来ました。

  • C++の関数を実装(Dartから呼び出す関数はCの関数として見せる)
  • ヘッダファイルを用意
    • コンパイル時はC++として扱う
    • DartからはCとして見えるようにする
  • CMakeLists.txtを書く
  • build.gradleでCMakeLists.txtが使用されるようにする
  • pubspec.yamlにffigenに関する記述を行う
  • dart run ffigenを実行する
  • 生成されたDart側の関数定義を呼ぶ

手間はかかりますが、C++を利用することにより得られるメリットもそれなりにあると思うので、適材適所で使い分けていければと思います。

コメント

タイトルとURLをコピーしました