Go back

Integrating lib3mf, Part 2: Creating a Flutter FFI Plugin and Adding Android Support

Published:  at  06:00 PM
⏱️ 1815 words • 10 min read

阅读中文版

Creating a Flutter FFI plugin for lib3mf, configuring the Android NDK, redirecting std::cout to Logcat, and handling Android 15's 16 KB page size requirements.

Preface

lib3mf integration series (1): compilation and sample testing introduced the compilation and sample testing of lib3mf. This article will introduce how to create a Flutter FFI plug-in and integrate it on the Android side.

Create Flutter FFI plug-in

Flutter has a built-in command to create ffi plug-in, the form is as follows

flutter create --template=plugin_ffi --platforms=macos native_lib3mf
flutter create --template=plugin_ffi --platforms=android native_lib3mf

Since my target platforms are macOS and Android, the specified platforms are macOS and android. Readers can specify the platform according to their own target platform.

The template generates the sum function for us, which is defined as follows

FFI_PLUGIN_EXPORT int sum(int a, int b) { return a + b; }

It also binds the call in the dart layer for us

int sum(int a, int b,) {
  return _sum(a, b, );
}
late final _sumPtr =
    _lookup<ffi.NativeFunction<ffi.Int Function(ffi.Int, ffi.Int)>>('sum');
late final _sum = _sumPtr.asFunction<int Function(int, int)>();

We create a simple route to test calling

import 'package:flutter/material.dart';
import 'package:native_lib3mf/native_lib3mf.dart' as nn;

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

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      constraints: BoxConstraints.expand(),
      child: Center(
        child: Material(color: Colors.transparent,child: Text('Native Lib3mf: 2+3=${nn.sum(2, 3)}')),
      ),
    );
  }
}

At this point, a minimal Flutter FFI plug-in has been created. Next, the integration on the Android side will be introduced.

Android side integration

At first, I naively thought that as long as I threw the native code into the Flutter project, Flutter would automatically compile the corresponding library files for each platform (for example, Android generates .so, macOS generates .dylib). It turns out that things are far from that simple - Flutter can indeed automatically compile the simple C code of the plug-in itself; but once a third-party C++ library like lib3mf is introduced, complex issues such as cross-compilation, ABI compatibility, and dependency management need to be manually handled. This is why we need to specify —platforms=android and —platforms=macos when executing commands before - the plug-in template builds the skeleton of cross-platform compilation for us, but the details of integrating third-party libraries still need to be filled in by ourselves.

If you want further automation, you can try integrating lib3mf directly into the plugin as a CMake subproject (add_subdirectory), so that lib3mf will be automatically compiled by Flutter’s build system along with the plugin code. However, it should be noted that lib3mf depends on libraries such as libzip. You need to ensure that these dependencies can be compiled correctly under Android NDK and macOS. In order to focus on the core integration process, this tutorial adopts the method of precompiled libraries. It first solves the problem of “how to make lib3mf run”, and then considers the optimization of “how to make it more automated”. Interested readers can explore on their own.

Compile Android platform lib3mf.so

By default, make uses the compilation tool chain in the system environment. If you want to compile the so file of the Android platform, you need to use the compilation tool chain provided by the android ndk. Regarding the installation of android ndk, you can refer to Install NDK, which will not be repeated in this article. The ANDROID_NDK_HOMEenvironment variable has been configured in the default environment, so it can be used directly

mkdir build && cd build
cmake .. \
  -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
  -DANDROID_ABI=arm64-v8a \
  -DANDROID_PLATFORM=android-21 \
  -DCMAKE_BUILD_TYPE=Release

| Parameter | Function | | -------------------- | ------------------------------------------------------------------- | ------------------- | | CMAKE_TOOLCHAIN_FILE | Points to the CMake tool chain file | that comes with NDK | | ANDROID_ABI | Specifies the architecture. The current mainstream is arm64-v8a | | ANDROID_PLATFORM | Minimum API level, it is recommended to set it to android-21 (5.0+) |

An error was reported during compilation, indicating that #include <sys/attr.h> could not be found. This is because the header file is unique to Apple. Modify Libraries/libzip/Source/unix/zip_source_file_stdio_named.c

#if defined(__APPLE__) && !defined(__ANDROID__)
#include <sys/attr.h>
#include <sys/clonefile.h>
#endif

The compilation was successful again and the lib3mf.so —arm64-v8a version was obtained.

Modify the -DANDROID_ABI parameter to armeabi-v7a to get lib3mf.so — armeabi-v7a version

Copy them to the corresponding folders respectively. /native_lib3mf/android/jniLibs/armeabi-v7a /native_lib3mf/android/jniLibs/arm64-v8a

The same applies to other abi

Copy header file

When generating so in the previous step, the header file has been generated. It is located under /lib3mf/Autogenerated/Bindings/Cpp

