
flutter 无胶水代码构建打包 ffi plugin
HumphreyDanflutter ffi plugin 开发有3种模式:
- 有 ffi 源码, flutter 可以直接打包构建
- 没有 ffi 源码,只有预购建好的库或者二进制文件
- 有 ffi 源码,也有二进制
其中1和3都比较简单,因为 flutter ffi plugin 模版默认都把构建打包脚本生成好了,只需要修改一下即可用.
模式2在全网搜了没有找到先例. 本文就是主要讨论如何在 flutter ffi plugin 项目中构建打包预编译二进制文件而不需要胶水代码.
这里以我自己写的 flutter js engine 为例.
社区已经有一个 flutter_js 引擎, 但是在使用过程中发现其在同时运行多个 JSContext 时会被顺序执行,无法并行执行.
所以借鉴这个项目,实现了一个基于 JavaScriptCore 和 Quickjs 引擎的 flutter 库.支持 Android/iOS/MacOS/Windows/Linux(后续计划通过基于 quickjs-wasm 支持 Web).
flujs
1、依赖库打包
依赖的 JavaScriptCore iOS/macOS 系统默认自带,所以不需要单独打包.
quickjs 主要有两个版本:
- bellard 原版 截止目前最新版是 2024-01-13
- quickjs-ng 截止目前最新版是 0.7.0
因为 quickjs-ng 项目还在持续维护更新,所以这里选择它.
- git clone quickjs 并 checkout 到 v0.7.0 tag
- 编写 xmake 脚本,见xmake quickjs-ng 交叉编译实践
- 编译生成跨平台动态库
- 将各个平台的动态库打包为 tar.gz
2、新建 flutter ffi plugin project
#android在 build.gradle
dependencies
中添加 implementation('cn.humphreyd.flujs-android:quickjs:0.6.1.2')
上面是需要手动将 qjs 动态库打包为aar,然后发布到 mvn 仓库.详情见安卓发布纯so库到maven
相比较之下还是比较麻烦,所以直接使用 gradle 脚本下载 so 到 jniLibs 并打包
1 | def targetAbis = android.defaultConfig.ndk.abiFilters |
1 | require "open-uri" |
#iOS
1 | flujsutil = FlujsUtil.new('iphoneos') |
#macOS
1 | flujsutil = FlujsUtil.new('macosx') |
#linux/windows
linux 和 mac 都使用 cmake 编译,所以这里共用一套逻辑.
script/artifact.cmake
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119 # Set BASEURL from environment variable or use default
if(DEFINED ENV{BASEURL})
set(BASEURL "$ENV{BASEURL}")
else()
set(BASEURL "https://gitee.com/flujs_project/flujs_libs_quickjs/releases/download")
endif()
# Set QJS_VERSION from environment variable or use default
if(DEFINED ENV{QJS_VERSION})
set(QJS_VERSION "$ENV{QJS_VERSION}")
else()
set(QJS_VERSION "0.8.0")
endif()
if(DEFINED ENV{QJS_TAR_NAME})
set(QJS_TAR_NAME "$ENV{QJS_TAR_NAME}")
elseif(NOT DEFINED QJS_TAR_NAME)
if(CMAKE_HOST_SYSTEM_NAME MATCHES "Linux")
if (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "x86_64")
set(QJS_TAR_NAME "qjs_linux_x86_64.tar.gz")
elseif (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "arm64")
set(QJS_TAR_NAME "qjs_linux_arm64.tar.gz")
else()
message(FATAL_ERROR "Unknown CPU!")
endif()
elseif(CMAKE_HOST_SYSTEM_NAME MATCHES "Windows")
if (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "X86" OR ${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL "AMD64" OR ${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL "IA64")
set(QJS_TAR_NAME "qjs_mingw_x86_64.zip")
elseif (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "ARM64")
set(CPU "arm64")
message(FATAL_ERROR "Unknown CPU!")
else()
message(FATAL_ERROR "Unknown CPU!")
endif()
endif()
endif()
if(DEFINED ENV{FORCE_DOWNLOAD})
set(FORCE_DOWNLOAD "$ENV{FORCE_DOWNLOAD}")
else()
set(FORCE_DOWNLOAD FALSE)
endif()
# download quickjs and set QJS_LIBRARY
function(prepareQJS)
set(QJS_TAR_PATH "${CMAKE_BINARY_DIR}/${QJS_TAR_NAME}")
if(CMAKE_HOST_SYSTEM_NAME MATCHES "Linux")
set(QJS_LIBRARY "${CMAKE_BINARY_DIR}/libqjs.so") # cannot use PARENT_SCOPE, it will not check the existence of QJS_LIBRARY
elseif(CMAKE_HOST_SYSTEM_NAME MATCHES "Windows")
set(QJS_LIBRARY "${CMAKE_BINARY_DIR}/release/lib/libqjs.dll")
endif()
message(STATUS "[prepareQJS] QJS_LIBRARY = ${QJS_LIBRARY} ${FORCE_DOWNLOAD}")
if(EXISTS "${QJS_LIBRARY}" AND NOT FORCE_DOWNLOAD)
message(STATUS "[prepareQJS] ${QJS_LIBRARY} already exists, skipping download and extraction. use flutter clean, and rerun if you insist on download and extraction. or set FORCE_DOWNLOAD=true")
set(QJS_LIBRARY "${QJS_LIBRARY}" PARENT_SCOPE)
return()
endif()
message(STATUS "[prepareQJS] ${QJS_LIBRARY_PATH} not found, downloading and extracting...")
set(URL "${BASEURL}/${QJS_VERSION}/${QJS_TAR_NAME}")
downloadVerify("${URL}" ${QJS_TAR_PATH})
if(CMAKE_HOST_SYSTEM_NAME MATCHES "Linux")
add_custom_target(
"QJS_EXTRACT"
ALL
COMMAND "${CMAKE_COMMAND}" -E tar xz "${QJS_TAR_PATH}"
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
COMMENT "Extracting QJS library"
)
set(QJS_LIBRARY "${CMAKE_BINARY_DIR}/libqjs.so" PARENT_SCOPE)
elseif(CMAKE_HOST_SYSTEM_NAME MATCHES "Windows")
execute_process(COMMAND powershell -Command "Expand-Archive -Path '${QJS_TAR_PATH}' -DestinationPath '${CMAKE_BINARY_DIR}' -Force" RESULT_VARIABLE extractResult)
if(NOT extractResult EQUAL 0)
message(FATAL_ERROR "Failed to extract ZIP file: ${QJS_TAR_PATH}")
endif()
set(QJS_LIBRARY "${CMAKE_BINARY_DIR}/release/lib/libqjs.dll" PARENT_SCOPE)
endif()
endfunction(prepareQJS)
# download url resource to locationForArchive
function(downloadVerify url locationForArchive )
message(STATUS "[downloadVerify] info = ${url}")
if (EXISTS "${locationForArchive}" AND NOT FORCE_DOWNLOAD)
message(STATUS "[downloadVerify] ${locationForArchive} exists!.")
return()
endif()
if(NOT EXISTS "${locationForArchive}")
message(STATUS "[downloadVerify] downloading archive from ${url}...")
file(DOWNLOAD "${url}" "${locationForArchive}"
STATUS download_status
LOG download_log)
message(STATUS "[downloadVerify] archive saved to ${locationForArchive}")
# check download status
list(GET download_status 0 status_code)
if(NOT status_code EQUAL 0)
message(FATAL_ERROR "Error downloading ${download_log}")
endif()
endif()
message(STATUS "[downloadVerify] succeed! ${locationForArchive}")
endfunction(downloadVerify url md5 locationForArchive)
# check target dir is empty, return result if it is true;
function(checkDirectoryEmpty dir result)
if(EXISTS "${dir}")
file(GLOB dir_contents "${dir}/*")
if(dir_contents)
set(${result} FALSE PARENT_SCOPE)
else()
set(${result} TRUE PARENT_SCOPE)
message(WARN "[checkDirectoryEmpty] Directory ${dir} exists but empty!")
endif()
else()
set(${result} TRUE PARENT_SCOPE)
message(WARN "[checkDirectoryEmpty] Directory ${dir} not exists!")
endif()
endfunction(checkDirectoryEmpty)
因为 linux/windows 公用一套逻辑,所以将 QJS 设置为 IMPORTED 对象,并拆分为共用的 CMakeLists.txt, 然后在各自 module 中 add_subdirectory 即可
1 | cmake_minimum_required(VERSION 3.10) |
最后在 linux/windows 目录的 CMakeLists.txt
中引入即可
1 | add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../qjs/src" "${CMAKE_CURRENT_BINARY_DIR}/shared") |
注意: linux/windows cmake 中必须要设置
set("flujs_qjs_bundled_libraries" $<TARGET_FILE:${QJS}> PARENT_SCOPE)
$<TARGET_FILE:${QJS}>
就是libqjs.dll/libqjs.so
的位置,这样打包时 cmake 才会通过 install 命令将动态库复制到产物目录