uni-gyp 是一个跨平台编译工具,用于把一个C/C++语言编写的库,编译成uasm插件的App平台版本。
uni-gyp 基于 gyp-next,扩展了Android、iOS 和 HarmonyOS 的 CMake 工程生成及编译能力。
这三个目标平台的编译不依赖 node-gyp;node-gyp 仅用于对比 Node.js 在桌面环境中的构建差异和执行相关测试。
uni-gyp)以上要求适用于 Android、HarmonyOS 和 iOS 平台编译,不要求安装或使用node-gyp。只有执行桌面 Node.js 对比测试时才会使用项目中的 node-gyp;
当前版本支持 Node.js 22.22.2+、24.15.0+ 或 26+。
可以先检查本机环境:
node --version
python3 --version
cmake --version
ninja --version
Windows 上如果没有 python3 命令,可改用:
py -3 --version
cmake 命令位于 PATH 中。 xcode-select --install
cmake.exe 和 ninja.exe 所在目录已加入PATH。只需将 ANDROID_NDK_HOME 指向 Android NDK 目录即可编译:
ANDROID_NDK_HOME:具体 NDK 版本目录,目录中应存在build/cmake/android.toolchain.cmake。如果没有配置 ANDROID_NDK_HOME,uni-gyp 也会尝试通过
ANDROID_SDK_ROOT、ANDROID_HOME 或 adb 自动查找已安装的最新 NDK。
ANDROID_SDK_ROOT 仅用于自动查找 NDK,并不是必需的。
macOS:
export ANDROID_NDK_HOME="/path/to/android-ndk"
如需永久生效,将以上内容写入 ~/.zshrc,然后执行:
source ~/.zshrc
Windows PowerShell(仅对当前终端生效):
$env:ANDROID_NDK_HOME = "C:\path\to\android-ndk"
$env:Path = "C:\Program Files\CMake\bin;C:\path\to\ninja;$env:Path"
如需永久生效,在 Windows 的“编辑用户环境变量”中添加ANDROID_NDK_HOME,并将 CMake 和 Ninja 的目录添加到用户 Path,然后重新打开终端。
推荐配置 OHOS_NDK_HOME。它可以指向以下任一目录:
native 目录;native 同级的 toolchains 目录。最终解析出的 Native SDK 目录中必须存在build/cmake/ohos.toolchain.cmake。
macOS:
export OHOS_NDK_HOME="/path/to/openharmony/native"
同样可将配置写入 ~/.zshrc 以永久生效。
Windows PowerShell:
$env:OHOS_NDK_HOME = "C:\path\to\openharmony\native"
Windows 上如需永久生效,在“编辑用户环境变量”中添加
OHOS_NDK_HOME。项目也兼容 HARMONY_NDK_HOME、OHOS_SDK_NATIVE 和 HARMONY_SDK_HOME。
uni-gyp 通常会自动查找 Python。如果本机安装了多个 Python,或自动查找失败,可通过 UNI_GYP_PYTHON 指定 Python 3.9 及以上版本:
macOS:
export UNI_GYP_PYTHON="/path/to/python3"
Windows PowerShell:
$env:UNI_GYP_PYTHON = "C:\path\to\python.exe"
新建项目目录,在项目根目录执行:
# 初始化
npm init
# 安装uni-gyp
pnpm add -D @dcloudio/uni-gyp
# or
npm install --save-dev @dcloudio/uni-gyp
# 可选napi风格,添加node-addon-api
npm i node-addon-api -save
# 可选桌面测试,添加node-gyp
npm i node-gyp -save
或全局安装 uni-gyp 创建项目
# 基础模版
uni-gyp create <plugin-id>
# 包含UTS插件模版
uni-gyp create <plugin-id> --template basic-and-uts
命令参数:
| 命令/参数 | 值 | 描述 |
|---|---|---|
| -platform | android / ios / harmony | 生成对应平台的 CMake 工程 |
| -arch | arm64 / x64 | 指定 CPU 架构,默认 arm64, iOS 支持多个架构用逗号分隔(模拟器) |
| --deployment-target | 15.0 | 指定 iOS 最小支持版本 |
| --build | Debug / Release | 执行构建 |
| module-pack | --target <plugin-id> | 生成 uni-module,插件名由 --target 指定 |
arm64。示例:
# 只生成 build/<platform>/<abi>/CMakeLists.txt
uni-gyp -platform=android --depth=. source/binding.gyp
# 生成 + 执行编译(Release模式)
uni-gyp -platform=android --depth=. --build=Release source/binding.gyp
# 指定 CPU 架构(Android 示例,x64 同理)
uni-gyp -platform=android --depth=. -arch=arm64 --build=Release source/binding.gyp
输出目录结构规则:
build/android/<abi>/<configuration>/lib.target/lib<UasmProduct>.so
build/harmony/<arch>/<configuration>/lib.target/lib<UasmProduct>.so
build/ios/xcframework/<configuration>/<UasmProduct>.xcframework
// 生成目录为 `uni_modules/<plugin-id>`
uni-gyp module-pack --target <plugin-id>
plugin-id 为插件目录名即插件id输入/输出结构规则
build/android/<abi>/<configuration>/lib.target/lib<UasmProduct>.so
-> uni_modules/<target>/uasm/app-android/libs/<abi>/lib<UasmProduct>.so
build/harmony/<arch>/<configuration>/lib.target/lib<UasmProduct>.so
-> uni_modules/<target>/uasm/app-harmony/libs/<arch>/lib<UasmProduct>.so
build/ios/xcframework/<configuration>/<UasmProduct>.xcframework
-> uni_modules/<target>/uasm/app-ios/frameworks/<UasmProduct>.xcframework
本示例使用zstd演示,zstd是一个C语言编写的压缩解压库。
项目源码gitcode仓库https://gitcode.com/dcloud/uasm-zstd
. 项目根目录
|-- zstd
| |-- lib // zstd C源码目录
| |-- binding.gyp // gyp 编译配置
| |-- android.exports // gyp 编译配置 --> 自定义平台导出符号配置
| |-- ios.exports // gyp 编译配置 --> 自定义平台导出符号配置
| |-- zstd_codec.h // cxx 通用封装
| |-- zstd_codec.cc // cxx 通用封装
| |-- jni_binding.cc // JNI 绑定
| |-- Zstd.kt // JNI 绑定
| |-- zstd.h // swift 绑定
| |-- swift_binding.h // swift 绑定
| |-- swift_binding.cc // swift 绑定
| |-- Zstd.swift // swift 绑定
| |-- wasm_binding.cc // WebAssembly 绑定
|-- package.json
zstd/binding.gyp 文件内容
{
'variables': {
"uni-gyp-root-dir%": "<!(node -p \"require('uni-gyp').root_dir\")",
'module-root-dir%': '<!(node -p "require(\'path\').resolve(\'.\')")',
'zstd_sources': [
# cxx_library(name='debug')
'lib/common/debug.c',
# cxx_library(name='bitstream')
# [no .c files]
# cxx_library(name='cpu')
# [no .c files]
# cxx_library(name='entropy')
'lib/common/entropy_common.c',
'lib/common/fse_decompress.c',
'lib/compress/fse_compress.c',
'lib/compress/huf_compress.c',
'lib/decompress/huf_decompress.c',
# cxx_library(name='pool')
'lib/common/pool.c',
# cxx_library(name='threading')
'lib/common/threading.c',
# cxx_library(name='xxhash')
'lib/common/xxhash.c',
# cxx_library(name='zstd_common')
'lib/common/zstd_common.c',
# cxx_library(name='errors')
'lib/common/error_private.c',
# cxx_library(name='mem')
# [no .c files]
# cxx_library(name='compiler')
# [no .c files]
# cxx_library(name='compress')
'lib/compress/hist.c',
# glob(compress/zstd*.c)
'lib/compress/zstd_compress.c',
'lib/compress/zstd_compress_literals.c',
'lib/compress/zstd_compress_sequences.c',
'lib/compress/zstd_compress_superblock.c',
'lib/compress/zstd_double_fast.c',
'lib/compress/zstd_fast.c',
'lib/compress/zstd_lazy.c',
'lib/compress/zstd_ldm.c',
'lib/compress/zstd_opt.c',
'lib/compress/zstd_preSplit.c',
'lib/compress/zstdmt_compress.c',
# cxx_library(name='decompress')
# glob(decompress/zstd*.c)
'lib/decompress/zstd_ddict.c',
'lib/decompress/zstd_decompress.c',
'lib/decompress/zstd_decompress_block.c',
'zstd_codec.cc',
'binding.cc',
],
},
'targets': [
{
'target_name': 'UasmUniZstd',
'type': 'shared_library',
'include_dirs': ['lib'],
# -pthread?
'direct_dependent_settings': {
'include_dirs': [ 'lib' ]
},
'defines': [
# cxx_library(name='xxhash')
'XXH_NAMESPACE=ZSTD_',
# cxx_library(name='threading')
'ZSTD_MULTITHREAD',
# TODO: Use deps/zstd/lib/decompress/huf_decompress_amd64.S.
'ZSTD_DISABLE_ASM',
],
'all_dependent_settings': {
'defines': [
'XXH_NAMESPACE=ZSTD_',
'ZSTD_MULTITHREAD',
# TODO: Use deps/zstd/lib/decompress/huf_decompress_amd64.S.
'ZSTD_DISABLE_ASM',
],
},
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")",
],
"cflags_cc": [ "-std=c++20", "-fno-exceptions" ],
'conditions': [
['OS == "android"', {
'defines': [
'GYP_ANDROID=1',
],
'sources': [
'jni_binding.cc',
],
'ldflags': [
'-Wl,--unresolved-symbols=ignore-all',
'-Wl,-exported_symbols_list,<(module-root-dir)/android.exports',
],
}],
['OS == "ios"', {
'type': 'shared_library',
'mac_bundle': 1,
'sources': [
'swift_binding.cc',
'swift_binding.h',
'zstd.h',
],
'mac_framework_headers': [
'swift_binding.h',
'zstd.h',
],
'defines': [
'GYP_IOS=1',
],
'xcode_settings': {
'PRODUCT_BUNDLE_IDENTIFIER': 'uts.sdk.modules.uniZstd',
},
'ldflags': [
'-Wl,-undefined,dynamic_lookup',
'-Wl,-exported_symbols_list,<(module-root-dir)/ios.exports',
],
}],
['OS == "harmony"', {
"type": "shared_library",
"defines": [
"GYP_HARMONY=1",
],
'libraries': [
'-lace_napi.z',
],
"ldflags": [
"-Wl,--unresolved-symbols=ignore-all",
"-Wl,--version-script=<(module-root-dir)/harmony.exports",
],
}],
],
'sources': [
'<@(zstd_sources)',
]
}
]
}
package.json 文件内容, 仅展示需要的配置
{
"name": "uni-zstd",
"version": "1.0.0",
"description": "uni-zstd-plugin",
"gypfile": true,
"scripts": {
"build:uni-zstd:android:release": "uni-gyp -platform=android --depth=. -arch=arm64 --build=Release zstd/binding.gyp && uni-gyp -platform=android --depth=. -arch=x64 --build=Release zstd/binding.gyp",
"build:uni-zstd:harmony:release": "uni-gyp -platform=harmony --depth=. -arch=arm64 --build=Release zstd/binding.gyp && uni-gyp -platform=harmony --depth=. -arch=x64 --build=Release zstd/binding.gyp",
"build:uni-zstd:ios:release": "uni-gyp -platform=ios --depth=. -sdk=all -arch=arm64,x64 --build Release zstd/binding.gyp",
"build:uni-zstd:pack:uni-module:release": "uni-gyp module-pack --target uni-zstd",
"build:uni-zstd:all": "npm run build:uni-zstd:android:release && npm run build:uni-zstd:harmony:release && npm run build:uni-zstd:ios:release && npm run build:uni-zstd:pack:uni-module:release",
},
"dependencies": {
}
}
一个命令生成 uni-module
// 生成目录为 `uni_modules/uni-zstd`
npm run build:uni-zstd:all
注意:上面的命令包含编译iOS的xcframework,仅在macOS上支持
ace_napi.z, node api运行时默认情况下,编译器会把所有符号都导出到动态符号表,这既会增加产物体积,也可能与非预期的同名符号发生冲突。
通过 android.exports、harmony.exports 的 version-script 以及 ios.exports 的导出白名单,可以只保留需要对外暴露的符号。
如果怀疑某个符号没有导出、或者导出了不该导出的符号,可以按下述方式检查。
借助 NDK 自带的 LLVM 工具,工具目录一般为
$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/<host-tag>/bin/(<host-tag> 如 darwin-x86_64、linux-x86_64、windows-x86_64)。
# 仅列出 so 的动态符号表中已定义的导出符号(-D 表示动态符号表,--defined-only 排除未定义引用)
llvm-nm -D --defined-only lib<UasmProduct>.so
# 更详细地查看动态符号表,包含符号类型、绑定方式、可见性
llvm-readelf -sW --dyn-syms lib<UasmProduct>.so
# 查看链接的动态库依赖
llvm-readelf -d lib<UasmProduct>.so
# 查看符号版本信息(version-script 生效后会生成版本节点)
llvm-readelf --version-info lib<UasmProduct>.so
android.exports 是否把该符号放进了 global:,其余符号应通过 local: *; 全部隐藏。-fvisibility=hidden 编译时使用了 __attribute__((visibility("default")));也可以检查是否被 local: 规则误隐藏。ldflags 中追加 -Wl,--exclude-libs,ALL 隐藏。工具位于 OpenHarmony NDK 的 llvm/bin/ 下,命令与 Android 基本一致:
$OHOS_NDK_HOME/llvm/bin/llvm-nm -D --defined-only lib<UasmProduct>.so
$OHOS_NDK_HOME/llvm/bin/llvm-readelf -sW --dyn-syms lib<UasmProduct>.so
$OHOS_NDK_HOME/llvm/bin/llvm-readelf --version-info lib<UasmProduct>.so
-Wl,--version-script=<...>/harmony.exports 控制导出;如需在项目内自定义,可参考 HarmonyOS 的 .exports 配置方式。xcframework 由多个架构目录组成,分析前先确认目标架构对应的二进制路径:
# 查看 xcframework 包含的架构
ls <UasmProduct>.xcframework
# 查看指定架构二进制的架构信息
lipo -info <UasmProduct>.xcframework/ios-arm64/<UasmProduct>.framework/<UasmProduct>
# 列出导出的全局已定义符号,并去掉 Mach-O 的符号前导下划线
nm -gU <UasmProduct>.xcframework/ios-arm64/<UasmProduct>.framework/<UasmProduct>
# 仅查看导出符号(exports trie,即真正对外可见的符号)
xcrun dyld_info -exports <UasmProduct>.xcframework/ios-arm64/<UasmProduct>.framework/<UasmProduct>
# 查看动态库依赖
otool -L <UasmProduct>.xcframework/ios-arm64/<UasmProduct>.framework/<UasmProduct>
uni.loadUasm() / uni.loadUasmSync() 通过 dlopen 加载,因此对外可见的符号必须出现在 ios.exports 白名单中,否则运行期 dlsym 会取不到符号。ios.exports 中的符号名带前导下划线(如 _uasm_test_add),对应 C 函数 uasm_test_add。nm -gU 的输出与 ios.exports,多出的符号应加入隐藏或收窄编译选项 -fvisibility=hidden。按平台查看产物各段/各架构的体积构成,定位是代码、调试信息还是多架构合并导致的。
Android / HarmonyOS:
# 按节区统计体积(text/data/bss 及调试节)
llvm-size -A lib<UasmProduct>.so
# 检查是否残留调试信息
llvm-readelf -S lib<UasmProduct>.so | grep -i debug
# 按符号统计体积,找出占用最大的符号(前 20 个)
llvm-nm -D -S --size-sort lib<UasmProduct>.so | tail -n 20
iOS:
# 按段/节统计大小
size -m <UasmProduct>.framework/<UasmProduct>
# 按符号统计体积(降序)
nm -S --size-sort -gU <UasmProduct>.framework/<UasmProduct>
# 查看 xcframework 内各架构二进制的体积
du -sh <UasmProduct>.xcframework/*
# 查看各架构 / 静态库体积
lipo -info <UasmProduct>.xcframework/ios-arm64/<UasmProduct>.framework/<UasmProduct>
ldflags 中追加 -Wl,--strip-debug,或使用 llvm-strip --strip-unneeded lib<UasmProduct>.so;iOS 由 Xcode 的 STRIP_INSTALLED_PRODUCT / DEPLOYMENT_POSTPROCESSING 控制,也可手动 strip -x。ios.exports 收窄到最小集合,并配合 -Wl,--exclude-libs,ALL 隐藏静态库符号。-ffunction-sections -fdata-sections,链接时开启 -Wl,--gc-sections(Android/HarmonyOS)或 -Wl,-dead_strip(iOS),可剔除未被引用的函数与数据。-fvisibility=hidden -fvisibility-inlines-hidden 编译,仅对需要导出的接口加 __attribute__((visibility("default")))。libc++_shared、libc++abi 等静态链接进产物;Android 可参考示例用 -nostdlib++ 并由 App 提供运行库。ios-arm64、ios-arm64_x86_64-simulator 等目录,最终包体会成倍增大。发布前确认只保留需要的架构(如仅真机包时移除模拟器切片)。binding.gyp 的 sources/libraries 是否重复引入相同源码或库文件。-flto,iOS LLVM_LTO),通常能同时减小体积并提升性能。mac_framework_headers、resources 等若把非必要文件打进产物,也会增加包体,需按需裁剪。