lib3mf_abi.hpp
lib3mf_implicit.hpp
lib3mf_types.hpp

Copy these files to /native_lib3mf/src

Write calling code

Refer to /lib3mf/SDK/Examples/Cpp/Source/ExtractInfo.cpp and write extract_info.cpp The parsing part will not be processed for the time being, and only the version will be printed to ensure that the transplant is successful and can be run.

Note: std::cout on Android will not be output to logcat, so it will be replaced by __android_log_print later. It is left as it is here for the convenience of readers’ comparison.

#include <algorithm>
#include <iostream>
#include <lib3mf_implicit.hpp>
#include <string>

#include "native_lib3mf.h"
using namespace Lib3MF;

void printVersion(PWrapper wrapper) {
  Lib3MF_uint32 nMajor, nMinor, nMicro;
  wrapper->GetLibraryVersion(nMajor, nMinor, nMicro);
  std::cout << "lib3mf version = " << nMajor << "." << nMinor << "." << nMicro;
  std::string sReleaseInfo, sBuildInfo;
  if (wrapper->GetPrereleaseInformation(sReleaseInfo)) {
    std::cout << "-" << sReleaseInfo;
  }
  if (wrapper->GetBuildInformation(sBuildInfo)) {
    std::cout << "+" << sBuildInfo;
  }
  std::cout << std::endl;
}

extern "C" {
FFI_PLUGIN_EXPORT int32_t extract_info(const char* path) {
  PWrapper wrapper = CWrapper::loadLibrary();
  std::cout
      << "------------------------------------------------------------------"
      << std::endl;
  std::cout << "3MF Read example" << std::endl;
  printVersion(wrapper);
  std::cout
      << "------------------------------------------------------------------"
      << std::endl;
  return 0;
}
}

Write CMakeList.txt script

cmake_minimum_required(VERSION 3.10)

