在 flutter 上使用 c 代碼 - (一) 有源碼的項目

在 flutter 的 1.10.x 後的分支, dart:ffi 被併入 flutter, 現在 flutter 中也可以使用 ffi 了。
這東西是啥玩意呢, 就是讓 dart 可以直接調用 c/c++ 代碼等東西的庫, FFI(foreign function interface), 官方文檔在這裏。

但是在當前版本中, 這東西在官方說明中依然處於技術預覽版, 就是可用, 但後續不保證 api 不變更。

開發環境

首先我是 mac 系統, windows 系統不保證腳本的可用和工具的可用, linux 的話可能一些必要工具需要使用自己平臺的包管理工具, 並且涉及到 ios 部分, 必須使用 mac。

所有需要的工具包

  • Xcode(或 XcodeBuild 命令行工具)
  • brew
  • clang
  • cmake
  • Android 工具鏈
    • Android SDK
    • NDK
    • Android Studio(可選)
    • Gradle
  • Flutter 工具鏈
    • SDK 1.10.x+
  • vscode(可選, 這東西看你的情況,作爲示例的話只要是文本編輯器即可, 我本人使用這個作爲主要的文本編輯器)

這裏說的是包含後續所有用到的東西, 並不僅僅是本文。
其中對於 flutter 開發者可能需要單獨安裝的應該只有 NDK 和 Cmake, 這兩個東西是包含在 android sdk 下的, 可以使用 android studio 下載, 也可以單獨下載

ffi 的簡單介紹

根據官方文檔說明

可以理解爲, 將 c 的類型和 dart 的類型關聯起來, 然後 ffi 會在內部將兩端關聯起來, 完成調用

有如下幾種類型
20191104142700.png

基本就是對應 c 中的類型, 對應 Void 各種長度的 有無符號的整型, 單雙精度浮點, 指針, 方法

轉化的過程

c 源碼核心就這點, 其他的都做不知即可

void hello_world()
{
    printf("Hello World\n");
}

導包, 這個是第一步要做的

import 'dart:ffi' as ffi;
// 定義一個ffi類型
typedef hello_world_func = ffi.Void Function();

// 將ffi類型定義爲dart類型
typedef HelloWorld = void Function();

// 打開動態庫, dylib是mac上的動態庫的後綴
final dylib = ffi.DynamicLibrary.open('hello_world.dylib');

// 這裏是最難理解的一步, 後面會詳細解說
final HelloWorld hello = dylib
    .lookup<ffi.NativeFunction<hello_world_func>>('hello_world')
    .asFunction();

// 調用
hello();

詳細理解轉化過程

這裏以 lookup 方法爲切入點,詳細理解下這裏做了什麼, 以便於後面我們可以自行完成這個過程

lookup 方法簽名如下:

external Pointer<T> lookup<T extends NativeType>(String symbolName);

參數

很好理解, 傳入一個方法名, 讓我們能找到 c 方法

泛型

這個是方法的類型簽名的 dart:ffi 表現形式.

c 方法的簽名是這樣的: void hello_world(), 所以我們就需要一個對應的類型, 也就是上面定義的 ffi 類型

ffi.Void Function()

返回類型

這裏的返回值是用於在實際調用時,轉化 c 方法的返回值爲 dart 的類型來使用的, 所以就是對應的 dart 類型


/// 定義是這樣的
void Function()

/// 接收的asFunction方法
final void Function() hello = XXXX;

寫起來的時候可能是這樣的,

實例

extern "C" {
// __attribute__((visibility("default"))) __attribute__((used)) // 雖然說需要這行, 但是沒這行也沒報錯
int32_t native_add(int32_t x, int32_t y) { return x + y; }

double double_add(double x, double y) { return x + y; }
}

import 'dart:ffi';

final DynamicLibrary dylib = Platform.isAndroid
    ? DynamicLibrary.open("libnative_add.so")
    : DynamicLibrary.open("native_add.framework/native_add");

final int Function(int x, int y) nativeAdd = dylib
    .lookup<NativeFunction<Int32 Function(Int32, Int32)>>("native_add")
    .asFunction();

final double Function(double, double) doubleAdd = dylib
    .lookup<NativeFunction<Double Function(Double, Double)>>("double_add")
    .asFunction();

打包和運行

在 dart vm 中,可以有多種方案, 只要能編譯出 dylib 即可

官方的hello world 示例中是直接使用 make, 內部使用 gcc 打包編譯

這裏有一個腳本,是設置 dylib 的目錄到環境變量中, 以便於運行時可以找到動態庫

在 flutter 中使用

接着就要開始在 flutter 中使用了, 和在 dart vm 中使用不一樣, 不能使用環境變量, 而是需要將庫置入到項目中

創建倉庫

直接使用 $ flutter create -t plugin native_add 的方式即可

cpp 文件

native_add.cpp

#include <stdint.h>

extern "C" {
// __attribute__((visibility("default"))) __attribute__((used))
int32_t native_add(int32_t x, int32_t y) { return x + y; }

double double_add(double x, double y) { return x + y; }
}

dart 文件

final DynamicLibrary dylib = Platform.isAndroid
    ? DynamicLibrary.open("libnative_add.so")
    : DynamicLibrary.open("native_add.framework/native_add");

final int Function(int x, int y) nativeAdd = dylib
    .lookup<NativeFunction<Int32 Function(Int32, Int32)>>("native_add")
    .asFunction();

final double Function(double, double) doubleAdd = dylib
    .lookup<NativeFunction<Double Function(Double, Double)>>("double_add")
    .asFunction();

界面:


class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter = nativeAdd(_counter, 1);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
            Text(
                "native double value = ${doubleAdd(_counter.toDouble(), _counter.toDouble())}"),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

ios

ios 中, 直接將 cpp 文件置入 ios/classes 文件夾內即可, 然後因爲 podspec 中包含默認配置的原因, 這個文件會被自動引入項目

s.source_files = 'Classes/**/*'

運行項目:

20191104162925.png

Android

android 中其實有兩種方法, 一是用傳統的 ndk 方式, 就是 Android.mk 那種方案, 我們略過這種方案, 因爲配置比較複雜, 我們使用第二種方案, 官方推薦的 cmake 方案

因爲 ios 中, 文件被置入源碼中, 我這裏直接使用相對路徑去引入這個文件

CMakeLists.txt:

cmake_minimum_required(VERSION 3.4.1)  # for example

add_library( native_add

    # Sets the library as a shared library.
    SHARED

    # Provides a relative path to your source file(s).
    ../ios/Classes/native_add.cpp )

  1. 指定源碼對應的庫是哪個庫
  2. 指定庫的類型, 這裏是動態庫, 所以用 SHARED
  3. 指定源碼目錄

然後因爲我們使用了 cmake, 爲了讓安卓項目知道, 我們需要修改 gradle 文件

android{
    // ...
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

這裏在 android 節點下, 添加屬性即可, 這裏是指定 Cmake 使用的文件

接着就可以運行項目了, 和 android 中一樣

20191104163902.png

簡單總結

現在 ffi 處於初始階段, 還有諸多不足.

比如, 文檔的缺失, 現在如何傳遞字符串,數組都是問題, 雖然有結構體的定義, 也能看到部分說明, 但沒有簡單的示例幫助開發者快速使用.

只有基本數據類型, 目前可能還不需要借用 c 來解決, 未來則要看 ffi 會開放到什麼程度.

後記

項目地址: https://github.com/caijinglong/example_for_flutter_ffi

以上

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章