Blog スタッフブログ

C++ Flutter システム開発

[Flutter]Flutterからdart:ffiでC++のコードを呼び出す

こんにちは、株式会社MIXシステム開発担当のBloomです。

今回はFlutterプロジェクトからC++など別言語の関数をコールするdart:ffiを利用する方法を掲載させていただきます。

まずはdart:ffiを導入しましょう。Googleが提供するチュートリアルでテンプレートの導入方法が紹介されているため、こちらに沿ってプラグインテンプレートを生成していきます。

flutter create --template=plugin_ffi \
  --platforms=android,ios ffigen_app

テンプレートの生成が完了したらffigen_appのプロジェクトの中にexampleのFlutterプロジェクトが内包されていることが確認できます。exampleプロジェクトを開き、ビルドして動作確認を行いましょう。

実行結果

ここまでできたらC++側で新たに関数を実装し、それを呼び出してみましょう。例としてリソースに含めたテキストファイルのパスを渡し、そのファイルの内容を読み出して文字列を返却する処理を構築してみます。

まずはffigen_app/srcへC++のソースを実装しましょう。

ffigen/app/src/ffigen_app.hへ追記

#ifdef __cplusplus
extern "C" {
#endif

char* ffigen_read_text_file(const char* path);
void  ffigen_free(void* p);

#ifdef __cplusplus
}
#endif

ffigen/app/src/read_text.c

#include "ffigen_app.h"
#include <stdlib.h>

// C++側の関数(Cから呼べるようにCリンケージで宣言)
#ifdef __cplusplus
extern "C" {
#endif
char* text_reader_cpp_read_utf8(const char* path);
#ifdef __cplusplus
}
#endif

char* ffigen_read_text_file(const char* path) {
    return text_reader_cpp_read_utf8(path);
}

void ffigen_free(void* p) {
    free(p);
}

ffigen/app/src/read_text.cpp

#include <cstdlib>
#include <cstring>
#include <fstream>
#include <sstream>
#include <string>

extern "C" char* text_reader_cpp_read_utf8(const char* path) {
  if (!path) return nullptr;

  std::ifstream ifs(path, std::ios::binary);
  if (!ifs) return nullptr;

  std::ostringstream oss;
  oss << ifs.rdbuf();
  const std::string s = oss.str();

  char* out = static_cast<char*>(std::malloc(s.size() + 1));
  if (!out) return nullptr;

  std::memcpy(out, s.data(), s.size());
  out[s.size()] = '\0';
  return out;
}

CのソースからC++をコールする形で設計しています。これはextern “C”で名前修飾を止めてC++を直接参照させる実装でも良いのですが、学習のためこの設計を採用しています。さて、このソースをコンパイルするために必要な部分へ追記していきましょう。

ffigen_app/src/CMakeLists.txt

cmake_minimum_required(VERSION 3.10)

project(ffigen_app_library VERSION 0.0.1 LANGUAGES C CXX)←「CXX」を追記しています

add_library(ffigen_app SHARED
  "ffigen_app.c"
  "read_text.c"
  "read_text.cpp"
)

set_target_properties(ffigen_app PROPERTIES
  PUBLIC_HEADER ffigen_app.h
  OUTPUT_NAME "ffigen_app"
)

set_property(TARGET ffigen_app PROPERTY CXX_STANDARD 11)←このset_property二行も追加
set_property(TARGET ffigen_app PROPERTY CXX_STANDARD_REQUIRED ON)

target_compile_definitions(ffigen_app PUBLIC DART_SHARED_LIB)

if (ANDROID)
  # Support Android 15 16k page size
  target_link_options(ffigen_app PRIVATE "-Wl,-z,max-page-size=16384")
endif()

ffigen_app/iOS/Classes/ffigen_app.c

// Relative import to be able to reuse the C sources.
// See the comment in ../ffigen_app.podspec for more information.
#include "../../src/ffigen_app.c"
#include "../../src/read_text.c"

ffigen_app/iOS/Classes/ffigen_app.mm

// C++ sources forwarder (compile as Objective-C++)
#include "../../src/read_text.cpp"

ここまでたどり着いたらコンソールから下記コマンドを実行しビルドしましょう。

dart run ffigen --config ffigen.yaml

さて、ビルドが完了するとffigen_app_bindings_generated.dartへこの関数を呼び出すインターフェースが自動的に生成されます。

それを中継するためのffigen_app/lib/ffigen_app.dartへ追記しましょう。


import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'package:ffi/ffi.dart';

import 'ffigen_app_bindings_generated.dart';

String readTextFile(String path) {
  final pathPtr = path.toNativeUtf8();
  try {
    final outPtr =
    _bindings.ffigen_read_text_file(pathPtr.cast<Char>());
    if (outPtr == nullptr) {
      throw StateError('ffigen_read_text_file returned null. path=$path');
    }
    try {
      return outPtr.cast<Utf8>().toDartString();
    } finally {
      // Free native-allocated buffer
      _bindings.ffigen_free(outPtr.cast<Void>());
    }
  } finally {
    malloc.free(pathPtr);
  }
}

あとは先ほどのexampleプロジェクトからこの関数をコールするだけです。assetsへの追加手順については省いて、このように追記してみましょう。

import 'dart:io';

import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart' show rootBundle;
import 'package:path_provider/path_provider.dart';

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

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

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late Future<String> asyncResult;

  @override
  void initState() {
    super.initState();
    asyncResult = _readTextFile();
  }

  Future<String> _readTextFile() async {
    final filePath = await materializeAssetToTempPath("assets/sample.txt");
    return ffigen_app.readTextFile(filePath);
  }

  Future<String> materializeAssetToTempPath(String assetKey) async {
    final data = await rootBundle.load(assetKey); //
    final dir = await getTemporaryDirectory();
    final file = File('${dir.path}/${assetKey.split('/').last}');
    await file.writeAsBytes(data.buffer.asUint8List(), flush: true);
    return file.path;
  }

  @override
  Widget build(BuildContext context) {
    const textStyle = TextStyle(fontSize: 25);
    const spacerSmall = SizedBox(height: 10);
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Native Packages'),
        ),
        body: SingleChildScrollView(
          child: Container(
            padding: const .all(10),
            child: Column(
              children: [
                const Text(
                  'This calls a native function through FFI that is shipped as source in the package. '
                  'The native code is built as part of the Flutter Runner build.',
                  style: textStyle,
                  textAlign: .center,
                ),
                spacerSmall,
                FutureBuilder<String>(
                  future: asyncResult,
                  builder: (BuildContext context, AsyncSnapshot<String> value) {
                    final displayValue =
                        (value.hasData) ? value.data : 'loading';
                    return Text(
                      'await asyncResult = $displayValue',
                      style: textStyle,
                      textAlign: .center,
                    );
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

実行結果

これでFlutterからC/C++のソースを呼び出すことができました。良かったですね。