# 迁移到了CXX, 默认是C的
project(native_lib3mf_library VERSION 0.0.1 LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

message(STATUS "DEBUG: CMAKE_CURRENT_SOURCE_DIR is: ${CMAKE_CURRENT_SOURCE_DIR}")

# 确定当前路径位置
get_filename_component(PLUGIN_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE)
# 手动指定so文件位置
set(LIB3MF_SO_PATH "${PLUGIN_ROOT}/android/jniLibs/${ANDROID_ABI}/lib3mf.so")

message(STATUS "NativeLib3mf: Target library path: ${LIB3MF_SO_PATH}")

add_library(native_lib3mf SHARED
  "native_lib3mf.c" "extract_info.cpp"
)

if(EXISTS "${LIB3MF_SO_PATH}")
    message(STATUS "NativeLib3mf: File physical check SUCCESS.")
    target_link_libraries(native_lib3mf PRIVATE ${LIB3MF_SO_PATH} dl
    log)
else()
    message(FATAL_ERROR "NativeLib3mf: File NOT found at ${LIB3MF_SO_PATH}")
endif()



target_include_directories(native_lib3mf PRIVATE .)

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


target_compile_definitions(native_lib3mf PUBLIC DART_SHARED_LIB)

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

As a result, due to the lack of x86 so, the compilation was interrupted with an error.

Try to modify the configuration of android/build.gradle.kts in the project root directory

    defaultConfig {
        // 	无关配置
        ndk {
            abiFilters += listOf("arm64-v8a", "armeabi-v7a")
        }
    }

In the end, he still tried to compile x86.

202602270025 Suddenly I remembered that I need to configure the native_lib3mf Android submodule. After configuration, there is no need to distinguish between platforms.

android {
    defaultConfig {
        ndk {
            // 设置支持的 SO 库架构
            abiFilters 'armeabi-v7a', 'arm64-v8a'
        }
    }
}

In order to solve this problem, it needs to be proactively dealt with

Modify CMakeLists.txt, skip if not found

# 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(native_lib3mf_library VERSION 0.0.1 LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)


add_library(native_lib3mf SHARED
  "native_lib3mf.c" "extract_info.cpp"
)

message(STATUS "DEBUG: CMAKE_CURRENT_SOURCE_DIR is: ${CMAKE_CURRENT_SOURCE_DIR}")

get_filename_component(PLUGIN_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE)
set(LIB3MF_SO_PATH "${PLUGIN_ROOT}/android/jniLibs/${ANDROID_ABI}/lib3mf.so")

message(STATUS "NativeLib3mf: Target library path: ${LIB3MF_SO_PATH}")


if(EXISTS "${LIB3MF_SO_PATH}")
    message(STATUS "NativeLib3mf: Found library for ${ANDROID_ABI}")
    target_link_libraries(native_lib3mf PRIVATE "${LIB3MF_SO_PATH}" dl log)
else()
    # 对于没有 lib3mf.so 的架构(如 x86),只链接系统库
    # 配合代码里的 #if 宏,链接器就不会报 undefined symbol 了
    message(WARNING "NativeLib3mf: No lib3mf.so for ${ANDROID_ABI}, building stub version.")
    target_link_libraries(native_lib3mf PRIVATE dl log)
endif()

target_include_directories(native_lib3mf PRIVATE .)

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


target_compile_definitions(native_lib3mf PUBLIC DART_SHARED_LIB)

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

At the same time, macros are added to the code to handle platform conditions such as x86, and std::cout is replaced with android printing, so that the output can be seen

#include <algorithm>
#include <iostream>
#if defined(__arm__) || defined(__aarch64__)
#include <lib3mf_implicit.hpp>
#endif
#include <string>

#ifdef __ANDROID__
#include <android/log.h>
#define LOG_TAG "NativeLib3mf"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#else
#include <cstdio>
#define LOGI(...)      \
  printf(__VA_ARGS__); \
  printf("\n")
#endif

#include "native_lib3mf.h"
#if defined(__arm__) || defined(__aarch64__)
using namespace Lib3MF;

void printVersion(PWrapper wrapper) {
  Lib3MF_uint32 nMajor, nMinor, nMicro;
  wrapper->GetLibraryVersion(nMajor, nMinor, nMicro);

  // 获取预发布和构建信息
  std::string sReleaseInfo, sBuildInfo;
  bool hasRelease = wrapper->GetPrereleaseInformation(sReleaseInfo);
  bool hasBuild = wrapper->GetBuildInformation(sBuildInfo);

  // 格式化输出
  if (hasRelease && hasBuild) {
    LOGI("lib3mf version = %u.%u.%u-%s+%s", nMajor, nMinor, nMicro,
         sReleaseInfo.c_str(), sBuildInfo.c_str());
  } else if (hasRelease) {
    LOGI("lib3mf version = %u.%u.%u-%s", nMajor, nMinor, nMicro,
         sReleaseInfo.c_str());
  } else if (hasBuild) {
    LOGI("lib3mf version = %u.%u.%u+%s", nMajor, nMinor, nMicro,
         sBuildInfo.c_str());
  } else {
    LOGI("lib3mf version = %u.%u.%u", nMajor, nMinor, nMicro);
  }
}
#endif

extern "C" {
FFI_PLUGIN_EXPORT int32_t extract_info(const char* path) {
#if defined(__arm__) || defined(__aarch64__)
  PWrapper wrapper = CWrapper::loadLibrary();
  std::cout
      << "------------------------------------------------------------------"
      << std::endl;
  std::cout << "3MF Read example" << std::endl;
  printVersion(wrapper);
  std::cout
      << "------------------------------------------------------------------"
      << std::endl;
  return 0;
#else
  return -999;
#endif
}
}

Export ffi function

Refer to the template to write the corresponding search and export for our extractInfo

  int extractInfo(String path) {
    final Pointer<Utf8> charPointer = path.toNativeUtf8();
    int ret = _extractInfo(charPointer);
    malloc.free(charPointer);
    return ret;
  }

  late final _extractInfoPtr =
      _lookup<
        ffi.NativeFunction<
          ffi.Int32 Function(ffi.Pointer<Utf8>) // CPP 签名
        >
      >('extract_info');

  late final _extractInfo = _extractInfoPtr
      .asFunction<
        int Function(ffi.Pointer<Utf8>) // Dart 签名
      >();

Write a test page

import 'package:flutter/material.dart';
import 'package:native_lib3mf/native_lib3mf.dart' as nn;

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

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      constraints: BoxConstraints.expand(),
      child: Center(
        child: Material(
          color: Colors.transparent,
          child: FilledButton(
            onPressed: () {
              debugPrint(nn.extractInfo("a").toString());
            },
            child: Text('lib3mf extractInfo'),
          ),
        ),
      ),
    );
  }
}

Actual running output

[+5453 ms] D/ProfileInstaller(24149): Installing profile for com.chaosgoo.metasequoia.metasequoia
[+5565 ms] I/NativeLib3mf(24149): lib3mf version = 2.4.1
[   +4 ms] I/flutter (24149): 0

Judging from the log output, lib3mf has been successfully loaded and the version number has been returned. The next step is to connect to the real lib3mf function.

Summary

In this article, the Flutter FFI plug-in was successfully created, lib3mf.so was compiled for Android, CMake was used to configure and handle multi-ABI compatibility, and the version query function of lib3mf was finally called in Flutter. The next article will improve the Extract3mfInfo function to realize the function of reading 3mf file information.


Share this post on:

Previous Post
Integrating lib3mf, Part 3: Reading 3MF File Information in Flutter
Next Post
Integrating lib3mf, Part 1: Building the Library and Running the Examples